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

315 lines
7.6 KiB
Markdown
Raw 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.
# 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>`:
| 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();
}
}
```