# Kiln Script API Reference Scripts are written in [Rhai](https://rhai.rs) 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. ```rhai 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. ```rhai 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. ```rhai 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 | 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. | ```rhai 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. ```rhai fn tick(dt) { if !blocked(East) { move(East); } } ``` ### `has_tag(tag) -> bool` Returns `true` if this object has the named tag. ```rhai if has_tag("enemy") { say("I'm an enemy!"); } ``` ### `get_tags() -> Array` Returns an array of strings listing every tag on this object. ```rhai 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`. ```rhai 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. ```rhai 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. ```rhai 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. ```rhai 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. ```rhai 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. ```rhai 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. ```rhai fn init() { set_tile(1); // ☺ } ``` ### `log(msg)` Appends a plain-text line to the game log. The message is a string. ```rhai 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. ```rhai 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. ```rhai // 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: ```rhai 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 ```rhai // 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(); } } ```