incanto 0.74.0 → 0.76.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 +861 -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-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  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-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  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-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  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-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -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 +29 -8
  54. package/skills/incanto-environment.md +257 -14
  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 +398 -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 +157 -3
  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,29 @@ 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
+ **`slopeLimitDeg` (55) is the steepest ground the character STANDS on**, and
112
+ below it a character does not slide at all — measured, on a hillside, for as
113
+ long as you leave it there. It used to slide down everything: 0.18 m in three
114
+ seconds on a 6.9° slope, 2.87 m on a 27° one, always exactly downhill. The
115
+ cause is the hover spring measuring its clearance STRAIGHT DOWN while the
116
+ clearance that decides whether the capsule touches is measured perpendicular
117
+ to the surface — negative past ~13° — and every rig setting `friction: 0` (the
118
+ right setting: the character rides the spring, so floor friction only brakes
119
+ it) then slid on the contact. So the spring aims perpendicular now, and on
120
+ ground inside the limit the controller cancels downhill drift it was not asked
121
+ for. Steeper than the limit the ground stops being ground — no `grounded`, no
122
+ jump reset — and the body slips off the face, which is what a slope limit means
123
+ everywhere else. 55 is a person; a goat is 80, something on wheels 25, and `90`
124
+ is the old no-limit behaviour. `controller.groundSlopeDeg` reports what it is
125
+ standing on, and `controller.groundNormal` which way that faces.
126
+
127
+ **`stepHeight` steps onto a ledge at the APEX of a jump too.** A fence 1.5 m
128
+ high was climbed by a jump that peaks at 1.3 m: at the apex the fence top sat
129
+ 0.2 m above the capsule's bottom, inside `stepHeight` 0.4, and the step-up
130
+ lifted the character over. That is what makes landing on a ledge forgiving; a
131
+ wall that must stop a walker is jump apex + `stepHeight` tall (`examples/steed-3d`'s
132
+ fence is 2 m).
133
+
95
134
  **Jump reads the button EDGE.** Holding the jump key no longer re-jumps every
96
135
  frame — that auto-hop was never intended and it made coyote time incoherent (a
97
136
  held button would re-fire through the whole window).
@@ -126,6 +165,248 @@ at rest: 0.01 → y 0.839, 0.05 → 0.840, 0.3 → 1.008, 0.6 → 1.308 for a ca
126
165
  contact height is 0.84. Leave it at the default and use `stepHeight` for ledges (the
127
166
  docstring's own advice: a raised `floatHeight` makes the character weightless).
128
167
 
168
+ ## Swimming (water too deep to stand in)
169
+
170
+ A `Water3D` is a surface with no bottom, and until now the controller never
171
+ asked it anything: a player who walked off a beach into a four-metre lagoon
172
+ stood on the bed with the camera underwater and the run clip playing, in every
173
+ 3D scene with water. Now the body SWIMS:
174
+
175
+ | prop | default | what it buys |
176
+ |---|---|---|
177
+ | `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. |
178
+ | `swimDepth` | `null` | how far the body's ORIGIN rides below the surface. `null` derives it from the capsule so the head rides 15 cm clear and the rest is in the water — which is what treading water looks like, and what a fixed number cannot be for every character size. (It used to default to a flat `0.35`, and on the 1.68 m capsule every template ships that put the head 0.46 m clear: the character floated along on top like a boat.) A number still wins. |
179
+ | `diveAction` | `"dive"` | held: swim DOWN at `swimSpeed`. Not in the input map = no diving, silently. |
180
+
181
+ - **When.** The body swims when floating would put it HIGHER than standing:
182
+ the waterline (`surface − swimDepth`) above where the hover spring would
183
+ hold it on the bed it can see, or no bed within reach. That one comparison
184
+ is the whole wade-in/wade-out — walk down a beach and the water takes you
185
+ where the bed drops away; swim toward a shelf and the spring takes over
186
+ where it rises. No zones, no triggers.
187
+ - **What.** Gravity is off, a spring holds the origin at the waterline (a
188
+ swimmer rides the swell the way a `Buoyancy` raft does — the same
189
+ `heightAt`), horizontal control is full (water gives purchase), and the
190
+ state is `swim` for the `animations` map / `movementStateChanged`.
191
+ - **Diving.** Hold `dive` to go down; let go and you rise on your own. With
192
+ the head under (`controller.submerged`), holding `jump` swims UP. Air,
193
+ drowning and what is down there are your game's business — read
194
+ `controller.submerged` in a behavior and count.
195
+ - **Getting out.** A swimmer's step is measured from the WATER, not from the
196
+ foot: swim into a shelf, a dock or a rock whose top is no more than
197
+ `stepHeight` above the surface and the body CLIMBS out on its own — driven
198
+ up at 2 m/s until its bottom is level with the ledge, then onto it. (The
199
+ capsule hangs `swimDepth + halfHeight` under the surface, so from its bottom
200
+ a shelf a hand under the waterline would be a 0.6 m wall; a swimmer at that
201
+ shelf simply puts their hands on it. The hover spring could not do this —
202
+ it is sized to hold a capsule a hair off the ground and stalled with the
203
+ body's round bottom a third of a metre under the ledge, pressed to its
204
+ face.) Higher than `stepHeight` above the water it is a wall, exactly as on
205
+ land. `jump` at the surface hops — SIZED to clear the surface by
206
+ `stepHeight`, whatever `jumpVelocity` says — and the same climb takes a
207
+ ledge on the way down. So author a dock or a rock a swimmer should reach
208
+ **no more than `stepHeight` above the water**. A beach needs nothing: the
209
+ bed rises, the standing height passes the waterline, and you walk out.
210
+ - **Readbacks.** `controller.swimming`, `controller.submerged`.
211
+ - **Found on the way (fixed, same release):** the ground probe's side rays
212
+ start `radius` out from the capsule's centre, so pressed against a wall one
213
+ of them started INSIDE the wall and hit it at distance zero — a character
214
+ hugging a wall in mid-air read as standing, the ground jump came back while
215
+ falling, and a hop out of the water at a rock's face was cancelled the frame
216
+ it started. A zero-distance hit is ignored now. And `wallSlideSpeed` held a
217
+ third of a metre a second loose (the solver added one tick of gravity back
218
+ after the clamp); at the cap gravity stands down.
219
+
220
+ ```json
221
+ { "name": "Controller", "type": "CharacterController3D",
222
+ "props": { "view": "free", "swimSpeed": 1.8, "stepHeight": 0.5,
223
+ "animations": { "idle": "$idle", "run": "$run", "swim": "$anims/swim" } } }
224
+ ```
225
+
226
+ Composed in `examples/lagoon-3d` (a pearl dive with an air meter); the
227
+ mixamorig `swim.glb` clip is on the CDN beside the locomotion set.
228
+
229
+ ## Ladders
230
+
231
+ Every 3D game with a second floor has one, and until now an `Area3D`
232
+ against a wall was a box the player walked into. A ladder is any `Area3D` in
233
+ the controller's `ladderGroup` — no node type, no code:
234
+
235
+ ```json
236
+ { "name": "Ladder", "type": "Area3D", "groups": ["ladder"],
237
+ "props": { "position": [-3, 2.2, 0.35], "collider": { "shape": "box", "size": [1, 4.4, 0.7] } },
238
+ "children": [ …rails and rungs as MeshInstance3D… ] }
239
+ ```
240
+
241
+ | prop | default | what it buys |
242
+ |---|---|---|
243
+ | `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.** |
244
+ | `ladderGroup` | `"ladder"` | which `Area3D`s are ladders |
245
+
246
+ - **Grabbing.** Push UP into a ladder you overlap and you are on it: gravity
247
+ off, held to the rungs, state `climb`. Walking PAST one never grabs it —
248
+ only pushing up does. Falling past one while holding DOWN grabs it too,
249
+ which is how you climb down from a ledge: stand at its edge over the
250
+ ladder, hold back, step off.
251
+ - **The top.** The same climb that takes a swimmer out of the water takes the
252
+ body onto the ledge: the ladder should lean on the FACE of the thing you
253
+ are climbing onto (a terrace, a wall with a platform on top, a loft's
254
+ edge) so the ledge is AHEAD of the climber — a ladder up through a hole in
255
+ a floor has its ledge to the sides, and the climb-out looks forward.
256
+ - **The bottom.** Down while standing at its foot steps off.
257
+ - **Letting go.** `jump` pushes off the rungs, away from the ladder, with a
258
+ moment's lockout so it is not grabbed straight back.
259
+ - **Animation.** The state is `climb` — map it in `animations`. The CDN's
260
+ mixamorig set has no climbing clip; `walk` reads as climbing well enough
261
+ on the rungs (`examples/tower-3d` does that).
262
+ - **Readback.** `controller.onLadder`.
263
+
264
+ Composed in `examples/tower-3d` ("Lantern Tower": three terraces, three
265
+ ladders, a sweeping beam and a lantern that wants three flasks).
266
+
267
+ ## Mantling (the pull-up)
268
+
269
+ A jump at `jumpVelocity: 5` rises 1.27 m and the capsule's round bottom rides
270
+ a corner up to ~0.3 m more, so a 1.6 m roof is cleared and a 2 m one is a
271
+ wall — a hand short. `mantleHeight: 0.9` catches it: on the way down from
272
+ ANY jump, while the stick is held toward the ledge, a top ahead within that
273
+ height of the capsule's bottom is taken by the same climb that takes a
274
+ swimmer out of the water and a climber off a ladder (driven up at 2 m/s,
275
+ then onto it; state `climb` meanwhile). Above the reach it is still a wall;
276
+ a vertical hop beside a ledge is still a hop. Composed in
277
+ `examples/rooftops-3d`.
278
+
279
+ ## Crouching (`crouchHeight`)
280
+
281
+ A duct to crawl through, a laser to duck under, a desk to hide behind:
282
+ every stealth game and shooter has a crouch, and the controller could not
283
+ make itself shorter. One prop turns it on:
284
+
285
+ ```json
286
+ { "name": "Ctl", "type": "CharacterController3D",
287
+ "props": { "crouchHeight": 0.3, "crouchSpeedMultiplier": 0.5 } }
288
+ ```
289
+
290
+ - **`crouchHeight`** (m, `0` = no crouch) is the capsule's `height` while the
291
+ `crouch` key is held — the standing capsule is `height` + 2 × `radius`
292
+ tall (1.6 m for the default rig), the crouched one `crouchHeight` +
293
+ 2 × `radius` (0.94 m), so a duct with a metre of headroom is crawled
294
+ through and stops a standing character at its mouth. The body sinks by
295
+ half the difference in the same step, so the feet stay put and the eye
296
+ drops with them in first person; the Skin's authored offset is lifted by
297
+ the same amount so the model's feet stay on the floor, and restored on
298
+ standing.
299
+ - **Hold-to-crouch.** The key let go stands you up — if there is headroom.
300
+ A ray from the capsule's top asks; under a duct you stay down until you
301
+ are out from under it, and `crouching` reads true the whole time. A jump
302
+ press while the key is held is not a hop and not a stand.
303
+ - **`crouchSpeedMultiplier`** (0.5) is the pace, as a fraction of the standing
304
+ pace. `state` reads `crouch` (still) and `sneak` (moving), so the
305
+ `animations` map takes clips for both.
306
+ - Declare the action: `"crouch": { "type": "button", "keys": ["KeyC", "ControlLeft"] }`
307
+ (`crouchAction` renames it). A missing action is tolerated and reported
308
+ once, like `sprint`.
309
+
310
+ `examples/vents-3d` is the composition: ducts, a laser grid at head height,
311
+ a guard's sightline you crouch under.
312
+
313
+ ## A death that falls — `Ragdoll3D`
314
+
315
+ Every enemy in every example died the same way: the model played a clip, or
316
+ vanished. Nothing crumpled, slid down a slope or hung off the rail, because a
317
+ ragdoll is ten bodies and nine joints built from a skeleton nobody wanted to
318
+ write. One node under the body, beside the Skin:
319
+
320
+ ```json
321
+ { "name": "Brute", "type": "CharacterBody3D", "children": [
322
+ { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } },
323
+ { "name": "Hp", "type": "Node", "script": { "name": "Health", "props": { "max": 50 } } },
324
+ { "name": "Ragdoll", "type": "Ragdoll3D", "props": { "target": "../Skin", "lifetime": 8 } }
325
+ ] }
326
+ ```
327
+ ```jsonc
328
+ { "signal": "died", "from": "Brute/Hp", "to": "Brute/Ragdoll", "handler": "activate" }
329
+ ```
330
+
331
+ - **Idle it is nothing.** `activate()` reads the skeleton at `target`
332
+ (Mixamo names — `Hips`, `Spine`, `Head`, `LeftArm`… with the forgiving
333
+ lookup `findBone` uses), builds a capsule `RigidBody3D` per limb where the
334
+ bones are — torso, head, upper arms, forearms, thighs, shins — joins each
335
+ to its parent with a spherical `Joint3D`, gives them the velocity of the
336
+ body they fell out of, and from then on writes the bones' world matrices
337
+ from the limbs every frame. The limbs go under the scene root in the
338
+ `ragdoll` group; `limbs()` lists them.
339
+ - **Turn the rest off beside it**: the body's collider (`enabled: false`) so
340
+ the capsule stops holding the corpse up, the controller or the chase.
341
+ `Health.freeOnDeath` would delete the model the ragdoll is driving — leave
342
+ it off and let `lifetime` free the limbs, then free the corpse yourself on
343
+ the `reset` signal.
344
+ - `mass` (70 kg, shared out by limb), `damping` (3 — how fast the flailing
345
+ dies), `collide` (limbs against each other; adjacent ones never). A rig
346
+ without hands or a `HeadTop_End` still builds — the forearm ends at the
347
+ wrist bone, the head is a ball.
348
+ - **Headless there is no skeleton**: `activate(pose)` takes bone positions
349
+ (`{ Hips: [x, y, z], … }`), so a harness can build one where the model would
350
+ be and ask that it fell, that the elbow still joins, that the limbs carried
351
+ the run. `active` reads true while it is up.
352
+
353
+ ## Letting go — `enabled`
354
+
355
+ The player gets into a car, sits through a cutscene, opens a menu: the
356
+ controller has to LET GO, and `enabled: false` is that. For getting into a car or
357
+ onto a horse, the whole handoff — this controller off, the body parked and
358
+ riding along, the Skin on the saddle, the steed's controller or `Vehicle3D`
359
+ on, and back — is the `Mount` gameplay behaviour (`incanto-gameplay-behaviors.md`). Off, it reads no
360
+ input, applies no forces (no hover spring either), drives no camera and
361
+ sets no animation — the body under it is an ordinary `RigidBody3D` until it
362
+ is on again. A game parks the character out of sight beside it:
363
+
364
+ ```ts
365
+ ctl.enabled = false; // the controller lets go
366
+ player.enabled = false; // the body's collider is off — nothing bumps the empty seat
367
+ player.visible = false;
368
+ player.gravityScale = 0; // a collider-less dynamic body would fall through the floor
369
+ // …and each frame while driving: player.position = car.position
370
+ ```
371
+
372
+ and on the way out puts it back where the door is, `enabled`/`visible` on,
373
+ `gravityScale` 1, THEN `ctl.enabled = true` — the controller re-finds the
374
+ current camera and drives it from the next frame (the camera pops to the
375
+ rig; smooth it yourself if the cut shows). `Vehicle3D.enabled` is the same
376
+ switch on the car's side, and `examples/errands-3d` flips both with one
377
+ `Interactable` press.
378
+
379
+ ## Two players at one keyboard
380
+
381
+ A second player is a second ACTION SET, and a controller told to read it:
382
+
383
+ ```jsonc
384
+ // the scene's input map declares both
385
+ "input": {
386
+ "move": { "type": "vector2", "keys": { "up": ["KeyW"], "down": ["KeyS"], "left": ["KeyA"], "right": ["KeyD"] } },
387
+ "jump": { "type": "button", "keys": ["Space"] },
388
+ "move2": { "type": "vector2", "keys": { "up": ["ArrowUp"], "down": ["ArrowDown"], "left": ["ArrowLeft"], "right": ["ArrowRight"] } },
389
+ "jump2": { "type": "button", "keys": ["ShiftRight"] }
390
+ }
391
+ ```
392
+ ```json
393
+ { "name": "Controller", "type": "CharacterController3D",
394
+ "props": { "view": "quarter", "camera": "none",
395
+ "moveAction": "move2", "jumpAction": "jump2", "sprintAction": "sprint2" } }
396
+ ```
397
+
398
+ - `moveAction` / `jumpAction` / `sprintAction` have been there since the
399
+ controller shipped and no example had ever set them. They work: measured on
400
+ `examples/coop-3d`, player one's keys move smith one 2.4 m and smith two
401
+ **0.00 m**.
402
+ - **Every player's controller needs `camera: "none"`**, and one camera gets a
403
+ `GroupCamera` (see `incanto-gameplay-behaviors.md`) — otherwise two
404
+ controllers and the group camera all write the same camera every frame.
405
+ - Anything else a player operates takes the same treatment: `Carry.action`,
406
+ `Interactable.action`, `Shoot`'s action — name the second player's own.
407
+ - The playtest bot drives EVERY controller it finds, each on its own action set,
408
+ so a two-player game is exercised rather than half-exercised.
409
+
129
410
  ## Views
130
411
 
131
412
  | view | what it does | key props |
@@ -135,7 +416,21 @@ docstring's own advice: a raised `floatHeight` makes the character weightless).
135
416
  | `quarter` | fixed isometric pitch 35.264°, NO mouse look | `camDistance` 40, `mouseLook: false` |
136
417
  | `side` | camera at +z `camDistance`, lock movement to ±x | `mouseLook: false` |
137
418
 
138
- The controller drives the scene's `current` Camera3D every frame (smoothed).
419
+ The controller drives the scene's `current` Camera3D every frame (smoothed)
420
+ unless you take it off that duty:
421
+
422
+ ```json
423
+ { "name": "Controller", "type": "CharacterController3D",
424
+ "props": { "view": "quarter", "camera": "none" } }
425
+ ```
426
+
427
+ **`camera: "none"` leaves the camera exactly where the scene puts it.** A whole
428
+ shelf of games has a FIXED view — a board game, a bomb arena, an isometric
429
+ puzzle, a fixed-angle horror — and could not use this controller at all, because
430
+ it re-posed their camera every frame. Pair it with an authored `Camera3D` (and
431
+ a `FollowCamera` behaviour if the board scrolls); `incanto-check` no longer
432
+ calls that pair a fight. The default is `"drive"`, which is what every existing
433
+ scene already gets.
139
434
  Wheel zooms only when `zoomMax > zoomMin`.
140
435
 
141
436
  In `free` view the camera AIMS at the character every frame (`lookAt`), so the
@@ -160,7 +455,7 @@ enemy never yanks the camera in. `firstPerson` skips it (the camera is at the ey
160
455
 
161
456
  ## Animations
162
457
 
163
- `controller.state` is `idle | walk | run | fastRun | airborne`; the
458
+ `controller.state` is `idle | walk | run | fastRun | airborne | swim | climb | crouch | sneak | dash`; the
164
459
  `movementStateChanged` signal fires on transitions — map states to clips
165
460
  (`$animation` assets are retargeted onto the skin's own rig — GLB and VRM alike — so a
166
461
  clip never changes your character's proportions):
@@ -168,13 +463,23 @@ clip never changes your character's proportions):
168
463
  ```ts
169
464
  controller.on('movementStateChanged', (state) => {
170
465
  skin.animation = { idle: '$idle', run: '$run', fastRun: '$runFast',
171
- airborne: '$jump', walk: '$walk' }[state];
466
+ airborne: '$jump', walk: '$walk', swim: '$swim',
467
+ climb: '$climb' }[state];
172
468
  });
173
469
  ```
174
470
 
175
471
  Setting `animation` to a new clip **crossfades** (0.2s blend) from the current
176
472
  one — idle↔walk↔run↔jump transitions are smooth, not a hard cut.
177
473
 
474
+ **A state your map does not name falls back — it never leaves the last clip
475
+ playing.** `swim → walk → idle`, `dash → fastRun → run → walk → idle`,
476
+ `sneak`/`climb → walk → idle`, `crouch`/`airborne → idle`. Every chain ends at
477
+ `idle`, so a map with one clip in it still animates. Without this, a scene that
478
+ mapped only the original five (which is every scene there was) put a swimmer
479
+ across the bay in a frozen JUMP pose, and did the same for `dash`, `climb`,
480
+ `sneak` and `crouch`. Map the state itself when you have the clip: the shipped
481
+ water templates now declare `swim` (`mixamorig/swim.glb` on the agent8 CDN).
482
+
178
483
  ## Reskin one model with `tint` (many variants from one GLB)
179
484
 
180
485
  `ModelInstance3D` has a `tint` prop (hex, `""` = off) that MULTIPLIES into every
@@ -225,7 +530,7 @@ Map movement states straight in JSON — no behavior needed:
225
530
  { "name": "Controller", "type": "CharacterController3D",
226
531
  "props": { "view": "free",
227
532
  "animations": { "idle": "$idle", "walk": "$walk", "run": "$run",
228
- "fastRun": "$sprint", "airborne": "$jump" } } }
533
+ "fastRun": "$sprint", "airborne": "$jump", "swim": "$swim" } } }
229
534
  ```
230
535
 
231
536
  The controller writes `skin.animation` on every state change (crossfaded by
@@ -233,7 +538,7 @@ the model). The `movementStateChanged` signal still fires for extras.
233
538
 
234
539
  **Where those clips come from.** Locomotion clips live at
235
540
  `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
541
+ (`idle-00`, `walk`, `run-medium`, `run-fast`, `jump`, `swim`) and play on any mixamorig
237
542
  model as they are — retargeted through the humanoid map for a VRM. You do not
238
543
  have to assemble any of this by hand: run the character URL your asset MCP gave
239
544
  you through `bunx incanto-model <url>` and it prints the whole thing — assets,
@@ -292,6 +597,11 @@ NPCs feel alive when they track you. One node, zero code:
292
597
  - `weight` (0.85) is how far the head commits; `forwardAxis` defaults to the
293
598
  mixamo head convention (+z).
294
599
  - Also good for chests (`bone: "Spine2"`, small weight) and turrets.
600
+ - **`node.engaged`** (0..1) is how far it is committed RIGHT NOW — the whole
601
+ observable effect of this node, and until now nothing could read it. It turns
602
+ a rendered bone, so it stays 0 in a headless run (no GLB, no skeleton): check
603
+ it in the browser, where `attachedBone` tells you the bone was found and
604
+ `engaged` tells you the head is following.
295
605
 
296
606
  ## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
297
607
 
@@ -383,6 +693,12 @@ This cannot be an `auditScene` warning: a scene file has no velocity, and the bu
383
693
  is a behavior writing a heading at runtime. `facingReport()` from `incanto/test`
384
694
  is the same check for your own tests.
385
695
 
696
+ Like the feel numbers above it, this presses your title screen's START before
697
+ driving the character — a paused world moves nowhere, and `did not move —
698
+ nothing to compare` was the answer it gave about three shipped games whose only
699
+ fault was that their menu was up. When nothing on screen starts the game it says
700
+ THAT instead.
701
+
386
702
  ## Facing the mouse in a top-down view (twin-stick)
387
703
 
388
704
  In `quarter` view the mouse does not look, and the controller turns the skin
@@ -404,29 +720,76 @@ Measured on `examples/survivor-3d`: a cursor 10 m east is yaw 90°, and a bolt
404
720
  given `direction: [dx, 0, dz]` flies where the skin looks. Boot with
405
721
  `pointer: { lockOnClick: false }` — a locked cursor is no aim at all.
406
722
 
407
- ## Lock-on and a dodge, in a few lines each
723
+ ## Lock-on (`lockOnAction`) and a dodge
408
724
 
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):
725
+ Lock-on used to be a one-line recipe the camera yaw toward the enemy, every
726
+ frame and the half it never had was the one that matters: the SKIN kept
727
+ facing the move direction, so a locked player circling a brute showed it a
728
+ shoulder, and a sword hitbox hung off the skin's yaw swung sideways. The
729
+ controller owns it now:
413
730
 
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
731
+ | prop | default | what it does |
732
+ |---|---|---|
733
+ | `lockOnAction` | `""` | the button that toggles the lock (`""` = no lock-on) |
734
+ | `lockGroup` | `"enemy"` | who can be locked: the nearest live, visible member within… |
735
+ | `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 |
736
+
737
+ ```json
738
+ { "name": "Controller", "type": "CharacterController3D",
739
+ "props": { "view": "free", "lockOnAction": "lockOn", "lockRange": 14 } }
418
740
  ```
419
741
 
420
- A dodge is a dash plus i-frames that start BEFORE the hit — `Health.protect`,
421
- not `invulnerableFor` (which opens only after one):
742
+ **Locked means the character FACES the target while its feet go anywhere.**
743
+ The camera yaw follows player target every frame, so the stick — which is
744
+ camera-relative — strafes round it; the skin turns to the target (at
745
+ `turnSpeed`) even standing still; `state` stays `walk`/`run` while circling.
746
+ Anything you hang off the skin's yaw — `examples/melee-3d`'s `Edge` hitbox at
747
+ `sin/cos(skin yaw)` — swings at the target. Methods `lock(node)`, `unlock()`,
748
+ `toggleLock()`; readback `lockTarget`; signal `lockChanged(target | null)`.
749
+ `examples/boss-3d` locks onto the colossus with `lockGroup: "boss"`,
750
+ `lockRange: 40`.
751
+
752
+ `incanto-feel` knows: a controller with a `lockTarget` is reported as
753
+ `LOCKED ON` rather than measured against its travel — a strafe is the
754
+ intended reading, not a defect.
755
+
756
+ A dodge is a dash plus i-frames that start BEFORE the hit. The dash is the
757
+ controller's (`dashSpeed` — see below); the i-frames are `Health.protect`,
758
+ not `invulnerableFor` (which opens only after one), hung on the `dashed`
759
+ signal:
422
760
 
761
+ ```json
762
+ { "name": "Ctl", "type": "CharacterController3D",
763
+ "props": { "dashSpeed": 9, "dashSeconds": 0.35, "dashCooldown": 0.5 } }
764
+ ```
423
765
  ```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];
766
+ controller.on('dashed', () => (player.behavior as Health).protect(0.35));
426
767
  ```
427
768
 
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.
769
+ ## A dash / dodge roll (`dashSpeed`)
770
+
771
+ Two examples carried the same twelve lines — a timer, a direction, and
772
+ `body.linearVelocity = dir × 9` written EVERY frame because the controller
773
+ brakes toward its own pace and a single impulse dies — while
774
+ `CharacterController2D` had `dashSpeed` since 0.33. Now the 3D controller
775
+ has it too:
776
+
777
+ | prop | default | what it does |
778
+ |---|---|---|
779
+ | `dashSpeed` | `0` | m/s for the roll; `0` = no dash |
780
+ | `dashSeconds` | `0.25` | how long the velocity is held |
781
+ | `dashCooldown` | `0` | seconds after a roll ends before the next can start |
782
+ | `dashAction` | `dash` | pressed = roll (tolerated when missing, like `sprint`) |
783
+ | `dashBackstep` | `false` | with no stick, roll AWAY from where the skin faces — the Souls-like backstep a lock-on game wants |
784
+
785
+ - The roll goes where the stick points (camera-relative), or with no stick
786
+ where the skin faces — or away from it with `dashBackstep`, which is what
787
+ a neutral roll means while locked on to something.
788
+ - `state` reads `dash` for the roll — map a clip to it — and `dashed` fires
789
+ once per roll. Not while crouched, on a ladder or swimming.
790
+ - The velocity is written last, every step of the roll, over whatever the
791
+ step's impulses did; gravity still runs, so a roll off a ledge falls.
792
+ - `examples/melee-3d` rolls with it and keeps its i-frames on `dashed`.
430
793
 
431
794
  ## First-person weapon viewmodel + the tracer-from-the-muzzle rule
432
795
 
@@ -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