Skip to main content

Scripts

Scripts are compiled to bytecode and executed by the runtime.

They are currently event-driven. They can live in external files under scripts/ or be embedded directly inside rect and sprite scene nodes.

External example:

on ready() {    log("boot")}

Inline scene example:

rect Hero {    on update(dt) {        self.x = self.x + 10.0 * dt    }}

Event handlers

Current handlers are defined as:

on ready() {    log("boot")}on update(dt) {    self.x = self.x + 10.0 * dt}

Current language features

Supported today:

  • assignments
  • numeric expressions
  • property reads and writes
  • local bindings with let
  • persistent per-entity script state with state
  • entity-local event dispatch with emit(...)
  • if / else
  • boolean conditions with &&, ||, !
  • top-level reusable functions
  • runtime query calls like input_left(), input_action(), and key("Space")
  • action builtins like play_sound(...), play_music(...), and stop_music()

Properties

Current readable/writable properties:

  • self.x
  • self.y
  • self.width
  • self.height
  • self.pos
  • self.size
  • self.rotation
  • self.color
  • self.texture for sprites
  • self.animation for named sprite animations
  • self.flip_x for sprites
  • self.flip_y for sprites
  • self.vx for platformer sprites
  • self.vy for platformer sprites
  • self.move_x for platformer sprites
  • self.jump for platformer sprites
  • self.grounded for platformer sprites
  • self.text for text nodes
  • self.some_state
  • Name.x
  • Name.y
  • Name.width
  • Name.height
  • Name.pos
  • Name.size
  • Name.rotation
  • Name.color
  • Name.texture for sprites
  • Name.animation for named sprite animations
  • Name.flip_x
  • Name.flip_y
  • Name.vx
  • Name.vy
  • Name.move_x
  • Name.jump
  • Name.grounded
  • Name.text for text nodes
  • Name.some_state

Example:

Accent.color = #7ce0ffself.pos = Mascot.posself.width = 96.0self.rotation = self.rotation + 1.6 * dt

flip_x, flip_y, jump, and grounded are represented as scalar 0 or 1 values in scripts.

Script Events

Scripts can dispatch entity-local events:

emit("motion", "run")

The same entity can receive them through a generic event handler:

on event(event, value) {    if event == "motion" {        if value == "run" {            self.animation = "run"        } else if value == "idle" {            self.animation = "idle"        }    }}

String equality and inequality are supported in conditions, so event == "motion" and value != "idle" are valid.

Platformer Input

For sprites using physics = platformer, scripts should express input intent and let the runtime handle acceleration, friction, gravity, jumping, and collision:

on update(dt) {    if input_left() {        self.move_x = -1        self.flip_x = 1    } else if input_right() {        self.move_x = 1        self.flip_x = 0    } else {        self.move_x = 0    }    if input_action() && self.grounded {        self.jump = 1    }}

The runtime updates self.vx, self.vy, and self.grounded.

Collision Events

The runtime emits entity collision events after movement and platformer physics.

collision_enter(other, group) fires once when two visible entities begin overlapping.

collision(other, group) fires every frame while two visible entities overlap.

Both events are sent to both entities. other is the other entity name and group is the other entity group, or an empty string if it has none.

on collision_enter(other, group) {    if group == "pickup" {        destroy(other)    } else if group == "hazard" && is_stomping(other) {        destroy(other)        self.vy = -185.0    } else if group == "hazard" {        self.vy = -150.0    }}

is_stomping(other) is a platformer helper for collision handlers. It returns true when the current entity is falling into the top band of other, which is useful for enemy stomp behavior.

Locals

Handler-local values can be introduced with let:

let next_x = Mascot.x - 12.0 * dtMascot.x = next_x

Locals are shared with nested if blocks and with called functions in the same handler execution.

Persistent state

Scripts can declare persistent state that survives across frames on the bound runtime entity:

state score = 0state lives = 3state invulnerable_until = 0

State values can be read as bare variables inside the same script:

score = score + 10

They can also be accessed through entity properties:

self.score = self.score + 10HudState.lives = HudState.lives - 1

Use let for temporary per-handler values and state for data that must persist between updates.

Conditions

Current condition features:

  • <
  • <=
  • >
  • >=
  • ==
  • !=
  • &&
  • ||
  • !
  • grouping with parentheses

Example:

if next_x < 120.0 || (Accent.x < 260.0 && !(self.y < 200.0)) {    Mascot.x = 520.0} else {    Accent.color = #7ce0ff}

Bare query calls are also valid conditions. They are treated as truthy when non-zero:

if input_left() {    self.x = self.x - 120.0 * dt}

Functions

Current functions are top-level, can take parameters, and can return a value:

fn accent_color(limit) {    if limit < 120.0 {        return #ff8899    } else {        return #7ce0ff    }}

Called as a statement:

call sync_accent(next_x)

Audio actions use the same call ... statement form:

call play_sound("shot.wav")call play_music("music-game.ogg")call stop_music()

play_sound(...) is intended for short one-shot effects.

play_music(...) starts looping background music and replaces any currently playing music.

stop_music() stops the current music track.

These actions work on both native and wasm/web. On the web, browsers may delay music start until the first user interaction because of autoplay rules.

Called as an expression:

Accent.color = accent_color(next_x)

Runtime queries

Current built-in runtime queries:

  • input_left()
  • input_right()
  • input_up()
  • input_down()
  • input_action()
  • key("Space")
  • exists("Name")
  • first_overlap("group")
  • is_stomping("Name")
  • high_score_name(index)
  • high_score_value(index)
  • format_int(value, digits)
  • lerp(a, b, t)
  • pulse(period)
  • smoothstep(edge0, edge1, x)
  • alpha(color, alpha)
  • time()
  • difficulty()
  • every(seconds)
  • every(min_seconds, max_seconds)
  • rand(min, max)
  • screen_width()
  • screen_height()

Example:

if input_action() {    self.color = #ffbf47}if key("Space") {    self.color = #ff8899}self.x = clamp(self.x, 80.0, screen_width() - 160.0)if every(1.2, 2.0) {    spawn("EnemyTemplate", screen_width() + 80.0, rand(48.0, screen_height() - 96.0))}

input_action() is the generic shoot/confirm/action abstraction. On desktop it currently maps to Space, Enter, Z, and X.

exists("Name") returns whether a live runtime instance with that name currently exists. This is useful for spawn gating:

if !exists("PlanetTop") && every(10.0, 14.0) {    spawn("PlanetTopTemplate", "PlanetTop", screen_width() + 96.0, -92.0)}

first_overlap("group") returns the name of the first overlapping live entity in that group, or an empty string if there is no hit. This is useful for simple projectile collisions:

let hit = first_overlap("hostile")if exists(hit) {    destroy(hit)    destroy(self)}

high_score_name(index) and high_score_value(index) read from the runtime high-score table using 1-based indices.

format_int(value, digits) formats a scalar as a zero-padded string. This is useful for HUD text:

ScoreLabel.text = format_int(score, 3)

lerp(a, b, t) linearly interpolates between two scalar values.

pulse(period) returns a repeating 0..1 pulse over the given period in seconds.

smoothstep(edge0, edge1, x) returns a smoothed 0..1 interpolation factor, useful for eased motion and fades.

alpha(color, alpha) returns the given color with a replaced alpha channel.

difficulty() is currently a simple time-based level that increases as the session runs.

every(seconds) is a per-script-line timer query that returns true when the interval elapses.

every(min_seconds, max_seconds) schedules the next trigger with a randomized interval in that range.

rand(min, max) returns a random scalar inside the given range.

Runtime instancing

Current runtime instance statements:

  • spawn("TemplateName", "InstanceName", x, y)
  • spawn("TemplateName", x, y)
  • destroy("InstanceName")
  • destroy(name_expr)
  • destroy(self)

Example:

on ready() {    spawn("EnemyTemplate", self.x + self.width, rand(self.y, self.y + self.height))}on update(dt) {    ScoutOne.x = ScoutOne.x - 180.0 * dt}

High scores

Scripts can submit scores to the runtime high-score table:

call submit_score("UNKNOWN", HudState.score)

This updates the internal table used by highscore scene nodes and the high_score_name(...) / high_score_value(...) queries.

Current limitations:

  • no closures
  • no column-precise diagnostics; warnings/errors currently report file and line

Compatibility helpers

The runtime still supports older helper-style script ops such as:

  • move_by(...)
  • move_by_dt(...)
  • set_pos(...)
  • set_color(...)
  • copy_pos(...)
  • clamp_x(...)
  • clamp_y(...)

The direction now is to prefer assignments and expressions instead of adding more one-off built-ins.