---
name: incanto-game-feel
description: Measure what your controls actually feel like — bunx incanto-feel reports top speed, ramp and stop times, turnaround, jump apex and airtime, and the coyote/jump-buffer windows MEASURED by probing rather than read from the schema. Use when tuning a character, or when a game "feels off" and you cannot see why.
---

# Game feel, as numbers

`capture()` and `framing()` are snapshots. Feel lives on the **time axis**.

```bash
bunx incanto-feel src/game.scene.json
```

```
feel of /Game/Player

  pushed           'move' [1, 0]
  top speed        5.62 u/s
  to full speed    383 ms
  to a stop        133 ms
  turnaround       367 ms
  input → motion   1 frame(s)

  jump apex        0.262 u   (tapped)
  held apex        0.981 u   (standing)
  moving apex      1.24 u walking · 1.41 u sprinting   ← size your level against the LARGEST
  time to apex     133 ms
  airtime          267 ms
  coyote window    117 ms
  jump buffer      150 ms
```

A `✗` or `!` line above the numbers is what the ENGINE said while they were
being taken — a script that threw and was quarantined, a camera following a node
with no position. The measurements below it are still real (a behaviour is
quarantined, the node is not), but a page of confident numbers taken while a
script was dying reads as a healthy character, so read those lines first.

## Why this exists

**`top speed` is what the character SUSTAINS, not its fastest frame.** The probe
holds `move` for five seconds and takes the 90th percentile of the run, because
the maximum belongs to whatever hit the character last. Measured on the shipped
`platformer-2d`, whose `RUN_SPEED` is 250: it reported `top speed 300 u/s` —
that is the knockback (then a hand-written `KNOCK_X`, now the player Health's
`knockback`), from an enemy the probe walked into — and `to full speed
2367 ms`, which was the walk to the enemy, on a character that reaches its speed
in ONE frame. Four numbers, all about a character being attacked. When the peak
and the sustained speed disagree the report says so:

```
top speed        250 u/s
to full speed    17 ms
peak             300 u/s   ← SOMETHING ELSE moved it
```

Measure feel somewhere nothing can hit you.

**A character that jumps in MID-AIR has no coyote window to measure.** The probe
lifts the body, waits, presses, and reads "going UP after the press, and sent
there by it" as a jump —
so a double jump (or a jetpack, an updraft) answers yes at any delay. It used to
read "velocity CHANGED upward" instead, which a glide satisfies too: Beacon
Isle's glide clamps a −20 m/s fall to −1.6, a +18 change, and the report called
its coyote window `not measurable — jumps in MID-AIR` about a character that
does not, with the buffer beside it marked suspect. Now it reads

```
coyote window    117 ms   (declared 120)
jump buffer      33 ms    (declared 150)
```

and the 33 is true, not suspect: holding `jump` while falling IS the glide,
so a press more than two frames before landing becomes a one-frame glide and
the buffered jump is spent. A starter that glides on the jump button buys a
short buffer with it. To measure the window of a true mid-air jumper, probe a
build with the air jump off.

**`jumpVelocity: 4` is not a height.** It is an impulse, and what it produces
depends on gravity, mass, drag and `fallGravity`. The apex in world units and the
hang time in milliseconds are what you are actually choosing.

**It says what it pushed.** The `pushed` line names the action AND the direction
that produced every number under it. The probe used to hold `(1, 0)` and only
`(1, 0)`, so a game whose movement reads the OTHER axis of the same action —
`-v.y`, which is this engine's own documented forward, `getVector` being y-down —
measured nothing and printed the nothing as `top speed 0 u/s` with four blank
rows and exit 0. A racer that its own harness clocked at 41 m/s reported 0.

**The windows are MEASURED, not read.** `coyoteMs` comes from probing — lift the
character clear of its support, wait, press, see whether it launched, binary
search the boundary. A `coyoteSeconds: 0.12` that the controller never consults
reports **0 ms**. That was a real bug in this engine's own 3D controller until
0.37, and the JSON looked correct the entire time.

## Reading it

| line | what it tells you |
|---|---|
| `to full speed` | acceleration. Under ~100 ms is twitchy-responsive; over ~500 ms is weighty. |
| `to a stop` | how much you slide. A platformer that overshoots ledges has a long one. |
| `turnaround` | the cost of changing your mind. The number players feel most and name least. |
| `input → motion` | 1 frame is immediate. More than 2 and something is buffering. |
| `jump apex` / `airtime` | compare against the gaps and platform heights in your level. **Use the HELD apex** — the plain one is a one-frame TAP, and with `jumpCutMultiplier` (the variable-height jump the 3D character skill recommends at `0.45`) that is a fraction of the real jump: measured 0.478 u tapped against 1.831 u held on the same scene. The held line only appears when the two differ. |
| `coyote window` | **0 reads to players as "the controls are broken"**, not as a missing feature. |
| `jump buffer` | 0 throws away a press made a few frames before landing. |

## When it cannot move your player

If the default push finds no motion, the tool SEARCHES: every declared `vector2`
action, each pushed ±x and ±y, and it keeps whichever moved the body furthest.
That is a fallback, never a replacement — a scene that already answered cannot
have its numbers changed by it. Two real games it rescues: a throttle on `-v.y`,
and a game where an unrelated `aim` happened to be declared FIRST in the input
map (the default move action is "the first `vector2`").

When nothing moves it says so instead of printing a zero:

```
  pushed           'move', 'aim' every way (±x, ±y) — the player did not move

  could not move /Game/Turret: no motion in 300 frames with 'move', 'aim' pushed
  ±x and ±y. Either the movement is not wired to a vector action, or it is
  driven by buttons. Nothing below was measured.
```

`--move <action>` pins the action explicitly, and pinning it turns the search
OFF — you asked about that action, so a silent answer about a different one
would be worse than no answer.

## It presses your title screen first

A game with a title screen boots PAUSED — `GameFlow.pause()` from the shell's
`onReady` is what holds the world behind the menu — and a probe that drives a
stopped world measures nothing. Three of this repo's own examples ship that
shell, and so does every game scaffolded from one.

So before measuring, the probe presses what a player can reach: the HUD's
`focusables()` (which already excludes hidden panels and disabled buttons),
buttons labelled PLAY / START / RESUME first, until `engine.timeScale` stops
being 0. Nothing is pressed in a game that was already running.

When even that does not start it, the report says so instead of printing zeros:

```
  the clock never ran: this game was PAUSED for the whole measurement
  (`engine.timeScale` 0), and it stayed that way after pressing every button on
  screen. …

  coyote window    not measured — the clock was stopped   (the scene declares 120 ms)
  jump buffer      not measured — the clock was stopped   (the scene declares 150 ms)
```

**A window nobody could measure is not a window of zero.** Before this, a shipped
starter that declares `coyoteSeconds: 0.12` reported `coyote window 0 ms` and
advised its author to set `coyoteSeconds: 0.12`. The advice below only ever fires
on a number that was actually taken.

It also sits out an opening **cutscene**. That is the other way a game holds the
player still, and it looks nothing like a pause: the clock runs, the world moves,
and only `controller.enabled` is false while a camera flies around. The probe
waits up to fifteen seconds for control and says so if it never comes.

## The windows are searched by JUMPING

`coyote window` and `jump buffer` are measured by lifting the body and pressing —
so a character that never leaves the ground fails every step of that search, and
that failure came back as `0`. Three shipped examples printed
`jump apex — (nothing left the ground)` and then `coyote window 0 ms` with advice
attached, one of them declaring `coyoteSeconds: 0.12` in the same file.

When nothing jumps in any of the four probes (standing, held, walking,
sprinting), both windows read `not measured — nothing ever jumped` and the report
says the thing worth saying instead:

```
  note: this scene declares 'jump' and NOTHING left the ground in any
        of the four jump probes — standing, held, walking, sprinting. Check that the
        controller reads 'jump', that its `jumpVelocity` is not 0, and that it is `enabled`.
```

**In a two-player scene, the player it drives and the buttons it presses are the
same character's.** They were not: the props were read from whichever controller
a stack-walk reached first, which is the LAST one in the file, so a local co-op
game's player one was sent player two's jump button and reported a jump apex of
nothing. Move survived it — the move probe has a fallback SEARCH — and jump has
none, which is why the whole jump half of that report was dashes.

## It reports, it does not grade

There are no PASS/FAIL bands here on purpose. A twitch platformer and a survival
horror want opposite numbers, and a table of thresholds invented by an engine
would be confidently wrong for most games. Two exceptions are called out because
they are almost never intentional: a coyote window of 0 and a jump buffer of 0.

## Tuning loop

```bash
bunx incanto-feel src/game.scene.json          # measure
# edit coyoteSeconds / jumpBufferSeconds / jumpVelocity / fallGravity
bunx incanto-feel src/game.scene.json          # measure again
```

The props live on `CharacterController2D` / `CharacterController3D` — see
`incanto-3d-character.md`.

## Pairs with the playtester

`incanto-feel` answers *how does it move*. `incanto-playtest` answers *can it be
finished*. A level whose jump apex is 0.26 u and whose gaps are 2 u apart will
show up in the second as `never reached`.
