FERRETGame engine over Babylon.js
Ferret

@ferret/motion

the motion language: S-expressions and JSON, waits that suspend across steps, par/race/states, budget, seeded random and snapshots.

39 exports · source in packages/motion/src

Classes

Module class

class Module
readonly globals: Env
readonly main: Form[]top-level forms that are not definitions: the programs of the file
constructor(readonly forms: Form[])

packages/motion/src/runtime.ts:72

MotionBridge class

class MotionBridge
module: Modulethe module that load builds; handles started from earlier versions keep theirs
constructor(readonly rt: MotionRuntime, private opts: BridgeOptions = {})
define(name: string, sig: Signature, fn: (ctx: Ctx, ...args: any[]) => unknown): thisInstant: returns a value now. Arguments typed function arrive as plain JS functions.
lasting(name: string, sig: Signature, fn: LastingFn): thisLasting: returns a waitable ({ done(), result }) or a promise; the program sleeps on it as on wait. A snapshot taken meanwhile resumes after it.
action(name: string, sig: Signature, spec: ActionSpec): thisAction: start returns { step, stop }, run every fixed step while the form is active.
canCall(name: string): booleanmay a program call this command? Editing, configuring and loading never; the rest by the allow list.
load(text: string, file?: string): ModuleRead definitions into the bridge's module (earlier ones stay).
tag(strings: TemplateStringsArray, ...values: unknown[]): MotionSourceThe tagged template: ` motion(par (stroke left) (stroke right)) `. Numbers, strings, symbols, lists and booleans are printed in.
run(program: string | MotionSource | Form | unknown, params: Record<string, unknown> = {}, opts: { owner?: boolean } = {}): ProgramStart a program from text, a tree, a template result or JSON; params are the program's parameters.
get(name: string): (...args: unknown[]) => ProgramA defmotion as a TypeScript function: get('stroke')(sym('left'), { power: 700 }) starts it and returns the handle.
parse(text: string, file?: string): Form[]
print(f: Form): string
toJson(f: Form): unknown
fromJson(j: unknown): Form
js(v: Value): unknownA language value as plain JS: symbols are their names, a lambda is a function that runs to the end (it must not wait).

packages/motion/src/bridge.ts:42

MotionError class

class MotionError extends Error

An error with the place in the source: parse errors name the file, line and column; run-time errors name the form.

constructor(message: string, readonly pos?: Pos, readonly form?: string)

packages/motion/src/reader.ts:26

MotionRuntime class

class MotionRuntime
time: number
readonly seed: number
readonly budget: number
readonly fns: Map<string, InstantFn>
readonly actions: Map<string, ActionSpec>
readonly macros: Map<string, (form: Form[]) => Form>forms a package rewrites into core forms when a file is loaded (over-angle becomes a loop of wait-until and while)
readonly programs: Map<number, Program>
readonly sigs: Map<string, Signature>signatures of functions defined from code (MotionBridge): arity and literal types are checked at load
constructor(opts: MotionOptions = {})
define(name: string, fn: InstantFn): thisa function that returns a value now: a sensor, a calculation; usable in conditions and arguments
macro(name: string, fn: (form: Form[]) => Form): thisa form rewritten into core forms at load: the rewrite sees the unexpanded arguments, and its result is expanded again
expand(f: Form): Form
action(name: string, spec: ActionSpec): thisan action that lasts while its form is active (a force, a motor)
on(event: string, fn: (payload: any) => void): () => voidevents of the runtime for a host: motion:error, motion:done, motion:stop, and whatever programs emit
emitHost(event: string, payload: unknown)
load(source: string | Form[] | unknown, file?: string): ModuleRead a file of definitions (defmotion, defn) and programs; every call and its arguments are checked here, not in the middle of a game.
validate(mod: Module, program: string | Form | unknown, params: string[] = []): voidcheck a program against a module without starting it (names, arity, keywords, states): throws with file:line:col
run(mod: Module, program: string | Form | unknown, params: Record<string, Value> = {}, opts: { owner?: boolean } = {}): ProgramStart a program: a form of the module (or text, or JSON) with parameters; it runs from the next tick.
spawn(p: Program, parent: Thread | null, ctl: Ctl): Thread
kill(p: Program, th: Thread): void
tick(dt: number): voidOne fixed step for every program.
deliverTo(p: Program, name: string, payload: Value): void
deliver(name: string, payload: Value = null, only?: Program): voidAn event from the outside (a game, the physics): the threads that wait for it see it on the next step.
evalInstant(form: Form, env: Env, ctx: Ctx): Value
snapshot(p: Program): unknown
restore(snap: any): Program
internals()
nextProgramId(): number
setNextProgramId(n: number)
makeProgram(mod: Module, root: Form, params: Record<string, Value>, id: number, seed: number): Program
statesSpecOf(f: Form[])
callClosure(c: Closure, args: Value[]): ValueCall a lambda or a defn from code, to the end (it must not wait); the arguments are plain values.
get isClosure()
get isMotionVal()

packages/motion/src/runtime.ts:164

Program class

class Program

A running program: the handle a host or a game holds.

status: ProgramStatus
error?: MotionError
time: number
params: Record<string, Value>
bb: Record<string, Value>
triggers: Set<string>
rng: number
threads: Thread[]
nextThread: number
left: number
readonly done: Promise<Value>resolves with the program's value when it ends, or rejects with its error
result: Value
constructor(readonly rt: MotionRuntime, readonly id: number, readonly module: Module, readonly root: Form, params: Record<string, Value>, seed: number)
stop(): void
pause(): void
resume(): void
set(param: string, value: Value): void
trigger(name: string): void
on(event: string, fn: (payload: Value) => void): () => void
state()what each thread waits for, and the params, for the inspector and motion.state
snapshot()
emitLocal(name: string, payload: Value)
killAll()
fail(e: unknown)
end(v: Value)

packages/motion/src/runtime.ts:88

Sym class

class Sym

The reader of the motion language: S-expressions in text, and the same tree as a JSON array. No Babylon, no physics.

A form is a number, true / false, null (nil), a string, a symbol (Sym), a list (a JS array) or a condition object (the animation graph's { param, op, value } vocabulary, only in JSON or written inline as {...}). In JSON a symbol or keyword is a plain string ("oarlock-left", ":max-torque"), a string literal is { "s": "text" }, so (force oarlock-left forward 600) is ["force", "oarlock-left", "forward", 600] and (emit "stroke" side) is ["emit", {"s": "stroke"}, "side"].

static of(name: string): Sym
get isKeyword(): boolean
toString()

packages/motion/src/reader.ts:9

Thread class

class Thread
stack: Frame[]
ctl: Ctl
wait: Wait | null
status: Status
value: Value
kids: Thread[]
daemons: Array<{ thread: Thread; depth: number }>
stateT0: number
constructor(readonly id: number, readonly parent: Thread | null)

packages/motion/src/runtime.ts:58

Functions

brief function

function brief(f: unknown, max = 80): string

A short text of a form for error messages.

packages/motion/src/reader.ts:151

fromJson function

function fromJson(j: unknown): Form

Back from JSON; file names the source for errors (a JSON program has no lines).

packages/motion/src/reader.ts:137

netPrograms function

function netPrograms(rt: MotionRuntime, net: MotionNetLike, onAdopt: (name: string, program: Program, from: string) => void): MotionHandover

packages/motion/src/net.ts:18

parse function

function parse(src: string, file?: string): Form[]

Read the text of a program: a list of top-level forms.

packages/motion/src/reader.ts:52

parseOne function

function parseOne(src: string, file?: string): Form

Read exactly one form.

packages/motion/src/reader.ts:111

posOf function

function posOf: (f: unknown) => Pos | undefined

packages/motion/src/reader.ts:23

print function

function print(f: Form): string

packages/motion/src/reader.ts:117

toJson function

function toJson(f: Form): unknown

The JSON array form of a tree (one to one with the text).

packages/motion/src/reader.ts:129

Interfaces

ActionSpec interface

interface ActionSpec
start(ctx: Ctx, args: Value[]): HoldHandle | voidcalled once when an action form starts; the handle's step runs every step with the arguments evaluated again

packages/motion/src/runtime.ts:31

BridgeOptions interface

interface BridgeOptions
commands?: CommandRunner
allow?: string[]exact names or prefixes ending in "." that programs may call through (cmd ...) (default DEFAULT_ALLOW)

packages/motion/src/bridge.ts:20

Closure interface

interface Closure
$: 'fn'
form: Form[]
env: Env

packages/motion/src/runtime.ts:16

CommandRunner interface

interface CommandRunner
execute(name: string, args?: unknown): Promise<unknown> | unknown

packages/motion/src/bridge.ts:13

Ctx interface

interface Ctx
rt: MotionRuntime
program: Program
thread: Thread
env: Env
age: numberseconds since the action that is being read started (the clock of half-sine and the like); 0 outside an action
scale: number0..1 while a ramp is rising, else 1
dt: number

packages/motion/src/runtime.ts:21

Env interface

interface Env
vars: Map<string, Value>
parent: Env | null

packages/motion/src/runtime.ts:15

HoldHandle interface

interface HoldHandle
step?(args: Value[], ctx: Ctx): void
stop?(): void

packages/motion/src/runtime.ts:30

MotionHandover interface

interface MotionHandover
handOver(name: string, program: Program, peer: string): voidstop program here and send its snapshot to peer, who runs it on from the same point
dispose(): void

packages/motion/src/net.ts:13

MotionNetLike interface

interface MotionNetLike

Programs over the net. A program that acts on a body runs only on the machine that owns the body. When the ownership moves (a player takes the wheel, the owner leaves), the old owner hands the program over as a snapshot and the new owner continues it exactly where it was, as long as the same actions are defined there. Duck-typed on NetService.

readonly id: string
send(name: string, data: unknown): void
on(name: string, fn: (data: any, from: string) => void): () => void

packages/motion/src/net.ts:8

MotionOptions interface

interface MotionOptions
seed?: numberthe world seed: with the program id it seeds random
budget?: numberreductions per thread and step before a loop that never waits is stopped (default 10000)

packages/motion/src/runtime.ts:157

MotionSource interface

interface MotionSource

A program written in code: the text and the tree.

readonly text: string
readonly form: Form

packages/motion/src/bridge.ts:27

MotionVal interface

interface MotionVal
$: 'motion'
form: Form[]
env: Env

packages/motion/src/runtime.ts:17

Pos interface

interface Pos
line: number
col: number
file?: string

packages/motion/src/reader.ts:21

Signature interface

interface Signature
args: SigType[]argument types; the first min are required (default all)
min?: number
rest?: SigTypeevery argument after args has this type
returns?: SigType
where?: 'everywhere' | 'owner'owner: it reads something local (input, the camera, a UI); a program that is not owner-run may not use it

packages/motion/src/runtime.ts:147

Waitable interface

interface Waitable

Code and the language, both ways. Functions from code come in three kinds: instant (a value now, usable in conditions), lasting (a waitable the program sleeps on, like wait) and action (step every fixed step while its form is active, stop when it ends). Every function declares a signature, checked when the program loads and when it is called, and where it may run (everywhere or owner). Programs come back the other way as handles and as TypeScript functions, a tagged template writes them inline, and a lambda handed to a function arrives as a plain JS function.

done(): boolean
result?: unknown

packages/motion/src/bridge.ts:11

Types

CondObj type

type CondObj = { [k: string]: unknown }

packages/motion/src/reader.ts:18

Form type

type Form = number | boolean | null | string | Sym | Form[] | CondObj

packages/motion/src/reader.ts:19

InstantFn type

type InstantFn = (ctx: Ctx, ...args: Value[]) => Value

packages/motion/src/runtime.ts:29

LastingFn type

type LastingFn = (ctx: Ctx, ...args: any[]) => Waitable | Promise<unknown>

packages/motion/src/bridge.ts:12

ProgramStatus type

type ProgramStatus = 'running' | 'paused' | 'done' | 'error' | 'stopped'

packages/motion/src/runtime.ts:70

SigType type

type SigType = 'number' | 'boolean' | 'string' | 'symbol' | 'vec3' | 'list' | 'function' | 'any'

Types a function signature can name (checked when the program loads for literals, and when the function is called for values).

packages/motion/src/runtime.ts:146

Value type

type Value = number | boolean | null | string | Sym | Value[] | Closure | MotionVal

The evaluator of the motion language. A program runs for many fixed steps, so it is a stack machine of plain data, not a recursive interpreter:

  - a *thread* has a stack of frames (`seq`, `repeat`, `loop`) and at most one wait (time, a condition, an event, a hold, its children);
  - `par`, `race`, `while`, `guard`, `for`, `on` and `states` start child threads, so killing a form kills what it started;
  - an action (`force` ...) is a *hold*: the thread waits on it for ever, its arguments are read again every step, and when something kills
    the thread (`race`, `for`, `while`, a guard) the action stops;
  - conditions, arguments, `defn` and lambdas are evaluated *instantly* by a recursive evaluator: they cannot wait;
  - one budget of reductions per thread and step stops a loop that never waits; the clock is the runtime's own (`tick(dt)`), the random
    numbers are seeded: the same program and the same inputs give the same run, and a snapshot continues it exactly.

packages/motion/src/runtime.ts:14

Constants

DEFAULT_ALLOW const

const DEFAULT_ALLOW: string[]

Commands a level program may call: run-time commands of the physics packages; the game can replace this list.

packages/motion/src/bridge.ts:16

sym const

const sym: (name: string) => Sym

packages/motion/src/reader.ts:16