better scripting api

This commit is contained in:
2026-06-09 22:38:17 -05:00
parent 9c854bc1b9
commit c2168b992d
8 changed files with 1199 additions and 300 deletions
+314
View File
@@ -0,0 +1,314 @@
# 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();
}
}
```