The motion language
@ferret/motion (stage A4) is a small Lisp for things that happen over time: a stroke of an oar, a gust, a door that opens and closes. It has no Babylon and no physics; hosts add the words that touch the world (force, a sensor) with define and action. Programs run across fixed steps: wait suspends, nothing blocks.
Text and JSON are one tree#
(defmotion stroke (side :power 600) ; positional, then keyword pairs with defaults
(par (for 0.4s (force side forward power))
(seq (wait 0.4s) (emit "stroke" side))))
The JSON form is the same tree: a list is an array, a symbol is a plain string, a string literal is { "s": "text" }: ["emit", {"s": "stroke"}, "side"]. Condition objects of the animation graph ({ "param": "row", "op": ">", "value": 0 }, { "trigger": "abort" }, { "stateTime": 0.4 }, { "finished": true }) are allowed wherever a condition is.
Units are folded in when reading: 30rpm, 90deg, 0.4s, 250ms, 600N, 2m, 5kg, 3W become numbers in SI (radians, seconds). Names are kebab-case. ; starts a comment.
Forms#
| Group | Forms |
|---|---|
| Time | (wait t), (wait-until cond), (wait-event name [filter]) (the payload is event) |
| Flow | seq, if, when, unless, cond, let, set!, loop, (repeat n …), (for t …), (while cond …), (guard cond …) |
| Together | (par a b …) waits for all, (race a b …) takes the first and kills the rest |
| Events | (on name …) handles every event while the enclosing form lives; (emit "name" payload) |
| States | (states :initial a (a body… [:until cond] -> b) (* :when cond -> c)); state-time counts from entering the state |
| Functions | (defmotion name (params…) …) lasts (may wait); (defn name (params…) …) and (lambda …) are instant calculations |
| Maths | + - * / mod min max abs sin cos sqrt floor clamp lerp, comparisons, and or not, vec +v -v *v dot cross len norm, random, curve |
| Data | (param name), (bb name), (bb-set! name v), time |
An action (force and the like, registered by the host) is a hold: the thread stays on it, its arguments are evaluated again every step, and it stops when the enclosing for, while, guard, race ends. (ramp t (action …)) fades a hold in over t. half-sine, periodic and curve read the age of the action they are in.
A symbol with no binding is a name: forward, oarlock-left.
Running#
const rt = new MotionRuntime({ seed: world.seed, budget: 10000 });
rt.define('log', (ctx, ...a) => …); // an instant word
rt.action('force', { start: (ctx, args) => ({ step: (a, c) => …, stop: () => … }) });
const mod = rt.load(text, 'rower.motion'); // checked now: names, arity, keywords, states
const p = rt.run(mod, '(stroke left)', { power: 400 }); // or mod.main[0]; JSON works too
rt.tick(1 / 60); // every fixed step
p.set('row', 1); p.trigger('abort'); rt.deliver('ping', 3);
p.pause(); p.resume(); p.stop(); await p.done;
load rejects unknown functions, wrong argument counts, unknown keywords and missing states with file:line:col:. Run-time errors (a number expected, a loop that never waits) stop that program, emit motion:error on the runtime, reject p.done and name the form.
Budget, determinism, snapshots#
- Budget: each thread may do
budgetreductions (default 10 000) per step; a loop with nowaitstops with an error naming the form. - Determinism: no wall clock;
randomis seeded from the world seed and the program id, and its state is part of the program. - Snapshot:
p.snapshot()is plain JSON (stack, waits, environments, random state).rt.restore(json)continues exactly as an uninterrupted run would; holds are started again in the restoring runtime.
Physics words (stage A5)#
motionPhysics(actuators) from @ferret/physics-actuators is the capability motion (phase field, before the actuators, requires actuators). It owns a MotionRuntime, ticks it once per fixed step and adds the words that touch the world.
| Kind | Words |
|---|---|
| Actions (last while their form does) | (force point dir magnitude), (torque axis magnitude), (motor axis speed :max-torque t :gain g), (flow-ramp source seconds value); :on Node picks another body |
| Instants | (impulse point dir magnitude), (command actuator v), actuator-on, actuator-off, (fire actuator), flow-on, flow-off, (flow-source name :key v …), (flow-move name (list (vec …) …) speed), list, (world x y z) |
| Sensors | speed, speed-along, angular-speed, position, height, tilt, rpm, angle, in-angle, in-zone, in-medium, flow-at, apparent-wind, wind-speed, wind-dir, power, work, stalled, phase |
| Macro | (over-angle shaft (from a b [axis]) body…): the body runs while the shaft is between the angles, at any speed and either way |
A point is a vector in the body's frame or a node name (oarlock-left finds Oarlock_L, ignoring case, - and _; addPoint(name, node) maps other names). A direction is forward (+z), back, up, down, right, left, a vector in the body's frame, or (world x y z).
A program has a target (the node it pushes): motion.run { target } or (bind "Boat" program) in a file. A level-wide program (weather) has none and may use only the flow words; a body word without a target stops that program with "no target body".
Events: actuator:*, machine:stall, zone:enter, zone:exit, interact:use reach wait-event (the payload is the actuator or zone name); emit, motion:done, motion:stop, motion:error come out as world events motion:<name>.
Commands: motion.run, motion.stop, motion.pause, motion.resume, motion.set, motion.trigger, motion.state, motion.eval (run to the first wait and return the value); edit.motion.set { name, source } (text or JSON; a file with an error is refused whole and the old one stays), edit.motion.remove, edit.motion.list. A level's physics.motions is a list of { name?, source } (the loader that reads the file hands the text over). The programs of a file that are not definitions start when it is set and stop when it is replaced or removed.
Not yet#
Machine and vessel words (throttle, cutoff, ignition, pitch, set-surface, pressure) register from their packages as those get their controls (engines: A10); touching and contact-force need the contacts in the field phase; depth; the RunMotion leaf of the behaviour tree. Code both ways is A6, strokes A7, networking A8.
Code and the language, both ways#
MotionBridge (in @ferret/motion) joins a runtime to code.
const br = new MotionBridge(rt, { commands: world.commands });
br.define('stamina', { args: [], returns: 'number' }, ctx => stats.stamina); // instant: usable in conditions
br.define('pressed', { args: ['symbol'], returns: 'boolean', where: 'owner' }, (c, a) => input.pressed(a)); // owner only
br.lasting('walk-to', { args: ['vec3'] }, (ctx, p) => agent.moveTo(p)); // a {done(), result} or a promise
br.action('vibrate', { args: ['number'] }, { start: (c, [s]) => ({ step() { pad.rumble(s); }, stop() { pad.rumble(0); } }) });
br.load('(defmotion stroke (side :power 600) ...)');
const h = br.run(br.tag`(par (stroke left) (stroke right))`, { power: 600 }); // a handle: stop, pause, resume, set, trigger, on, state, done
br.get('stroke')(sym('left'), { power: 700 }); // a defmotion as a TypeScript function
- A signature (
args,min,rest,returns,where) is checked when the program loads (argument count, literal types) and when the function is called (values). An argument typedfunctionarrives as a plain JS function: a(lambda ...)run to the end (it must not wait). - A lasting call is a statement (like
wait): the program sleeps until the waitable is done; the value is the statement's. A snapshot taken while it runs resumes after it. where: 'owner'marks a function that reads something local;br.run(..., {}, { owner: false })refuses a program that uses one.(cmd "name" :key value ...)calls a registered command. A program may call only the allow list (actuator.,flow.,rotor.,vessel.,motion.,gravity.,wind.,body.by default; the game can replace it) and neveredit.*,physics.configure/enable/disable,pkg.*orcatalog.*.br.parse,br.print,br.toJson,br.fromJsongive programs as data for tools.
Handing a program to another machine (A8)#
netPrograms(rt, net, onAdopt).handOver(name, program, peer) stops the program here and sends its snapshot; the peer restores it and continues exactly where it stopped (the same actions must be defined there).