# 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.