Files
dusk/docs/SCRIPTING.md
2026-07-12 10:10:05 -05:00

380 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scripting
Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to
drive gameplay logic from JavaScript. The engine itself (rendering, physics,
asset loading, entity storage) is all C; scripts sit on top and manipulate that
state through a small set of bound objects.
This document covers the JS-facing scripting API. **UI (buttons, sliders,
menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See
[UI.md](UI.md) if you're building screens/menus from engine/game C code.
## Lifecycle
On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main
script, then calls the global `init()` function if one is defined. From then
on, every engine tick calls (in order):
1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that
must be deterministic and independent of display refresh rate (movement,
physics-adjacent input handling, etc). Skipped on interpolation/dynamic
frames when the build has variable-timestep rendering enabled.
2. `update()` — once per rendered frame, including interpolation frames. Use
this for smooth, purely presentational animation (nothing that needs to be
deterministic).
On shutdown, `deinit()` is called once.
All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional**
if a script doesn't define one, the engine simply skips it, no error.
```js
function update() {
cubePosition.rotation.y += TIME.delta * 1.5;
}
```
### `async init()` and `include()`
`init` (or any of the other hooks) can be declared `async` and use `await`
freely, including awaiting `include()` (see below), even though the engine
calls these functions synchronously from C with no external JS event loop.
When a hook returns a pending `Promise`, the engine keeps driving the asset
system and JerryScript's job queue until that promise settles before
continuing — so by the time e.g. `init()` "returns" from the engine's point of
view, everything it awaited has actually finished.
```js
async function init() {
Actions = await include("input.js");
// ...
}
```
If the awaited work throws/rejects, it surfaces as a C-level error.
## Loading other scripts: `include(path)`
```js
var Actions = await include("input.js");
```
`include(path)` always returns a `Promise`. The named file is loaded and
evaluated once no matter how many times (or from how many different scripts)
you `include()` it — later calls for the same path are handed the same
in-flight/resolved promise rather than re-running the file.
The included script communicates its result back by assigning to the bare
global `module`:
```js
// input.js
Input.bind("w", INPUT_ACTION_UP);
// ...
module = {
UP: INPUT_ACTION_UP,
DOWN: INPUT_ACTION_DOWN,
// ...
};
```
Whatever `input.js` assigns to `module` becomes the resolved value of the
promise `include("input.js")` returned — that's what `Actions` ends up being
in the example above. If the included script throws, the promise rejects
instead.
## Full example
This is the actual shipped example content (`assets/engine.js` +
`assets/input.js`):
```js
// input.js — binds physical buttons to abstract actions, then exports the
// action constants so other scripts don't need to know raw INPUT_ACTION_* names.
Input.bind("w", INPUT_ACTION_UP);
Input.bind("s", INPUT_ACTION_DOWN);
Input.bind("a", INPUT_ACTION_LEFT);
Input.bind("d", INPUT_ACTION_RIGHT);
Input.bind("space", INPUT_ACTION_ACCEPT);
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
if(typeof INPUT_GAMEPAD !== "undefined") {
Input.bind("gamepad_up", INPUT_ACTION_UP);
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
}
module = {
UP: INPUT_ACTION_UP,
DOWN: INPUT_ACTION_DOWN,
LEFT: INPUT_ACTION_LEFT,
RIGHT: INPUT_ACTION_RIGHT,
ACCEPT: INPUT_ACTION_ACCEPT,
CANCEL: INPUT_ACTION_CANCEL,
RAGEQUIT: INPUT_ACTION_RAGEQUIT
};
```
```js
// engine.js
var Actions;
var camera, cameraPosition;
var cube, cubePosition, cubeRenderable, cubeMesh;
async function init() {
Actions = await include("input.js");
camera = new Entity();
cameraPosition = camera.add(POSITION);
camera.add(CAMERA);
cameraPosition.position = new Vec3(3, 3, -6);
cameraPosition.lookAt(new Vec3(0, 0, 0));
cube = new Entity();
cubePosition = cube.add(POSITION);
cubeRenderable = cube.add(RENDERABLE);
cubeMesh = Mesh.createCube();
cubeRenderable.mesh = cubeMesh;
cubeRenderable.color = Color.red();
}
function update() {
cubePosition.rotation.y += TIME.delta * 1.5;
cubePosition.rotation.x += TIME.delta * 0.7;
}
function fixedUpdate() {
var move = 3.0 * TIME.delta;
if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move;
if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move;
if(Input.isDown(Actions.UP)) cubePosition.position.z += move;
if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move;
if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0);
}
function deinit() {
cube.dispose();
camera.dispose();
}
```
## API reference
### `TIME`
Plain global object, live getters (read fresh engine state every access, not
snapshotted):
| Property | Type | Description |
|---|---|---|
| `TIME.delta` | number | Seconds since the last frame. |
| `TIME.time` | number | Total elapsed engine time, in seconds. |
### `PLATFORM`
A single global string constant — the compile-time target name, e.g.
`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may
inject additional platform-specific globals via their own
`modulePlatformPlatform()` hook; those aren't documented here since they vary
per target.
### `Input`
Static namespace (not constructible — there's no `new Input()`).
| Method | Description |
|---|---|
| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. |
| `Input.isDown(action)` → boolean | Is the action currently held. |
| `Input.pressed(action)` → boolean | Action transitioned to down this frame. |
| `Input.released(action)` → boolean | Action transitioned to up this frame. |
| `Input.getValue(action)` → number | Current analog value for the action. |
| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. |
| `Input.axis2D(negX, posX, negY, posY)``Vec2` | Combined 2D axis from four actions. |
Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric
values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`,
`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`,
`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`,
`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`.
Conditionally-defined boolean globals reflecting build capability — only
present at all if the corresponding input method is compiled in, so
feature-test with `typeof`, don't assume they exist:
`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`.
### `Vec2` / `Vec3` / `Vec4`
`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all
components optional, default `0`.
Common instance surface across all three: `.dot(other)`, `.length()`,
`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`,
`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/
`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally
has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too;
`Vec4` has neither `.cross()` nor `.distance()`.
`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0`
(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code.
All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()`
like `"Vec3(1, 2, 3)"`.
**"Vec3Ref" — live references.** Several engine properties (entity
`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`)
return a vector-*like* object instead of a plain `Vec3`. It has the identical
`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying
native buffer — writing `.x` on `entity.position.position` immediately moves
the entity, no separate assignment needed. Anywhere the API expects a `Vec3`
argument, a Vec3Ref works too. You never construct one directly; you only
ever receive them from properties like the ones above.
### `Mat4`
`new Mat4()` — always constructs identity; no other constructor form.
| Member | Description |
|---|---|
| `.mul(other)``Mat4` | `this * other`. |
| `.transpose()``Mat4` | |
| `.inverse()``Mat4` | |
| `.determinant()` → number | |
| `.mulVec3(vec3, w?)``Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. |
| `.mulVec4(vec4)``Vec4` | |
| `.translate(vec3)``Mat4` | Non-mutating — returns a translated copy. |
| `.scale(vec3)``Mat4` | Non-mutating — returns a scaled copy. |
| `Mat4.identity()``Mat4` | Static. |
| `Mat4.perspective(fov, aspect, near, far)``Mat4` | Static, all 4 args required. |
| `Mat4.lookAt(eye, center, up)``Mat4` | Static, all 3 args required `Vec3`s. |
### `Color`
`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so
`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set.
Named factories, each a zero-arg static returning a new opaque `Color`
(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`,
`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`,
`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()`
(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`,
`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`,
`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`,
`Color.teal()`, `Color.cornflower_blue()`.
`Color.rainbow(t?, speed?)``Color``t` defaults to `TIME.time * 4.0`;
produces a shifting rainbow color, useful for debug visuals.
### `Mesh`
`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer
(not yet uploaded to the GPU).
| Member | Description |
|---|---|
| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). |
| `.vertexCount` | Read-only. |
| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. |
| `.dispose()` | Frees GPU + CPU resources. |
Static engine-owned singletons (read-only, not something you dispose):
`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`,
`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`.
Static factories (each builds and uploads a brand-new `Mesh`):
| Factory | Notes |
|---|---|
| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. |
| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0``1,1`. |
| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. |
| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. |
| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. |
| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. |
### `Entity` and components
```js
var e = new Entity();
var pos = e.add(POSITION);
```
`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID.
`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the
constants below). `.dispose()` removes the entity and all its components.
Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`,
`TRIGGER`. Each entity also exposes a lowercase getter that returns the
existing wrapper if the component is present, or `undefined` if not (it does
**not** add the component — use `.add()` for that): `entity.position`,
`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`.
#### `entity.add(POSITION)` → position component
| Member | Description |
|---|---|
| `.position` | Vec3Ref. Writing rebuilds the transform automatically. |
| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. |
| `.scale` | Vec3Ref. Same rebuild-on-write behavior. |
| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. |
| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. |
#### `entity.add(CAMERA)` → camera component
| Member | Description |
|---|---|
| `.zNear` / `.zFar` | Numbers. |
| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. |
| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. |
| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. |
#### `entity.add(RENDERABLE)` → renderable component
| Member | Description |
|---|---|
| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. |
| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. |
| `.color` | Get/set a `Color` instance (throws if given something else). |
| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. |
| `.clearSprites()` | Clears the sprite batch. |
| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. |
#### `entity.add(PHYSICS)` → physics component
| Member | Description |
|---|---|
| `.velocity` | Vec3Ref, plain (no rebuild-on-write). |
| `.onGround` | Read-only boolean. |
| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. |
| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. |
| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. |
| `.setShapeSphere(radius)` | Number. |
| `.setShapeCapsule(radius, halfHeight)` | Two numbers. |
| `.setShapePlane(normal, distance)` | `Vec3` + number. |
Shape-type constants (for reading `.type` on the underlying shape, not for
`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`,
`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`.
#### `entity.add(TRIGGER)` → trigger component
| Member | Description |
|---|---|
| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). |
| `.setBounds(min, max)` | Sets both at once. |
| `.contains(point)` → boolean | `point` a `Vec3`. |
## Not yet available to scripts
The following C modules exist and are fully implemented, but aren't currently
wired into script registration (`moduleRegister()` in
`src/dusk/script/module/module.h`), so none of these globals exist in a
script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`,
`Engine`. If you need one of these from a script, it needs to be registered
in `moduleRegister()` first — see the existing entries there and the modules
under `src/dusk/script/module/` for the pattern to follow.