---
name: incanto-physics-and-input
description: Incanto physics bodies (2D/3D Rapier), colliders-as-props, trigger signals, and the declarative InputMap. Use when adding movement, collisions, pickups, gravity, or keyboard controls.
---

# Physics & Input in Incanto

> Shipped inside the `incanto` npm package — this document always matches the
> installed engine version. Sibling skills live in `node_modules/incanto/skills/`.

## Enabling physics (async — Rapier wasm loads on demand)

```ts
import { enablePhysics2D } from 'incanto/2d'; // or enablePhysics3D from 'incanto/3d'
await enablePhysics2D(engine); // AFTER engine.setScene(scene)
engine.start();
```
Games that never call this never download Rapier (~600KB gz). 2D and 3D worlds are
independent; enable the one matching your scene.

## Units & gravity

| | 2D | 3D |
|---|---|---|
| Units | pixels, y-DOWN | meters, y-UP |
| Default gravity | `[0, 980]` | `[0, -9.81, 0]` |
| Scene header | `"physics": { "gravity": [0, 1400] }` | `"physics": { "gravity": [0, -9.81, 0] }` |

## Body nodes (colliders are PROPS, never child nodes)

2D collider shapes: `{"shape":"rect","size":[w,h]}` · `{"shape":"circle","radius":r}` ·
`{"shape":"capsule","radius":r,"height":h}`.
3D: `{"shape":"auto"}` · `{"shape":"box","size":[x,y,z]}` · `{"shape":"sphere","radius":r}` ·
`{"shape":"capsule",...}` · `{"shape":"trimesh","vertices":[...],"indices":[...]}` ·
`{"shape":"heightfield"}` (STATIC-only; reads the grid from a `Terrain3D` child — see the
incanto-environment skill).
Malformed colliders are hard `BAD_FORMAT` errors at enable time.
The collider FOLLOWS the terrain: reshape the `Terrain3D` at runtime — `maxHeight`,
`seed`, `roughness`, a channel a `River3D` cuts — and the heightfield is rebuilt on the
next physics step, so the ground is walked on where it is drawn (the editor's collider
outline moves with it for the same reason).

**`{"shape":"auto"}` is the one to reach for on props.** It shapes the collider like the
`MeshInstance3D` the body CARRIES — box→cuboid, sphere→ball, cylinder/capsule→their own
shapes, `gem`→a convex hull of its real vertices — so a blocker can never disagree with what
the player sees. Hand-typed extents drift: a box "around" a bridge ends up wider than the
bridge and you stop in mid-air; a sphere "for" a stone ends up inside it and you clip through
the corners. Give the body its mesh as a CHILD and let the shape follow:

```json
{ "name": "DeckBody", "type": "StaticBody3D",
  "props": { "position": [0, 3.2, -8], "rotation": [0, 34, 0], "collider": { "shape": "auto" } },
  "children": [ { "name": "Deck", "type": "MeshInstance3D",
    "props": { "mesh": "box", "size": [14, 0.22, 2.2] } } ] }
```
It FOLLOWS the mesh, too: resize it, turn the box into a sphere, or move it inside the
body at runtime and the collider is rebuilt on the next physics step — the same rule the
heightfield keeps with its terrain. A body that takes its shape from another node moves
with that node, not only with its own `collider` prop.

The child's OWN `position`, `rotation` and `scale` are part of the shape: raise the plank on
the child instead of the body and the collider goes up with it; scale the crate 4× and it is
solid at 4×. (It was not before 0.62 — the child transform was dropped and the collider sat
at the body's origin at the authored size, so you walked through the deck you could see and
landed on an invisible slab at ground level. Measured: a plank drawn with its top at y=3.2
had its surface at y=0.2.) `collider.offset` stacks on top of that.

A ball, a capsule and a cylinder have ONE radius between them, so a child scaled unevenly in
x and z has no exact shape — the widest of the two is used and the engine says so by name.

Bodies ignore an ANCESTOR's rotation, so a yawed structure puts each body at top level with
its own `rotation` — the child mesh then turns with it, visual and collider together.

**`auto` works on a `ModelInstance3D` (GLB/glTF/VRM) too** — the shape comes from the model's
real triangles, baked through their own transforms, so an offset or `targetHeight`-fitted model
collides where it is DRAWN:

- on a **static** body (`StaticBody3D`, `Area3D`) → a triangle mesh: the model keeps its holes,
  so you walk THROUGH an archway instead of into it. Level art can be dense; a collider over
  ~20k triangles logs a note suggesting a low-poly collision model.
- on a **moving** body (`RigidBody3D`, `CharacterBody3D`) → the convex hull. Rapier's triangle
  meshes are hollow shells; a dynamic one falls through the world.

The body simply does not exist until the model has downloaded — nothing is invented in the
meantime — and it appears the step the asset lands. A body is ONE shape, so `auto` fits one
mesh or one model and warns when the body carries more.

- **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`, and the SURFACE it
  is: `friction 0.5` (0 = ice, 1+ = glue) and `restitution 0` (1 = trampoline; above 1
  is allowed and is a catapult). Two things a pad under the PLAYER taught: the controller's
  falls run at `fallGravity` (2.5×) and its rises at 1×, so a `restitution 1` pad injects
  energy on every bounce of a character that falls onto it (measured: 6.9 m, then 11.6 m) —
  use ~0.8 for a pad players land on again and again; and `friction 0` is not ice for a
  CharacterController3D, because the controller brakes itself (its own damping, not the
  floor's friction, stops it) — an ice patch slows a crate, not the character. A static
  body's authored value WINS the pair — restitution above 0 combines by Max, friction below
  the default by Min and above it by Max — so a trampoline launches a dead ball and an ice
  patch slides a grippy crate. The same on a `RigidBody`: its own `friction`/`restitution`
  win the pair too; when both sides speak, Rapier's order (Max > Multiply > Min > Average)
  decides. Left at the defaults it pairs by average, as it always did.
  On a `Terrain3D`, `snapToGround: <lift>` on the body sits the pad on the island at load
  (beacon-isle's trampoline: `snapToGround: 0.1` for a 0.2 m slab).
- **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
  `fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
  read back every step; `velocity` is an alias for it). 3D also has
  `angularVelocity` (RADIANS per second about each world axis — the one place the
  engine is not in degrees). Every one of them is LIVE — write it mid-run and the solver
  picks it up on the next step. Three things a first composition of these measured:
  an authored `restitution` or `friction` WINS the pair whichever side wrote it — a `0.75`
  ball on a plain floor keeps 75% of its speed (it used to average with the floor's 0 and
  keep 37%), a `friction: 0` body slides on a plain floor as if the floor were ice (the
  character skill's recipe, honoured at last: walk 4.0, not 3.7);
  `gravityScale: 0` with an `angularVelocity` and a `linearVelocity` is a floating,
  spinning, drifting prop and a NEGATIVE `gravityScale` rises; and the body's
  `rotation` is written back as Euler XYZ, which reports a yaw past 90° as
  `[180, 180 − yaw, 180]` — recover a heading from all three, never from `rotation[1]`.
- **`Area2D/3D`** — sensor. Emits `triggerEnter(other)` / `triggerExit(other)`. Never blocks
  movement. Solid bodies emit the same signals on real contact (one mental model).
  Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
  enemy-hitbox Area) — neither side needs to be a Body.
- **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
  (capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`,
  `stepHeight` (3D `0.35` m / 2D `35` px — see below), **`pushes 0`** — the
  mass (kg) this body SHOVES dynamic bodies with. The KCC treats a dynamic
  body as an OBSTACLE — a character walks around things — so a beam on a
  `Patrol` slid up to the player and stopped dead against them, forever. A
  piston, a sweeping arm, a moving wall or a bruiser enemy is a thing that
  shoves: `pushes: 90` and it pushes what it meets, the harder the heavier
  (`examples/tower-3d`'s beam). `0` keeps every scene as it was. And
  **`collideWithCharacters true`** — whether OTHER characters are obstacles to
  this one. Leave it on for a few enemies; turn it OFF for a horde: eighty
  chasers around a player cost 64–286 ms a STEP with it on (each slide
  shape-casts against every neighbour — O(n²) in the crowd, and 93% of the
  profile was Rapier) and 1.6 ms off. The horde overlaps itself and nobody
  minds; it still collides with the floor and the walls.
  API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`,
  **`isOnWall()`** and **`isOnCeiling()`**. Which wall is answered in the shape
  each dimension has: 2D's `wallSide()` is `-1` left / `+1` right, 3D's
  `wallNormal()` is the wall's normal pointing away from it — the direction a
  wall jump pushes. Without them a wall jump or a wall slide is not awkward to
  write, it is impossible: there is nothing to ask.

### `enabled` — a collider that is off for now

Every physics body takes **`enabled`** (default `true`). Off means no contacts
and no `triggerEnter`/`triggerExit`; the body stays, so nothing is rebuilt and
re-arming is one write.

```jsonc
// a melee hitbox: real collider, authored OFF
{ "name": "Sword", "type": "Area2D",
  "props": { "collider": { "shape": "circle", "radius": 30 }, "enabled": false } }
```
```ts
sword.enabled = true;    // the active window of the swing
sword.enabled = false;   // …and done
```

A switched-off SENSOR overlaps nothing — including a body teleported into it
while it is off. Rapier still reports an intersection-start for a disabled
collider in that case (measured: `isEnabled()` false, event delivered), and a
guard's grab disarmed for a cutscene killed the player the camera had left
standing in it; both adapters now drop any event on a node whose `enabled` is
false, and re-arming reports what the sensor is inside of.

**A child body sits where its parent's frame puts it — rotation included.** A
hit sensor two metres down +z of a hull turned 90° is at world +x, where the
renderer draws it. Both adapters used to sum positions up the tree and ignore
every rotation, so a guard's grab box "in front" of a turned guard, a tank's
barrel-tip sensor and a turret's muzzle all sat where nothing was drawn; a
dynamic body under a turned parent keeps a LOCAL pose on the way back. The same
composition is behind `worldPosition()` for behaviours, so `Turret`, `Sight`
and `FollowCamera` measure from where a node IS. Scale is still not composed.

**A collider's keys are CLOSED**: `shape`, `size`, `radius`, `height`, `offset`
and (2D) `oneWay` — plus `vertices`/`indices` for a 3D trimesh. Anything else is
a load error naming the nearest one, because
`{"shape": "circle", "radius": 8, "offest": [0, -24]}` used to load clean with
its hitbox 24 px from where it was meant to be. `friction`, `restitution` and
mass are props of the BODY, not of its collider.

**Do not arm a hitbox by swapping `collider` in and out.** Replacing the
collider prop tears the rigid body down and builds a new one — twice per swing —
and a scene authored `"collider": {}` warns `has no collider — physics skips it`
on every boot, which is true and unactionable and teaches its reader to ignore
warnings.

Good for anything that is sometimes solid: a door that opens, a platform that
phases, a shield that is only up while blocking, a trigger that fires once and
retires.

Physics simulates WORLD positions: bodies under offset parents work (offsets compose),
but ancestor ROTATION/SCALE are not supported for physics bodies — keep body ancestors
untransformed or translation-only.

**Gravity is NOT auto-applied to CharacterBody** (Godot semantics) — integrate it yourself:

```ts
class Player extends CharacterBody2D {
  static override readonly typeName = 'Player';
  override fixedUpdate(dt: number): void {
    const dir = input.getVector('move');
    this.velocity[0] = dir.x * 260;
    if (this.isOnFloor()) {
      this.velocity[1] = input.justPressed('jump') ? -640 : 20; // small bias keeps ground snap
    } else {
      this.velocity[1] += 1400 * dt;
    }
    this.moveAndSlide();
  }
}
registerNode(Player); // then use "type": "Player" in scene JSON
```

## Declarative input (scene JSON `input{}` → `engine.input`)

```json
"input": {
  "move": { "type": "vector2", "keys": { "up": ["KeyW","ArrowUp"], "down": ["KeyS","ArrowDown"], "left": ["KeyA","ArrowLeft"], "right": ["KeyD","ArrowRight"] }, "touch": "joystick" },
  "jump": { "type": "button", "keys": ["Space"], "touch": "button" }
}
```

```ts
engine.input.attachKeyboard(window); // explicit DOM wiring (browser only)
engine.input.isPressed('jump');      // held
engine.input.justPressed('jump');    // one-frame edge (settled per tick)
engine.input.getVector('move');      // normalized {x, y}, y-down (up = -y)
```
Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`) — plus `Mouse0..4`
and `Pad0..16`, which live in the same space. **A value outside that set is a load
error** naming the one you probably meant (`'W' → "KeyW"`, `'space' → "Space"`,
`'Shift' → "ShiftLeft" or "ShiftRight"`): the map is looked up by exact string, so a
misspelled code binds to nothing and the player silently does not have that control.
An EMPTY key list is still fine — `{"keys": [], "touch": "button"}` is a touch-only
control. Unknown actions and
wrong-kind queries (getVector on a button) are hard errors listing valid names.
Action declarations RESET on every `setScene` (no keybind bleed between scenes).

`attachKeyboard` calls `preventDefault()` ONLY for keys bound to a declared action
(default `{ preventDefault: 'bound' }` — embedded games no longer scroll the host page on
Space/arrows; pass `'none'` to opt out). Keys typed into INPUT/TEXTAREA/SELECT/
contenteditable are ignored entirely, so DOM UI overlays keep working.
`engine.input.dispose()` detaches every attached source (keyboard and pointer).

**One press is ONE edge**, in `update` and in `fixedUpdate` alike — the fixed pass has
its own edge view, drained after its first step, so a dropped frame carrying five steps
delivers the edge once and a 144 Hz tick that runs no fixed step at all does not lose it.
Same for `justReleased`. (Until 0.63 this warned that the edge repeated in every fixed
step of a slow frame; it has not since the fixed pass got its own edge sets, and nothing
noticed for two releases.)

⚠️ What IS worth knowing: **a press injected from inside the frame lands on the NEXT
tick.** `pressAction`/`releaseAction` called from a `fixedUpdate`, an `update` or an
`engine.updated` handler — replay drivers, AI input, touch overlays — is invisible for
the rest of that tick and arrives at the next one, because an edge created after the
pass that reads it would be created and destroyed without anything seeing it. The HELD
state (`isPressed`) is immediate either way.

## Touch controls (mobile web)

Declare `"touch": "joystick"` on a vector2 action and/or `"touch": "button"` on button
actions (as above) and the game grows on-screen controls: a left-side virtual stick
feeding `setActionVector`, right-side buttons feeding `pressAction`/`releaseAction` —
the game logic never distinguishes touch from keyboard. `createGame2D/3D` shows them
automatically on coarse-pointer devices (`touch: 'auto'` default; `true` forces, `false`
disables; they overlay `touchContainer` — default the canvas's parent, give it
`position: relative`). Manual boots call `attachTouchControls(engine, container,
{ force? })` from `incanto`. Wrong touch kinds (`"joystick"` on a button) fail at load.

**Three things a phone needs that a desktop never shows you**, two of which the
engine now does for you:

- **The canvas owns its touch gestures.** `createGame2D/3D` set
  `touch-action: none` (plus no selection/callout) on the canvas, or the browser
  keeps pan, pinch-zoom and pull-to-refresh over the play surface — a drag meant
  for aim or a virtual stick SCROLLS THE PAGE instead. `pageGestures: true`
  gives them back to the browser (a small canvas inside a scrolling article).
- **The controls clear the phone's furniture.** They sit
  `calc(24px + env(safe-area-inset-*))` from the edges, off the iPhone home
  indicator — whose band is also the OS's "leave the app" swipe.
- **Your PAGE has to opt in**, and this part is yours: `env()` is 0 unless the
  document says so, and `100vh` is the toolbar-hidden height, so the bottom strip
  (where the controls are) hides under the browser chrome.

  ```html
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <style>
    html, body { margin: 0; height: 100%; overflow: hidden; }
    canvas { display: block; width: 100%; height: 100%; }   /* NOT 100vw/100vh */
  </style>
  ```

  Every `incanto-new` template and every runnable example ships exactly this.

**A 3D game needs two more things a 2D one does not**, and both are one line:

- **The camera has to turn.** The stick walks; looking around is a DRAG on the
  play area, and that only exists when the game asks for it:
  `createGame3D({ pointer: true })`. Without it a phone player can walk and
  never look. (A drag on the stick or a button never turns the camera — the
  controls are siblings of the canvas and eat their own gestures.)
- **Portrait is a keyhole.** `Camera3D.fov` is the VERTICAL field of view, so a
  `fov: 60` camera shows 91° across a 16:9 window and **30° across a 390×844
  phone** — the same game, down a drinking straw. Set
  `Camera3D.minHorizontalFov` (52 is a good floor) and the vertical fov widens
  until that much is on screen; a window already wider keeps the fov it was
  authored with. Pair it with `CharacterController3D.pitchStart` (about −14°) —
  a tall screen spends its top half on sky with a level camera.

Two more phone facts worth knowing:

- **Audio unlocks on ANY first gesture** — the canvas, a key, or the on-screen
  stick and buttons. (Those controls are siblings of the canvas, so their taps
  never reached it; a game with on-screen controls used to stay silent until the
  player happened to touch the play area.)
- **2D renders at `pixelRatio: 1` by default** (Phaser parity — the browser
  upscales, edges read soft). Phones are 2–3× denser, so a text-heavy or
  vector-heavy 2D game wants
  `createGame2D({ pixelRatio: Math.min(devicePixelRatio, 2) })`, or
  `"environment": { "rendering": { "pixelRatio": "device" } }` in the scene. 3D
  already defaults to `min(dpr, 2)`; pixel art is usually happier left at 1.

Collision layers/masks are not in v0 — everything collides with everything; use groups +
`triggerEnter` filtering for game logic.

## Action-level injection (scripted tests, touch overlays)

Drive the game BY INTENT — no key codes to reverse-engineer:

```ts
engine.input.pressAction('jump');           // one justPressed frame, held until…
engine.input.releaseAction('jump');         // …released (one justReleased frame)
engine.input.setActionVector('move', 1, 0); // analog vector2; (0, 0) clears
```

**An action name you typed that does not exist says so.** A controller tolerates
a missing action — a game with no sprint key must not crash — but a value you
NAMED and never declared is a typo, and it used to be indistinguishable from the
tolerated case:

```
[incanto] /Game/Player/Ctl: Unknown input action 'movement'.
Declared actions: [move, jump]. The character will not move.
```

Once per node and action, and only when the prop differs from its default —
leaving `sprintAction` alone in a game with no sprint key is the ordinary case
and stays silent.
Injected state combines with key state (vectors clamped to unit length).

## Patterns

- Pickups: `Area2D` in a group + `triggerEnter` → check `other.isInGroup('player')` →
  `queueFree()` — wire it via JSON `connections` to a behavior method (the `Pickup`
  pattern in `incanto-behaviors-and-scripts.md`), or imperatively with `node.on(...)`.
- Teleport: just write `node.position` — the body follows. **`node.rotation` follows
  too**: turn a static or kinematic body at runtime and its COLLIDER turns with it, so
  a spinning blade cuts and a swinging gate blocks. (It did not before 0.62 — the
  angle was applied once at creation, the mesh turned and the collider stayed put.)
  A free dynamic body's rotation belongs to the solver and is left alone.
  Launch: write `linearVelocity`.
- Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/planetarium/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — gravity, jump, attack lockout, custom
  `Player` node type. Verified in Chromium end-to-end.

## Platformer staples (2D)

- **One-way platforms**: `"collider": { "shape": "rect", "size": [300, 20],
  "oneWay": true }` on a StaticBody2D — characters jump up THROUGH it and
  land ON it (solid only when falling from above). The Mario ledge.
- **Moving platforms carry riders automatically**: a CharacterBody2D
  standing on ANY body follows that body's movement (elevators, patrol
  platforms — just animate the platform's `position`; the character rides).

## Joints (Joint2D / Joint3D)

Link two bodies: put the joint node as a CHILD of body A, point `target` at
body B (any node path — `%Name` is typical):

```json
{ "name": "Ball", "type": "RigidBody2D",
  "props": { "collider": { "shape": "circle", "radius": 10 } },
  "children": [
    { "name": "Rope", "type": "Joint2D",
      "props": { "type": "rope", "target": "%Anchor", "length": 100 } }
  ]}
```

Types — 2D: `fixed` (weld) · `revolute` (pin/hinge) · `prismatic` (a SLIDE
along `axis`) · `rope` (max distance) · `spring` (pull toward `length` with
`stiffness`/`damping`). 3D has all of those plus `spherical` (ball joint);
its `revolute` turns about `axis` and its `prismatic` slides along it (local
to the joint's body, default `[0, 1, 0]`; 2D's `axis` defaults to `[0, 1]`,
so an "up" slide in y-down 2D is `[0, -1]`). `length: 0` measures the body
distance at creation — and a rope's or spring's `length`, `stiffness` and
`damping` are LIVE: a write REMAKES the joint on the next step, the bodies
keeping their motion, which is how a grapple reels its rope in
(`rope.length -= 5 * dt` — `examples/grapple-3d`). `anchor`/`targetAnchor`
are local offsets (px / m).
Chains work (pendulums, bridges: each link a body + joint to the previous —
`examples/wreck-3d` hangs a wrecking ball off six of them).

**A hinge has stops and a motor; so does a slide.** On a `revolute` or a
`prismatic` in either dimension:

| prop | default | what it is |
|---|---|---|
| `limits` | `[]` | the stops — `[minDeg, maxDeg]` for a hinge, `[min, max]` metres (px) for a slide; empty = free. A door that opens one way: `[0, 110]`; a portcullis that lifts three metres: `[0, 3]` |
| `motorSpeed` | `0` | the speed the motor drives it at — degrees a second (hinge) or metres a second (slide; px in 2D); `0` = no motor, the joint is free. A mill wheel: `40`; a drawbridge coming down: `-25` toward its lower limit, where the motor then HOLDS it; an elevator going up: `1.2` |
| `motorStrength` | `20` | the motor's gain — acceleration per unit of speed error. **Against gravity the motor sags by g/motorStrength**: a lift at `20` rises at half the asked speed, at `100` within a tenth of it |
| `angle` | read-only | a hinge's current angle in degrees: the joint's OWN body turned about the axis, relative to the target. `if (hinge.angle > 60)` is "the door is open" |
| `travel` | read-only | a slide's current travel in metres (px): the joint's own anchor along the axis relative to the target's — `0` where the anchors coincide, which is where the scene loaded |

All three props are LIVE — a lever behaviour that writes `hinge.motorSpeed = -25`
starts the winch that frame — and `limits`/`angle` share one convention (the
joint's body relative to the target), so a limit you read off `angle` is the
limit you write. `examples/contraption-3d` is a gate you push, a drawbridge a
lever lowers, a millstone that never stops and a seesaw with stops, all from
JSON — and the portcullis at its keep is a `prismatic` a `bridgeDown` signal
lifts.

Four things a hinge taught while that example was built:

- **`angle` is the ACTUAL turn, not "0 as loaded".** A drawbridge authored
  raised at `rotation: [85, 0, 0]` reads `angle: 85` on the first frame, and
  its limits are absolute too: `[0, 85]` is "flat to raised". Nothing is
  measured from the pose the scene loaded in.
- **Author a turned body about its hinge.** The two anchors must COINCIDE at
  load (rule 5 below). A deck turned 85° about its near edge has its CENTRE
  where that turn puts it — compute it (`sill + [0, 3.1·sin 85°, −3.1·cos 85°]`
  for a 6.2 m deck), do not type it; a mismatch snaps the body into place and
  the snap looks like a limit pushing it.
- **A raised thing needs HOLDING.** A hinge with no motor is free: a deck
  authored raised falls flat on the first frame. `motorSpeed: 25` toward the
  upper stop holds it there; the lever writes `-25` and the same motor lowers
  it to the lower stop and holds THAT.
- **A jointed thing must not rest on static ground.** A millstone whose rim
  touches the walkway slabs is jammed by friction and turns at 2°/s; a seesaw
  whose ends rest on the landings does not tilt. Leave a gap.

A grapple is a joint made at RUNTIME: `examples/grapple-3d` casts a ray from
the eye, and on a static hit builds a `Joint3D` in code — `new Joint3D('Rope')`,
`type: 'rope'`, `target` the hit body's path, `targetAnchor` the hit point in
that body's frame, `length` the distance plus a little slack — and adds it
under the player; `free()` it to let go. The adapter picks a new joint up on
the next step and drops a freed one; gravity does the swing, and a rope has a
little give under a swinging weight (about 4% of its length).

## Vehicle3D — a car in one node

`examples/buggy-3d` proves a car can be BUILT from joints, and that building
one is a round's work with two traps in it. Every racing game, delivery game
and open world with a road wants one in a node: `Vehicle3D`, under a
`RigidBody3D` chassis, is Rapier's raycast vehicle — wheels on springs that
never touch anything (a ray each), so a car cannot catch a kerb on a wheel
body or disagree with its axle.

```json
{ "name": "Car", "type": "RigidBody3D",
  "props": { "mass": 800, "collider": { "shape": "box", "size": [1.8, 0.6, 4] } },
  "children": [
    { "name": "Drive", "type": "Vehicle3D", "props": { "wheels": [
        { "position": [-0.8, -0.1,  1.4], "steer": true },
        { "position": [ 0.8, -0.1,  1.4], "steer": true },
        { "position": [-0.8, -0.1, -1.4], "drive": true },
        { "position": [ 0.8, -0.1, -1.4], "drive": true } ] } },
    { "name": "Wheel0", "type": "Node3D", "props": { "position": [-0.8, -0.1, 1.4] },
      "children": [ { "name": "Hub", "type": "Node3D", "children": [
        { "name": "Tyre", "type": "MeshInstance3D",
          "props": { "mesh": "cylinder", "size": [0.35, 0.25, 0.35], "rotation": [0, 0, 90] } } ] } ] }
  ] }
```

| prop | default | what it does |
|---|---|---|
| `wheels` | `[]` | `[{ position, radius?, rest?, steer?, drive? }]` — where each spring hangs from in the CHASSIS's frame; radius 0.35, rest length 0.3; at least one |
| `suspensionStiffness` · `suspensionCompression` · `suspensionRelaxation` · `maxTravel` | `30` · `2.3` · `3.5` · `0.3` | the springs (20 soft, 30 a car, 60 a kart) |
| `friction` · `sideFriction` | `2.5` · `1` | tyre grip along and across the wheel (lower side friction = more drift) |
| `maxSteerDeg` · `engineForce` · `brakeForce` | `32` · `1500` · `40` | full lock, force per driven wheel at full throttle, brake per wheel |
| `moveAction` · `brakeAction` | `"move"` · `"brake"` | the stick (up throttle, down reverse, left/right steer) and the brake button; `moveAction: ""` = your code writes `throttle`/`steer`/`brake` (−1..1, −1..1, 0..1) |

- **Forward is +z** (the +Z-forward rule, like a skin). Put the camera behind
  at −z and turn it with the chassis (`examples/courier-3d`'s `ChaseCam`).
- **The wheel nodes are posed for you.** A child of the chassis named
  `Wheel<i>` is put at its mount, hanging by the spring's current length, and
  turned to steer; its child `Hub` spins as the wheel rolls — so a tyre mesh
  under `Hub`, rotated `[0, 0, 90]` to lie on the axle, just works.
- **`enabled: false` is parked**: the throttle and steering read nothing and
  the brake is held, while the suspension still settles and the wheels still
  pose. Both the car and the character read `move`, so a game with a driver
  who can get OUT flips this and `CharacterController3D.enabled` together
  (`examples/errands-3d`; "Letting go" in `incanto-3d-character.md`).
- **Readbacks.** `speed` (m/s, negative in reverse), `wheel(i)` →
  `{ contact, suspension, rotation, steering }`.
- The chassis is an ordinary `RigidBody3D`: `mass` is the car's weight, its
  `collider` is what hits walls, `angularDamping` calms a flip, and a
  `Respawn` under it catches a drive off the map. A car on its roof is a
  stuck game — give the player a reset (the example's `R` rights the car).
- **Zones are driven THROUGH.** The wheel rays ignore every `Area3D`, so a
  checkpoint, a delivery bay or a speed trap is a sensor box over the road
  and the car passes through it level — it does not climb onto the box.
- **Lift-off is `linearDamping`.** A raycast vehicle has no rolling
  resistance: released, it coasts at CONSTANT speed until it hits something
  (measured 14.2 m/s, unchanged over a second). `linearDamping: 0.3` on the
  chassis is the engine braking (a ~3 s decay) and the top speed —
  `engineForce × driven wheels / mass ÷ damping` — in one number; the
  errands van has none and stops on its brake, the race car has it.
- **Online, a car is its owner's body** — `network: { mode: "owner", sync:
  ["position", "rotation", "Wheel0.rotation", "Wheel1.rotation"] }` on the
  chassis, and the other player's car a kinematic `CharacterBody3D` copy you
  can bump. `examples/race-mp-3d` and "An online race" in
  `incanto-multiplayer.md`.

A vehicle is the same parts: `examples/buggy-3d` is a box chassis, four
cylinder wheels on `revolute` axles (the rear pair motored — forward is a
negative speed about +x), and two steering knuckles whose hinge motor is a
servo, `motorSpeed = (target − angle) · rate`, with `limits` at full lock. A
knuckle needs inertia (mass, a real box) and a strong motor to turn a tyre
that is scrubbing on the ground, and a brake is a motor asked for ~0 with a
high `motorStrength`.

And two things the ENGINE learned: a motorised body no longer falls asleep
(Rapier sleeps anything that moves slowly for a while, and a door at 30°/s is
slow — the motor ran two seconds and froze mid-swing), and a TURNING ground
now carries its rider round — the moving-platform carry was the ground's
origin, which a millstone never moves, so its rider stood still while the
floor turned under him. Both riders (a `CharacterBody3D`'s `moveAndSlide`
and the player's `platformCarry`), both dimensions.

## Pushing a body around (the API that was in no skill)

A dynamic `RigidBody2D`/`RigidBody3D` has a force API and none of these appeared
in any shipped skill, so an agent building a physics puzzle reported the engine
as having "no force/impulse API at all" and designed around its absence:

```ts
const body = this.getNode('/root/Crate') as RigidBody3D;
body.applyImpulse([0, 6, 0]);      // an instant kick, in mass*units/second
body.velocity = [2, 0, 0];         // or set the velocity outright
```

`applyImpulse` is what the character controller itself runs on. It is an
IMPULSE, not a force: it changes velocity once, so a continuous push is one call
per frame from `update(dt)` scaled by `dt`. In 2D the impulse is in px·kg/s,
y-down, exactly like `linearVelocity`.

Readable/writable body state, in BOTH adapters: `velocity` (= `linearVelocity`),
`angularVelocity`, `mass`, `gravityScale`, `friction`, `restitution`,
`linearDamping`, `angularDamping`. Read the
spin to see how fast a lever is swinging; write it to launch a spinning body.
3D's is a 3-vector about the world axes, 2D's is the scalar rad/s about z with
the same sign as `rotation` (clockwise, because 2D is y-down). There is no
torque call in either. Writes land on the next step — these are live props, not
load-time constants.

**An impulse composes with a velocity written the same frame.** A mirror
steering a ball by `linearVelocity`, a magnet, a conveyor — and a kick in the
same `update` — used to lose the kick: the write reached the solver a step
later and overwrote it. Now `applyImpulse` lands the pending write first, then
the impulse, and `linearVelocity` reads the result at once (a harness that
reads it right after the kick sees the kick).

`fixedRotation` is authored, not live: it locks the body's rotation when the
body is CREATED. Changing it mid-run does nothing, and a locked body's
`angularVelocity` is zero rather than whatever was authored.

> Four of these were once false, silently: `applyImpulse` existed only in
> 3D (a TypeError in 2D), `body.velocity = […]` wrote a stray field nothing read
> in both, `mass`/`friction`/`restitution` were read once at creation in both,
> and `gravityScale` was live in 3D and dead in 2D.

### A ball that never stops (`angularDamping`, `linearDamping`)

A sphere on a flat floor, one impulse: 4.27 m/s at one second and 4.27 at
eight. Rapier has no rolling resistance — friction only converts slip into
spin, and a rolling ball is a wheel — so a golf ball, a bowling ball, a marble
and a kicked can all rolled to the edge of the world. Two props, per second,
`0` by default (nothing you built changes):

| prop | what it damps | reach for it when |
|---|---|---|
| `angularDamping` | the SPIN, so a rolling body coasts to a stop | anything that rolls: `2` stops a golf ball from 4 m/s in about four seconds, `0.5` is a long coast |
| `linearDamping` | the VELOCITY itself, `e^(-k·t)` | drag through air or water: a parachute, a puck on felt, a body that must not fly forever |

```json
{ "name": "Ball", "type": "RigidBody3D",
  "props": { "friction": 0.6, "restitution": 0.3, "angularDamping": 2,
             "collider": { "shape": "sphere", "radius": 0.2 } } }
```

Both are live — a green that slows the ball, a pond that drags it — and both
exist on `RigidBody2D` in the same terms. "At rest" is yours to decide: read
`physics.velocityOf(ball)` and call it stopped under ~0.05 for a few frames.
`examples/minigolf-3d` is the composition.

### A rudder: write `angularVelocity`

`body.angularVelocity = [0, rate, 0]` (or `body.angularVelocity[1] = rate`)
spins a `RigidBody3D` from that step on, in rad/s about the world axes —
`+y` turns +z toward +x. It is how `examples/harbor-3d` steers a boat, and
it did NOTHING in 3D until the harbor round: the adapter read the solver's
spin back every step and never looked at a script's write (2D always had the
path).
A `fixedRotation` body ignores it, as it ignores an authored spin.

### A crate that floats (`buoyancy`)

`RigidBody3D.buoyancy: 1` and a `Water3D` under it: the body is lifted by the
submerged part of its collider's volume at water's density, so `mass` against
the collider decides — a 1 m³ crate at 150 kg floats, at 3000 kg it sinks, a
wide hull rights itself, and it rides the water's waves. `Water3D.drag` slows
it. The whole rule, with the numbers, is in `incanto-environment.md` ("Things
that float"); `examples/harbor-3d` drives a boat with it.

## What is inside this Area right now?

`triggerEnter(other)` / `triggerExit(other)` tell you about CROSSINGS. A pressure
plate, a capture point, "how many enemies are in the blast" and a shop trigger
all want OCCUPANCY, and asking used to mean keeping your own `Set` fed by those
two signals — plus a geometric re-check every frame, because a body that is
teleported or freed inside a sensor does not reliably announce its exit.

```ts
const load = plate.overlapping('cargo')
  .reduce((kg, b) => kg + ((b as RigidBody3D).mass ?? 0), 0);
door.open = load >= 60;
```

`Area2D`/`Area3D` `overlapping(group?)` returns what the solver currently says is
inside, and drops freed nodes on read. **It includes the STATIC world** — a plate
laid into the floor genuinely contains the floor — so pass a group to ask the
question you usually mean.

## Joints: the three things that decide whether yours works

1. **A hinge is `revolute` with an `axis`** (3D; 2D's needs no axis). Before it
   existed the recipe was two `spherical` joints along the axis — that still
   works, but it has no stops and no motor, and the sentence "there is no hinge
   in 3D" was true for a year. A seesaw, a door, a wheel and a drawbridge are one
   joint each now.
2. **`collide` decides whether the two ends touch, and its default reads the
   type.** Rapier lets jointed bodies collide, and a hinge wants its bodies
   overlapping at the pivot — so the contact solver fights the joint and flings
   them. Before this, a first hinge produced a bar that spun chaotically forever
   and threw a 40 kg box thirty metres, and the author nearly filed "spherical
   joints inject energy".

   | joint | default | why |
   |---|---|---|
   | `fixed` · `spherical` · `revolute` | **off** | a pivot's bodies overlap by construction |
   | `rope` · `spring` | **on** | a tether anchors a thing to the world, and it still has to rest on it |

   Set `true`/`false` to say it outright. **It governs the two JOINTED bodies
   only** — never their contact with the rest of the world. Defaulting a spring
   off drops a barrel roped to the floor straight through it, which is how this
   table was arrived at.
3. **A joint's two ends must be two DIFFERENT bodies.** It is a child of one and
   points at the other, so `"target": ".."` — which reads like naming the body
   it belongs to — is now a load-time error: Rapier accepts a body jointed to
   itself and constrains nothing, and the rope fell exactly as if it were not
   there.
4. **A joint's own bodies are not the world to the character controller.**
   A `CharacterBody3D` crane hook with a chain of jointed links hanging off it
   used to RISE on its own (the controller's shape-cast saw the first link
   inside the hook's box and pushed the hook out of it, every step) and, parked
   over the next tower with its chain, landed twice as far as asked (the
   controller's ground ray hit that same link, so the moving-platform carry
   dragged the hook along by however far the link had moved — which was however
   far the hook had just moved). Now a body jointed to a character with the
   pair's contacts off is not its obstacle, and a body jointed to it is never
   its ground. So the natural crane — a kinematic hook you drive with
   `moveAndSlide`, a chain and a ball on `spherical` joints — just works;
   `examples/wreck-3d` is that crane. Both dimensions.
5. **The two bodies must AGREE about the axis.** A hinge's or a slide's `axis`
   is one vector, and Rapier builds a frame from it in EACH body's local space.
   So the two bodies' rotations may differ only by a turn ABOUT that axis: a
   drawbridge authored raised about its own hinge is fine; a wheel body turned
   on its side to lay its cylinder along the axle is not — the chassis reads
   `[1,0,0]` as world x and the wheel reads it as world y, and a buggy built
   that way went onto its roof on the first frame. A joint that breaks the
   rule is refused at creation, by name, with the fix: keep the body
   unrotated and turn the MESH inside it (`collider: auto` follows the mesh) —
   `examples/buggy-3d` is four wheels built exactly so.
6. **`anchor`/`targetAnchor` are local offsets from each body's ORIGIN**,
   resolved against the bodies' positions at load. If the two disagree the solver
   snaps the body into place on the first frame, and a 4 cm typo is silent. Author
   them with arithmetic (generate the scene) rather than by hand.

Limits and motors live on `revolute` (see the table above) — a lever's travel
is `limits`, not static geometry in its way.

## Raycasts

**Where `physics` comes from.** From a `Behavior` — where AI lives, and where
line of sight is decided — it is `this.physics`. At the boot site it is
`game.physics`, and anywhere you hold the engine it is `engine.physics`. All
three are the same object; it is `null` in a game with no physics bodies, so
`?.` and treat a missing world as "nothing in the way", or don't.

```ts
// inside a Behavior
const hit = this.physics?.castRay(eye, dir, range, this.node);
const blocked = hit != null && hit.node !== player;
```

Both runtimes expose the same query (2D in PIXELS y-down, 3D in meters):

```ts
const hit = physics.castRay(origin, dir, maxLen, excludeBody?, { staticOnly?: true });
// hit: { distance, normal, node, point } — `point` is WHERE it landed, in the
// scene's own units. `dir` may be any length; the ray normalizes it, so
// `origin + dir * distance` by hand is wrong unless you normalize first.
// → { distance, normal, node } | null   (sensors never block rays)
```

Both also answer two questions about a body that the node props cannot:

```ts
physics.velocityOf(body);  // px/s (2D, y-down) or m/s (3D) — from the SOLVER
physics.massOf(body);      // what the solver settled on, collider-derived
```

`linearVelocity` on the node is written back once per step, AFTER the solve — so
a behavior reading it from inside `fixedUpdate`, which is where a collision
response belongs, sees the value from BEFORE the impact. That is exactly the
moment a game wants to know how hard it hit something. Scale a push by `massOf`
rather than by the authored `mass` prop, which the solver may not be using.

`dir` may be ANY length — `target - eye` is the usual spelling and is metres
long. It is normalized for you, so `distance` and `maxLen` are always plain
metres (2D: pixels), never multiples of the vector you passed. A zero-length
direction points nowhere and returns `null`.

Exclude the shooter's own body when casting from inside it — a ray that starts
inside its own collider hits itself at distance 0, which reads as "blocked" and
is the reason a vision cone can come back permanently blind.

Both also have a THICK ray — a sphere sweep in 3D, a circle sweep in 2D — for
probes where skimming matters (the camera boom, ledge feelers, a shot that must
not thread a one-pixel gap between two floor tiles):

```ts
const hit = physics.castSphere(origin, dir, radius, maxLen, excludeBody?, { staticOnly?: true });
// → { distance, node } | null   (distance = travel of the CENTRE)
```

## Gamepad

`engine.input.attachGamepad(engine)` polls the first connected pad every
frame (headless no-op). Buttons are codes `Pad0`..`Pad16` in the SAME space
as keys — declare them in actions: `"jump": { "keys": ["Space", "Pad0"] }`
(standard mapping: Pad0=A/×, Pad1=B/○, Pad9=Start). Sticks:
`input.padAxes(0)` / `padAxes(1)` → deadzoned `{x, y}`. Unplugging releases
everything (no stuck inputs).

## Changing colliders at runtime

REPLACE the object — `body.collider = { shape: 'rect', size: [w, 60] }` —
and physics rebuilds the body that step. In-place mutation of the existing
collider object (`body.collider.size[0] = w`) is NOT watched (the per-step
change scan was removed for performance).

**To switch a body OFF, write `enabled = false`** — not `collider = null`. The
prop is an object and a non-object used to reach the physics sync and throw
`null is not an object (evaluating 'node.collider.shape')` out of the frame
loop, with no node path: the loader refuses `"collider": null` in JSON with a
typed error, and a runtime write was checked nowhere. It is now refused with
that sentence, naming the node, and the collider it had is kept.

## Debug drawing

`physics.debugDraw = true` (the instance `enablePhysics2D/3D` returns) renders
every collider as light-green wireframe lines IN THE GAME VIEW — the shape data
the solver holds, so what you see is what it collides with. Default OFF; toggle
it live whenever physics feels wrong.

`physics.debugScope = node` narrows those lines to ONE node: its own colliders,
its descendants', or the nearest body above it when it owns none (so a visual
mesh child shows the body it belongs to). Null again for the whole world. Without
it, a map with terrain is a wall of grid lines and the shape you came to check is
somewhere inside it. The debug overlay's **Colliders** menu item cycles
off → all → selected over exactly these two fields.

### Colliders without a simulation

`enablePhysics3D(engine, { simulate: false })` (and the 2D twin) builds the world
and never advances it: the body set is kept in step with the tree, every body is
posed FROM the tree each tick and propagated to its colliders, and then it stops
— no solver, no collision events, no write-back into node props. `debugDraw`
works exactly as above.

This is how the scene editor shows colliders while you edit: the real shapes,
including the ones that are not in the scene JSON at all (a mesh-fitted body, an
InstancedMesh3D scatter's hulls, a terrain, a character capsule), sitting where
the tree says, without your scene falling over while you look at it. Use it for
any "show me the collision geometry" tool. Do NOT use it to freeze a running
game — that is `engine.timeScale = 0`, which keeps the world coherent.

## A walking body climbs a step (`stepHeight`, 2D **and** 3D)

`CharacterBody3D.stepHeight` (default `0.35` m) and `CharacterBody2D.stepHeight`
(default `35` px — the same 0.35 m at the 2D world's scale of 100 px = 1 m,
which is why gravity defaults to 980) are how high a ledge the body walks UP
without jumping. `0` = off. It is Rapier's autostep, and **Rapier does not
autostep unless it is asked** — before 0.63 nothing asked in 3D, and for longer
still nothing asked in 2D at all, where a lip ONE PIXEL high stopped a walking body
dead.

You will not see this on the player: `CharacterController3D` rides a hover spring
rather than the character controller, so it floats over small ledges already.
It is the ENEMIES that walk, and a chaser walks a straight line — it cannot go
around. Measured on the shipped `tps-3d` template, whose arena has a 0.6 m ramp
between the spawn and the player, standing still for 45 seconds:

```
before   hits=0  hp=100  closest an enemy ever got = 33.91m
after    hits=9  hp=..   closest = 1.80m   (exactly Chase.stopRange)
```

They chased at full speed the whole time and piled up against the near face of a
knee-high box. Raise `stepHeight` for a world with real stairs; set it to `0` for
something that genuinely should be blocked by a curb.

**It loses to a large downward velocity.** Autostep happens inside the movement
solve, so a body driven with a big constant gravity term every frame never
climbs. Same 0.3 m ledge, same `stepHeight: 0.35`, only the `velocity[1]`
differs: `0` → cleared, `−0.1` → cleared, `−2` → stopped at the face. If you are
integrating your own gravity, apply it as a falling SPEED that resets on the
ground, not as a constant push. (`moveBody`, which `Chase` and `Patrol` use,
derives velocity from a position delta and carries no such term.)

## Moving platforms carry their riders (2D **and** 3D)

A `CharacterBody2D`/`CharacterBody3D` standing on a body that moves is dragged
along by however far that body moved — elevators, patrol platforms, conveyors,
rotating discs. It is what makes a floating-island game work.

Author it the obvious way: move the platform's `position` (a behavior, a
`PathFollow`, an `Oscillate`). Nothing else to declare.

**The 3D PLAYER is a different rig, and it needs one prop.** A
`CharacterController3D` must sit under a dynamic `RigidBody3D` (hard error
otherwise), not a `CharacterBody3D` — the character bodies in a 3D game are its
enemies and NPCs. The controller steers toward a target velocity every fixed
step, so pressing nothing means "target zero", which in world space is a brake
aimed at the platform's own motion. `platformCarry` (default **true**) makes
that target relative to the floor instead. Measured on a 400 m deck moving
4 m/s, rider pressing nothing: **95.9%** of the travel kept, **0.0%** with
`platformCarry: false`.

```json
{ "name": "Ctl", "type": "CharacterController3D",
  "props": { "view": "free", "platformCarry": false } }
```

Turn it off for a conveyor you want to be scenery. Vertical carry needs no prop
either way — the hover spring rides whatever surface is under it, which is why
a 3D elevator always looked right while a conveyor did not.

The carry comes from the platform's POSITION DELTA, not its `linearVelocity`,
because writing `position` calls `setTranslation` — a teleport that transfers no
momentum to anything. Both authoring styles work.

The carry stops the moment the character is no longer grounded on it, so walking
off an edge or jumping is not "sticky", and it is vertical as well as horizontal
— an elevator lifts you.

## The mouse as gameplay input (click, hover)

`pointerDelta()` answers "how far did the mouse move" — the mouse-look question.
For match-3, tower defense, card games, point-and-click and RTS that is the
wrong question, and it used to be the only one the engine could answer.

```ts
engine.input.pointerPosition();   // { x, y } in CANVAS pixels, or null
engine.input.mousePressed(0);     // 0 left · 1 middle · 2 right
engine.input.mouseJustPressed(0); // and mouseJustReleased(0)
game.pick(x, y);                  // the node under that pixel, or null
```

A stretched canvas is handled: `pointerPosition()` scales client → canvas pixels,
so the coordinates are the ones `pick()` wants.

**The pointer has to be attached, and now attaches itself.** `createGame2D`/
`createGame3D` turn it on when the scene contains a `Clickable` — the same
`'auto'` shape `physics` has always had — and they ask again on every scene
change, because a title screen you click into a level has no `Clickable` in it
(a `UiButton` is DOM) and the level is full of them. Pass `pointer: true` explicitly when a
behaviour of YOUR OWN reads `pointerPosition()`; the boot cannot see that. A
`Clickable` that finds no pointer (or no picker) for two seconds says so by
name instead of sitting there inert, which is what it used to do: no error, a
clean `incanto-check`, a clean audit, and a board that did nothing when clicked.

From scene JSON, the **`Clickable`** behavior needs no code at all:

```json
{ "name": "Tile", "type": "MeshInstance3D", "script": { "name": "Clickable" } }
```

| prop | default | |
| --- | --- | --- |
| `button` | `0` | 0 left · 1 middle · 2 right |
| `maxDistance` | `0` | ignore clicks further than this **from the current camera** (0 = any) |
| `enabled` | `true` | stop responding without detaching |

Signals: **`clicked(node)`**, **`hovered(node)`**, **`unhovered(node)`** — wire
them in `connections` like any other. **Each one carries the node it happened
on**, so one handler can serve a whole board of tiles:

```jsonc
{ "signal": "clicked", "from": "/Board/Tile3", "to": "/Board", "handler": "onTileClicked" }
```
```ts
onTileClicked(tile: Node) { this.flip(tile); }   // which tile, without a wire each
```

`clicked` fires on RELEASE over the same node the press started on (a drag that
ends elsewhere is not a click, the way every button on every platform behaves),
and a hit on a CHILD counts as a hit on the node — the raycast lands on the
visual mesh, which is usually a child.

**A finger that lifts stops hovering.** `hovered`/`unhovered` follow the cursor,
and a mouse cursor stays where you left it — but a finger ceases to exist, so a
touch release clears the pointer at the end of that frame (after the click has
fired, not with it). Without that the last-tapped node stayed `hovering: true`
for the rest of the session: a permanently highlighted tile, mole or card on
every phone. Do not build a hover-only affordance — on touch there is no hover
before the tap, only after it, and only for a frame.

**`visible: false` is not clickable.** A pixel the frame does not draw has
nothing under it, so hiding a node (or any ancestor of it) takes it out of the
pick — which is how you disable a button, a dialogue choice or a mole down its
hole without detaching anything. Before 0.71 three's raycaster reported hidden
geometry and `pick()` handed it straight to `Clickable`.

### Testing a mouse-driven game headlessly

**Use `runScript`'s `click` step** — `{ atMs: 100, click: 'Board/Tile3' }` — which
installs a geometric picker, points the cursor and spreads the press and release
across the two frames `Clickable` needs. `incanto-playtest` clicks too, and its
report says how many clicks it landed. See `incanto-verifying-your-game.md`.

The raw recipe below is what that does, and is still what you want when the
cursor has to be somewhere a node is not. There is no renderer and therefore no
raycast, so `engine.picker` is null and `Clickable` is inert. That is not a dead
end: give the engine a picker of your own and drive the cursor.

```ts
// A picker that answers from the tree instead of a GPU raycast.
engine.picker = (x, y) => hitTestYourBoard(x, y);       // return a Node or null

engine.input.setPointerPosition(150, 0);                // where the cursor IS
engine.input.handleMouseButton(0, true);                // press …
engine.step();
engine.input.handleMouseButton(0, false);               // … and release
engine.step();                                          // `clicked` fires here
```

Note the BUTTON, not an action: `Clickable` reads `mouseJustPressed` directly,
so `pressAction('click')` drives nothing. Moving the cursor between the press
and the release correctly produces no click.

Without this a mouse game's entire input surface is load-validated and never
once executed — the scene is legal, every wire resolves, and nothing has ever
been clicked.

## Placement rules

`CharacterController2D` MUST be a direct child of a `CharacterBody2D` — the
loader hard-fails otherwise (it steers its parent body; nothing else has a
`moveAndSlide`). Bodies themselves can sit anywhere in the tree.

## Validation

Collider shapes are validated at LOAD TIME: a wrong shape (`box` on a 2D body,
`circle` on a 3D body) or missing dimensions is a hard `loadScene` error — you
find out when you author the JSON, not when physics starts. A body with NO
collider loads fine and physics skips it with a console warning;
`moveAndSlide()` on it throws a "has no collider" error.


## Pointer input (mouse look, buttons, wheel)

`engine.input.attachPointer(canvas, { lockOnClick: true })` wires the mouse:
buttons become codes `Mouse0/1/2` usable in any input-map action, look
movement accumulates into `input.pointerDelta()` (drain once per frame —
movementX/Y, so pointer lock just works), and `input.wheelDelta()` drains the
wheel. `lockOnClick` requests pointer lock on click — the FPS pattern.
