# Controller

> Talos next · Since 0.6

Present on `ctx.controller` when the entity has a character-controller component (kinematic capsule).

**Order within a tick.** The controller steps once per tick, after physics and before behaviours, so everything a behaviour writes here — `move`, `jump`, `setVelocity`, `addVelocity`, `gravityScale` — takes effect on the controller's step at the top of the next tick, and everything it reads describes the step that just ran. At that step, in order: moving ground carries the character, an upward velocity write lifts it off the ground, `move` accelerates the horizontal velocity towards its target, a `jump` press sets the vertical speed, gravity is applied, and the capsule is swept. **Velocity writes and `move`.** The controller's velocity is one number that everything shares. `setVelocity`/`addVelocity` change it; `move(dir, speed)` then accelerates the horizontal part of it towards `dir * speed` at the ground acceleration (50 m/s²) or the air acceleration (12 m/s²). So a sideways shove on the ground is braked within a few ticks, and a launch's horizontal speed bleeds towards whatever the stick asks for while it is in the air. A game that wants a launch to keep its speed keeps asking `move` for that speed until it lands. **Who else writes it.** With the component's `input: "auto"` (the default) the ENGINE reads the device and drives this controller itself: at the top of every tick, before the controller steps and before any behaviour runs, it writes the player's `move` stick (keys, pad or a `POST /input` hold) as the movement, jumps on the jump button at the component's `jump` height, latches crouch and turns the heading with `look`. A `move` from here is written after that step, for the next one, and the next tick's stick overwrites it: **while the stick is pushed, the stick wins**, and the engine jumps whatever this behaviour does. `inputBasis` picks the stick's frame: `"camera"` (default: away from the drawn view, screen-right is right; world −Z with no camera), `"world"` (−Z) or `"controller"` (this heading). With `input: "script"` the engine writes nothing here; `ctx.input` still reads the device, this behaviour is the only writer, and `ctx.camera.viewYaw` gives the frame the engine would have used. See docs/API.md, "Who drives the controller".

Reached in a behaviour as `ctx.controller`.

## Signature

```ts
export interface Controller {
  /** Request movement this tick in world direction `dir` at `speed` m/s; slopes, steps and collisions resolve in Rust. */
  move(dir: Vec3, speed: number): void
  /**
   * Jump on the next tick, if grounded and standing. `height` is metres above the take-off point, reached
   * under this character's own gravity (so `gravityScale` changes how long the jump takes, not how high it
   * goes); omitted, the component's `jump` height. A press in the air does nothing and is not saved for the
   * landing: a second jump in the air is the game's, built with {@link Controller.setVelocity}.
   * Holding the press (calling this every tick) jumps once. Throws `RangeError` for a negative or non-finite
   * height.
   */
  jump(height?: number): void
  crouch(on: boolean): void
  /**
   * Replace the velocity, m/s. Visible at once through {@link Controller.velocity}; integrated from at the
   * next step. A write whose `y` is above zero lifts a grounded character off the ground without a `jump`
   * event — a jump pad, a knockback, a vehicle's collision kick — which is the only way to launch a
   * character upward. Throws `RangeError` for a NaN or infinite component.
   */
  setVelocity(v: Vec3): void
  /**
   * Add to the velocity, m/s. A velocity change rather than a force: the character has no mass. Calls in
   * one tick add up. Same ground rule and errors as {@link Controller.setVelocity}.
   */
  addVelocity(v: Vec3): void
  readonly grounded: boolean
  readonly velocity: Vec3
  /** Normal of the surface under the feet, walkable or not (a slope too steep to stand on still reports); null in the air. */
  readonly groundNormal: Vec3 | null
  /** The entity stood on while grounded — a platform, a vehicle deck, a floor whose tag picks a footstep sound; null otherwise. */
  readonly groundEntity: EntityHandle | null
  /**
   * How fast moving ground carried the character on the last step, m/s; zero on still ground, in the air and
   * with `moveWithGround` off. A rider that jumps or walks off moving ground keeps this speed on its own —
   * the jump is relative to the platform — and {@link Controller.velocity} includes it while airborne.
   */
  readonly groundVelocity: Vec3
  /**
   * A moving body closing on the character on the last step — a door coming down on its head, a lift
   * carrying it into a ceiling, a kinematic wall sliding into it — or null. The character is never pushed
   * through the floor or a wall to make room: it stays where it is, can still walk out from under the
   * mover, and the engine raises `"character-squeezed"` ({@link BusCharacterSqueezed}) once when it starts.
   */
  readonly squeezedBy: EntityHandle | null
  /**
   * The last surface too steep to stand on that the character walked into on the last step, as a unit
   * normal pointing out of it; null when the move touched no wall. For a wall slide, a vault, a bump sound.
   */
  readonly wallNormal: Vec3 | null
  /** The last move ran into something it could neither climb nor slide past. */
  readonly blocked: boolean
  /**
   * Multiplier on this character's gravity — the component's `gravity`, else the length of the scene's
   * `meta.gravity`; 1 by default. 0.5 for a low-gravity zone or a glide, 2 for a
   * heavier fall, 0 to hang. Throws `RangeError` below 0 or when not finite.
   */
  gravityScale: number
  /**
   * Whether moving ground carries this character — a lift, a ferry, a turntable, a tipping seesaw — by its
   * movement and turn since the last tick, before the character's own move. On by default; off for a
   * character that should slide off a moving deck.
   */
  moveWithGround: boolean
}
```

Import: `import type { Controller } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `move` | `move(dir: Vec3, speed: number): void` | Request movement this tick in world direction `dir` at `speed` m/s; slopes, steps and collisions resolve in Rust. |
| `jump` | `jump(height?: number): void` | Jump on the next tick, if grounded and standing. `height` is metres above the take-off point, reached under this character's own gravity (so `gravityScale` changes how long the jump takes, not how high it goes); omitted, the component's `jump` height. A press in the air does nothing and is not saved for the landing: a second jump in the air is the game's, built with {@link Controller.setVelocity}. Holding the press (calling this every tick) jumps once. Throws `RangeError` for a negative or non-finite height. |
| `crouch` | `crouch(on: boolean): void` |  |
| `setVelocity` | `setVelocity(v: Vec3): void` | Replace the velocity, m/s. Visible at once through {@link Controller.velocity}; integrated from at the next step. A write whose `y` is above zero lifts a grounded character off the ground without a `jump` event — a jump pad, a knockback, a vehicle's collision kick — which is the only way to launch a character upward. Throws `RangeError` for a NaN or infinite component. |
| `addVelocity` | `addVelocity(v: Vec3): void` | Add to the velocity, m/s. A velocity change rather than a force: the character has no mass. Calls in one tick add up. Same ground rule and errors as {@link Controller.setVelocity}. |
| `grounded` | `readonly grounded: boolean` |  |
| `velocity` | `readonly velocity: Vec3` |  |
| `groundNormal` | `readonly groundNormal: Vec3 \| null` | Normal of the surface under the feet, walkable or not (a slope too steep to stand on still reports); null in the air. |
| `groundEntity` | `readonly groundEntity: EntityHandle \| null` | The entity stood on while grounded — a platform, a vehicle deck, a floor whose tag picks a footstep sound; null otherwise. |
| `groundVelocity` | `readonly groundVelocity: Vec3` | How fast moving ground carried the character on the last step, m/s; zero on still ground, in the air and with `moveWithGround` off. A rider that jumps or walks off moving ground keeps this speed on its own — the jump is relative to the platform — and {@link Controller.velocity} includes it while airborne. |
| `squeezedBy` | `readonly squeezedBy: EntityHandle \| null` | A moving body closing on the character on the last step — a door coming down on its head, a lift carrying it into a ceiling, a kinematic wall sliding into it — or null. The character is never pushed through the floor or a wall to make room: it stays where it is, can still walk out from under the mover, and the engine raises `"character-squeezed"` ({@link BusCharacterSqueezed}) once when it starts. |
| `wallNormal` | `readonly wallNormal: Vec3 \| null` | The last surface too steep to stand on that the character walked into on the last step, as a unit normal pointing out of it; null when the move touched no wall. For a wall slide, a vault, a bump sound. |
| `blocked` | `readonly blocked: boolean` | The last move ran into something it could neither climb nor slide past. |
| `gravityScale` | `gravityScale: number` | Multiplier on this character's gravity — the component's `gravity`, else the length of the scene's `meta.gravity`; 1 by default. 0.5 for a low-gravity zone or a glide, 2 for a heavier fall, 0 to hang. Throws `RangeError` below 0 or when not finite. |
| `moveWithGround` | `moveWithGround: boolean` | Whether moving ground carries this character — a lift, a ferry, a turntable, a tipping seesaw — by its movement and turn since the last tick, before the character's own move. On by default; off for a character that should slide off a moving deck. |

## Example

The tested fixture `fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/elevator.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A lift: a kinematic slab that rides between two heights and waits at each end.
 *
 * It only moves itself, by `position`. Whoever stands on it is carried by the engine — the character
 * controller follows moving ground on its own — so there is no rider list and no parenting here.
 */
interface State {
  /** +1 going up, -1 going down, 0 waiting. */
  heading: number
  /** Seconds left to wait at the current end. */
  wait: number
  /** Where the next leg goes once the wait is over. */
  next: number
}

interface Props {
  /** Height of the slab's origin at the bottom and the top, metres. */
  bottom?: number
  top?: number
  /** Metres per second. Default 1.5. */
  speed?: number
  /** Seconds to wait at each end. Default 1. */
  pause?: number
}

export default {
  init(ctx) {
    if (typeof ctx.state.heading !== "number") ctx.state = { heading: 0, wait: ctx.props.pause ?? 1, next: 1 }
  },

  update(ctx, dt) {
    const bottom = ctx.props.bottom ?? 0
    const top = ctx.props.top ?? 4
    const p = ctx.entity.position
    if (ctx.state.heading === 0) {
      ctx.state.wait -= dt
      if (ctx.state.wait <= 0) ctx.state.heading = ctx.state.next
      return
    }
    let y = p[1] + ctx.state.heading * (ctx.props.speed ?? 1.5) * dt
    const end = ctx.state.heading > 0 ? top : bottom
    if ((ctx.state.heading > 0 && y >= end) || (ctx.state.heading < 0 && y <= end)) {
      y = end
      ctx.state.next = -ctx.state.heading
      ctx.state.heading = 0
      ctx.state.wait = ctx.props.pause ?? 1
    }
    // `position`, not `teleport`: this is a move, so it is smoothed between ticks and the rider is
    // carried by exactly the distance the slab travelled.
    ctx.entity.position = [p[0], y, p[2]]
  },
} satisfies Behavior<State, Props>
```
