Files
kiln/docs/script-api.md
T
2026-06-09 22:38:17 -05:00

7.6 KiB
Raw Blame History

Kiln Script API Reference

Scripts are written in Rhai and live in the [scripts] section of a .toml map file. Each scripted object references a script by name via script_name. A script may define up to three lifecycle hooks; any or all may be omitted.


Lifecycle hooks

fn init()

Called once for every scripted object after the entire map is loaded, before the first frame. Use it to set up initial tile appearance, log a startup message, etc.

fn init() {
    set_tile(42);
    log("ready");
}

fn tick(dt)

Called every frame. dt is the elapsed time since the last tick, in seconds (a f64). Movement and other timed actions pace themselves through the queue; you do not need to track time manually for simple walking patterns.

fn tick(dt) {
    if Queue.length() == 0 && !blocked(North) {
        move(North);
    }
}

fn bump(id)

Called when another entity walks into this object's cell.

  • id is the bumper's ObjectId (an i64).
  • id == -1 means the player bumped this object.
fn bump(id) {
    if id == -1 {
        say("Hey! Watch it.");
    } else {
        log(`object ${id} bumped me`);
    }
}

Constants in scope

These are pre-set in every object's scope and cannot be reassigned.

Name Type Description
Board Board Read-only handle to the game world.
Queue Queue Handle to this object's own pending-action queue.
North Direction Cardinal direction: up (y 1).
South Direction Cardinal direction: down (y + 1).
East Direction Cardinal direction: right (x + 1).
West Direction Cardinal direction: left (x 1).
MY_ID i64 This object's stable ObjectId. Use with set_tag.

Read functions

Board getters

Access via Board.<property>:

Property Type Description
Board.player_x i64 Player's current column (0-indexed).
Board.player_y i64 Player's current row (0-indexed).
Board.width i64 Board width in cells.
Board.height i64 Board height in cells.
let dist_x = (Board.player_x - 30).abs();

blocked(dir) -> bool

Returns true if moving in dir from this object's current cell would be impossible — because the target cell is out of bounds, holds a solid, or another object has already queued a move there.

Note: the object's own queued moves are not yet on the board queue when blocked is called, so an object cannot accidentally block itself.

fn tick(dt) {
    if !blocked(East) { move(East); }
}

has_tag(tag) -> bool

Returns true if this object has the named tag.

if has_tag("enemy") { say("I'm an enemy!"); }

get_tags() -> Array

Returns an array of strings listing every tag on this object.

for t in get_tags() { log(t); }

objects_with_tag(tag) -> Array

Returns an array of i64 object IDs for every object on the board that currently carries tag.

let enemies = objects_with_tag("enemy");
log(`there are ${enemies.len()} enemies`);

my_name() -> String

Returns this object's name string, or "" if the object has no name.

log(`I am ${my_name()}`);

object_id_for_name(name) -> i64

Looks up an object by its name and returns its ObjectId. Returns 0 if no object has that name. 0 is never a valid id, so it can be used as a null check.

let door_id = object_id_for_name("door");
if door_id != 0 {
    set_tag(door_id, "open", true);
}

Write functions

Scripts do not mutate the world directly. Each write function appends an action to this object's output queue. Actions are drained to the shared board queue between script calls and resolved by the engine after the batch.

move(dir)

Enqueues a move one cell in dir. Also inserts a Delay of 0.25 s after the move, limiting this object to ~4 moves per second regardless of frame rate. A blocked move still costs the cooldown.

fn tick(dt) {
    if !blocked(South) { move(South); }
}

delay(secs)

Inserts an explicit pause of secs seconds into this object's queue. Adjacent delays are merged (you never get two consecutive Delay entries). Use it to slow down an action sequence or pause between speech bubbles.

fn init() {
    say("One…");
    delay(2.0);
    say("Two!");
    now();   // promote the last say() past the delay
}

now()

Moves the most recently enqueued action to the front of the queue, bypassing any pending delays. Useful after a zero-cost action (like say or log) to make it visible immediately even if a move delay is in progress.

fn bump(id) {
    say("Ouch!");
    now();   // show the bubble right away
}

set_tile(n)

Sets this object's glyph tile index to n (an integer). The tile index corresponds to a CP437 code point when using the default kiln font.

fn init() {
    set_tile(1);   // ☺
}

log(msg)

Appends a plain-text line to the game log. The message is a string.

log(`player is at ${Board.player_x}, ${Board.player_y}`);

say(msg)

Displays a speech bubble above this object containing msg. The bubble lasts 3 seconds. If the object already has an active bubble, it is replaced (text and timer reset). Zero-cost; combine with now() to surface it immediately from a bump or init hook.

fn bump(id) {
    say("Hey!");
    now();
}

set_tag(target_id, tag, present)

Adds (present = true) or removes (present = false) a tag on any object identified by target_id. Use MY_ID to mutate the calling object's own tags.

// Add a tag to self
set_tag(MY_ID, "activated", true);

// Remove a tag from another object by name
let lever_id = object_id_for_name("lever");
set_tag(lever_id, "pulled", true);

Queue object

Accessed via the Queue constant in scope. Provides inspection and control of this object's own pending-action queue.

Method Returns Description
Queue.length() i64 Number of actions currently in the queue.
Queue.clear() Discard all pending actions.

Use Queue.length() == 0 in tick to avoid queuing new moves while a previous one is in flight:

fn tick(dt) {
    if Queue.length() == 0 {
        if !blocked(East) { move(East); }
        else if !blocked(West) { move(West); }
    }
}

Rate limiting and timing

  • move(dir) costs 0.25 s of queue delay (~4 moves/second max).
  • delay(secs) adds an arbitrary pause.
  • now() can bypass a delay for zero-cost actions already in the queue.
  • Queue.clear() cancels everything, including pending delays.
  • Adjacent delays are always merged: two delay(0.5) calls become one Delay(1.0) entry.

Error handling

Script errors (compile-time or runtime) are logged to the game log as plain-text lines and do not crash the game. A script that throws an exception on tick will log the error and resume normally on the next frame.


Full example

// A guard that patrols east/west and greets the player when bumped.

fn init() {
    set_tile(2);
    log(`${my_name()} standing watch`);
}

fn tick(dt) {
    if Queue.length() > 0 { return; }   // still moving

    if !blocked(East) {
        move(East);
    } else if !blocked(West) {
        move(West);
    }
    // Stuck in a corner — do nothing this tick.
}

fn bump(id) {
    if id == -1 {
        say("Halt! Who goes there?");
        now();
    } else {
        say("Watch where you're going!");
        now();
    }
}