# Actuators

# Actuators

Package `@ferret/physics-actuators`, capability name `actuators`, phase `field`.

## What it is for

"This force, at this point, on this schedule", as data: a push of a hand, the stroke of an oar, a gust on a flag, a motor that holds a speed. The forces enter the same integration as gravity and drag (the `field` phase), so a free body obeys momentum and angular momentum exactly: one off-centre force turns the body.

## An actuator

```json
"physics": {
  "capabilities": ["actuators"],
  "actuators": [
    { "name": "oar-left", "node": "Boat", "kind": "force", "at": [-0.9, 0, 0.2],
      "dir": { "frame": "body", "v": [0, 0, 1] }, "magnitude": 600,
      "profile": { "periodic": { "period": 2, "duty": 0.4, "shape": "half-sine" } }, "command": 0 }
  ]
}
```

| Field | Meaning |
|---|---|
| `node` | the entity of the body (a `RigidBody`), or a target added with `addTarget` (a `Vessel` through `vesselTarget`) |
| `kind` | `force` (N), `impulse` (N·s per firing), `torque` (N·m), `motor` (holds `speed`, `"30rpm"` or rad/s, within `maxTorque`, `gain` N·m per rad/s), `spin` (a shaft that turns: below) |
| `at` / `point` | where it acts: a point in the body's frame, or the name of a child node whose origin is the point (default: the centre of mass) |
| `dir` / `axis` | a vector in the body frame (a plain `[x, y, z]`), or `{ "frame": "body" \| "world" \| "<other node>", "v": [...] }` |
| `profile` | `"const"`, `{ "for": { duration } }`, `{ "delay": { after, inner } }`, `{ "periodic": { period, duty, phase, shape } }` (`square`, `sine`, `half-sine`, `triangle`, `saw`), `{ "pulse": { period, count } }`, `{ "curve": { keys, interp, loop } }`, `{ "ramp": { time } }` |
| `over` | what the profile runs over instead of time: `{ "angle": "<node>", "turns": 1 }` (one cycle per turn of that body about `axis`), `{ "phase": "<actuator>" }`, `{ "sensor": "<name>" }` (added with `addSensor`); then the profile's variable is in cycles and its period is 1 |
| `instances`, `offset` | copies of one definition shifted along what the profile runs over (`"180deg"` for an angle): a firing order, two oars |
| `command` | the run-time multiplier (a throttle); `on: false` starts it off |

An `impulse` fires on each firing of a `pulse` profile, or on the rising edge of any other profile.

## Readouts and events

`actuator.state` gives per actuator: force, torque, power (F·v at the point, τ·ω), work since start, the profile value, the phase of the cycle, the number of firings, and an `error` when its body is missing. Events: `actuator:start`, `actuator:stop`, `actuator:done` (a `for` or finite `pulse` ended), `actuator:stroke` (an impulse fired).

## Commands

`actuator.command { name, command }`, `actuator.on`, `actuator.off`, `actuator.fire { name }` (from the start), `actuator.state { name? }`; editor: `edit.physics.actuator.set { name, node, kind, ... }` (checked against the schema), `.remove`, `.list`.

## Spin: a shaft that turns

`{ "name": "mill", "node": "MillShaft", "kind": "spin", "axis": [1,0,0], "speed": "12rpm", "mode": "unstoppable" }`. `mode` is one or a list:

- `unstoppable`: a **kinematic** body whose angle is written as `initialAngle + ω·t` on the shared clock (not integrated), so it is exact after hours, needs no replication on the net, never slows, carries and crushes what touches it.
- `jammable`: a motor with `maxTorque`; held under 10 % of its speed for `stallTime` (0.5 s) it raises `actuator:stall { stalled: true }`, and `stalled: false` once it is above half speed again.
- `switchable`: `actuator.command` 0 or 1 turns it on and off along `rampUp` / `rampDown` seconds; the angle is the exact integral of the ramp; switch instants are the event `actuator:switch`. Alone it is an unstoppable shaft with a switch; it combines with either of the others.

Inside a machine: `addSpinSink(node, drivetrain.speedSource(...))` makes the spin the speed source of a drivetrain shaft (the shaft is eternal, the gears behind it feel real torques). `actuator.state` adds `speed`, `angle` and `stalled`.

## Bindings: who drives the command

Bindings (stage A3) connect an actuator's `command` to the world without code. They live in the level's `physics.bindings` list (dropped with the level), or `edit.physics.binding.set` / `.remove` / `.list`, or `cap.bind(def)`.

```json
"physics": {
  "capabilities": ["actuators"],
  "actuators": [ { "name": "oar-left", "node": "Boat", "kind": "force", "point": "Oarlock_L", "dir": [0, 0, 1], "magnitude": 400, "command": 0 } ],
  "bindings": [
    { "actuator": "oar-left", "action": "row-left", "as": "press" },
    { "actuator": "oar-left", "event": "anim:notify", "match": { "entity": "Rower", "name": "blade-in" },  "as": "set", "value": 1 },
    { "actuator": "oar-left", "event": "anim:notify", "match": { "entity": "Rower", "name": "blade-out" }, "as": "set", "value": 0 },
    { "actuator": "oar-left", "animate": { "entity": "Rower", "trigger": "row", "on": "start" } },
    { "actuator": "winch", "interactable": "WinchLever" },
    { "actuator": "gust", "zone": "Hill", "actor": "tag:player" }
  ]
}
```

One source per binding; `as` says what the source does (the default is in bold):

| Source | Modes |
|---|---|
| `action` (an input action) | **`hold`** (command while down), `press` (one impulse or one cycle: `actuator.fire`), `toggle`, `axis` (the stick or trigger value times `value`; `component` x or y) |
| `interactable` (`interact:use` of an entity) | **`toggle`**, `press`, `on`, `off` |
| `zone` (`zone:enter` / `zone:exit`, `actor` = an entity name or `tag:player`) | **`inside`** (command while anyone counts as inside), `toggle`, `on`, `off`, `press` |
| `event` (any world event; `match` compares payload fields, an entity matches by name) | `on`, `off`, `toggle`, `press`, `set` (the command becomes `value`) |

`value` is the command while active (default 1), `rest` the command otherwise (default 0). Physics events switch actuators too: `{ "actuator": "belt", "event": "machine:stall", "match": { "element": "winch" }, "as": "off" }`.

**What comes back.** Events: `actuator:start`, `stop`, `done`, `stroke`, `stall`, `switch`, and `actuator:overload { overloaded }` when a motor sits at its torque limit; state for a UI from `actuator.state`. **Animation sync, both ways:** an `animate` part writes a number of the actuator (`from`: `phase`, `value`, `power`, `speed`, `command`, `work`; times `scale`) to a parameter of the animation graph of the named entity every step, or fires a trigger of the graph when the actuator starts, stops, is done, strokes or stalls. The other way round, the animation drives the profile through its notifies: an `anim:notify` event binding (as above) turns the force on while the blade is in the water in the animation, so picture and push never disagree. The entity needs an `Animator` (or any component with a `graph`).

**AI.** `setCommand(world, actuator, value)`, `fireActuator` and `switchActuator` in `@ferret/ai` are behaviour-tree leaves: a rower on a galley or a horse at a capstan sets the same command as the player does.

Bindings read the `input` service (`down`, `pressed`, `axis`) once per fixed step; a binding with a missing service or actuator reports its error through `cap.bindings()` and leaves the rest running. Which button does which action is the game's call.

## Not yet

Bindings run on every peer; owner-only execution and command forwarding come with A8, the motion language with A4 and A5.

## The stroke of an oar

A `blade` actuator with `oar: { pivot, reach, axis, arc: [catch, finish], period, drive: 0.4, lift: 0.25, feather: true }` carries a plate along an oar's path (body frame, metres and radians): the blade is at the end of `reach` from the oarlock `pivot`, turned about `axis` from `arc[0]` to `arc[1]` in the drive and back in the recovery. In the recovery it is lifted out of the water along the axis (within a quarter of the recovery) and turned edge-on. The force is `plateForce` of the flow against the blade's own velocity, in the medium *at the blade's place*, so it pulls in the water, slips in the air, and pulls less against a current. `command` 0 stops the stroke. `strokePose(oar, time)` is the pure path, for tools and tests.

## Over the net (A8)

`netActuators(cap, net, { now, delay, owns })` makes actuators safe on several machines. Forces, torques, motors and `jammable` spins act only where `owns(def)` is true (the owner of the body; the others receive the body by replication). `unstoppable` and `switchable` spins are functions of the shared clock and run on every machine. `command`, `on`, `off` and `fire` from the returned object go to everyone with an effect time `now() + delay`; each machine applies them when its clock reaches that time, so a switch happens at one moment everywhere. Call `step()` every fixed step. Messages that are not well formed are ignored.

## Inspector and editor (A9)

`actuator.arrows` gives one entry per running actuator: its force arrow at the point where it acts (`length` by magnitude, `colour` by kind), torque, phase, power, work, and for a spin the speed, angle and stall. `actuator.profile {profile, from, to, samples}` samples a profile for the editor's curve preview. `motion.eval` is the REPL.

## From Unreal (A9)

`tools/ue-export/physics-dynamics.mjs`: `PhysicsThrusterComponent` becomes a `force` actuator (kg cm/s2 / 100 = N), `RotatingMovementComponent` a `spin unstoppable` (axis from pitch/yaw/roll, rate in rad/s), `WindDirectionalSource` a `uniform` flow source (speed x strength). A radial force, a point wind source and a pivot translation have no equivalent and are listed in `report.unsupportedDynamics`.
