refactor
This commit is contained in:
+266
-175
@@ -1,17 +1,25 @@
|
|||||||
# Kiln Script API Reference
|
# Kiln Script API Reference
|
||||||
|
|
||||||
Scripts are written in [Rhai](https://rhai.rs) and live in the `[scripts]` section of a `.toml` map
|
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
|
file. Each scripted object references a script by name via `script_name`. A script defines lifecycle
|
||||||
three lifecycle hooks; any or all may be omitted.
|
hooks (any may be omitted) and may define arbitrary handler functions reachable via `send()` and
|
||||||
|
scroll choices.
|
||||||
|
|
||||||
|
> **Status:** this document describes the API as it currently exists. It has grown organically and
|
||||||
|
> has known rough edges — duplication, naming inconsistency, and some unwieldy machinery. Those are
|
||||||
|
> catalogued in [Design notes](#design-notes-rough-edges) at the end, as the starting point for a
|
||||||
|
> redesign.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lifecycle hooks
|
## Lifecycle hooks
|
||||||
|
|
||||||
|
Hooks are detected by name **and** arity (parameter count). A function with the right name but wrong
|
||||||
|
number of parameters is not recognized as a hook.
|
||||||
|
|
||||||
### `fn init()`
|
### `fn init()`
|
||||||
|
|
||||||
Called once for every scripted object after the entire map is loaded, before the first frame.
|
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
|
```rhai
|
||||||
fn init() {
|
fn init() {
|
||||||
@@ -22,9 +30,8 @@ fn init() {
|
|||||||
|
|
||||||
### `fn tick(dt)`
|
### `fn tick(dt)`
|
||||||
|
|
||||||
Called every frame. `dt` is the elapsed time since the last tick, in seconds (a `f64`). Movement
|
Called every frame. `dt` is the elapsed time since the last tick, in seconds (a `f64`). Timed
|
||||||
and other timed actions pace themselves through the queue; you do not need to track time manually
|
actions pace themselves through the queue; you do not track time manually for simple walking.
|
||||||
for simple walking patterns.
|
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
@@ -39,60 +46,155 @@ fn tick(dt) {
|
|||||||
Called when another entity walks into this object's cell.
|
Called when another entity walks into this object's cell.
|
||||||
|
|
||||||
- `id` is the bumper's `ObjectId` (an `i64`).
|
- `id` is the bumper's `ObjectId` (an `i64`).
|
||||||
- `id == -1` means the player bumped this object.
|
- `id == -1` means the **player** bumped this object.
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
fn bump(id) {
|
fn bump(id) {
|
||||||
if id == -1 {
|
if id == -1 { say("Hey! Watch it."); }
|
||||||
say("Hey! Watch it.");
|
else { log(`object ${id} bumped me`); }
|
||||||
} else {
|
|
||||||
log(`object ${id} bumped me`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `fn grab()`
|
||||||
|
|
||||||
|
Called when the **player walks onto** this object and the object is a *grab* thing (gems, hearts —
|
||||||
|
`solid + grab`). Unlike `bump`, the player ends up on the object's cell; the hook typically adjusts a
|
||||||
|
player stat and removes the object. It fires followed by an immediate resolve, so any `die()` /
|
||||||
|
`add_gems()` / `alter_health()` applies before the player's move returns.
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
fn grab() {
|
||||||
|
add_gems(1);
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Grab is **only** triggered by the player walking onto the object. A grab thing *pushed* or
|
||||||
|
> *shifted* into the player is treated as an ordinary solid (no `grab()`).
|
||||||
|
|
||||||
|
### Custom handler functions
|
||||||
|
|
||||||
|
Any other function can be invoked on an object two ways:
|
||||||
|
|
||||||
|
- Another object calls `send(target_id, "fn_name" [, arg])`.
|
||||||
|
- A `scroll()` **choice** is selected: the choice key is dispatched to the source object as a
|
||||||
|
zero-argument function call of that name.
|
||||||
|
|
||||||
|
A handler may take **zero or one** parameter; the host calls the 1-arg form when an arg is supplied,
|
||||||
|
else the 0-arg form. If neither arity exists, the call is silently dropped.
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// reached via send(my_id, "open") or a scroll choice keyed "open"
|
||||||
|
fn open() { set_tile(47); }
|
||||||
|
fn set_level(n){ set_tag(Me.id, "level", true); log(`level ${n}`); }
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Constants in scope
|
## Constants in scope
|
||||||
|
|
||||||
These are pre-set in every object's scope and cannot be reassigned.
|
Pre-set in every object's scope. The object handles are scope constants; direction/color constants
|
||||||
|
come from a global module visible at any call depth (including Rhai→Rhai calls).
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `Board` | `Board` | Read-only handle to the game world. |
|
| `Board` | `Board` | Read-only handle to the current board / world. |
|
||||||
| `Queue` | `Queue` | Handle to this object's own pending-action queue. |
|
| `Queue` | `Queue` | This object's own pending-action queue. |
|
||||||
| `North` | `Direction` | Cardinal direction: up (y − 1). |
|
| `Me` | `Me` | Self-reference (id, name, position, tags, glyph). |
|
||||||
| `South` | `Direction` | Cardinal direction: down (y + 1). |
|
| `Registry` | `Registry` | Board-scoped key→value store; persists across board transitions. |
|
||||||
| `East` | `Direction` | Cardinal direction: right (x + 1). |
|
| `Player` | `Player` | Read-only snapshot of the player's game-global stats. |
|
||||||
| `West` | `Direction` | Cardinal direction: left (x − 1). |
|
| `North` / `South` / `East` / `West` | `Direction` | Cardinal directions. |
|
||||||
| `MY_ID` | `i64` | This object's stable `ObjectId`. Use with `set_tag`. |
|
| `Black` … `White` (16) | `String` | EGA/VGA palette colors as `"#RRGGBB"` strings. |
|
||||||
|
|
||||||
|
The 16 color names, in palette order: `Black`, `Blue`, `Green`, `Cyan`, `Red`, `Magenta`, `Brown`,
|
||||||
|
`LightGray`, `DarkGray`, `BrightBlue`, `BrightGreen`, `BrightCyan`, `BrightRed`, `BrightMagenta`,
|
||||||
|
`Yellow`, `White`. They are plain hex strings, so they are accepted anywhere a color string is (e.g.
|
||||||
|
`set_fg(BrightRed)`).
|
||||||
|
|
||||||
|
> There is **no** `MY_ID` constant any more — use `Me.id`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Read functions
|
## Self-reference: `Me`
|
||||||
|
|
||||||
### `Board` getters
|
| Member | Kind | Returns | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `Me.id` | getter | `i64` | This object's stable `ObjectId`. |
|
||||||
|
| `Me.name` | getter | `String` | Name, or `""` if unnamed. |
|
||||||
|
| `Me.x` / `Me.y` | getter | `i64` | Current cell position. |
|
||||||
|
| `Me.has_tag(tag)` | method | `bool` | Whether this object carries `tag`. |
|
||||||
|
| `Me.tags()` | method | `Array` | All tag strings on this object. |
|
||||||
|
| `Me.glyph()` | method | `Glyph` | This object's glyph (`.tile`, `.fg`, `.bg`). |
|
||||||
|
|
||||||
Access via `Board.<property>`:
|
```rhai
|
||||||
|
if Me.has_tag("locked") { say(`${Me.name} is locked`); }
|
||||||
|
set_tag(Me.id, "seen", true); // self-mutation still needs the explicit id
|
||||||
|
```
|
||||||
|
|
||||||
|
A `Glyph` value (from `Me.glyph()` or `ObjectInfo` lookups) exposes `.tile` (`i64`), `.fg`, `.bg`
|
||||||
|
(both `"#RRGGBB"` strings).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Player stats: `Player`
|
||||||
|
|
||||||
|
A read-only snapshot of the game-global player state, refreshed before each hook call.
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
|----------|------|-------------|
|
|----------|------|-------------|
|
||||||
| `Board.player_x` | `i64` | Player's current column (0-indexed). |
|
| `Player.gems` | `i64` | Gems collected. |
|
||||||
| `Board.player_y` | `i64` | Player's current row (0-indexed). |
|
| `Player.health` | `i64` | Current health. |
|
||||||
|
| `Player.max_health` | `i64` | Health ceiling. |
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
if Player.health < 2 { say("You look hurt."); }
|
||||||
|
```
|
||||||
|
|
||||||
|
> The player's **keys** are not readable from scripts yet (you can `set_key` but not query it).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Board reads
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `Board.player_x` | `i64` | Player's column. |
|
||||||
|
| `Board.player_y` | `i64` | Player's row. |
|
||||||
| `Board.width` | `i64` | Board width in cells. |
|
| `Board.width` | `i64` | Board width in cells. |
|
||||||
| `Board.height` | `i64` | Board height in cells. |
|
| `Board.height` | `i64` | Board height in cells. |
|
||||||
|
|
||||||
|
### Object lookups
|
||||||
|
|
||||||
|
| Call | Returns | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `Board.tagged(tag)` | `Array` of `ObjectInfo` | Every object carrying `tag`. |
|
||||||
|
| `Board.named(name)` | `ObjectInfo` or `()` | The object with that name, else unit. |
|
||||||
|
| `Board.get(id)` | `ObjectInfo` or `()` | Look up by id; `-1` returns the player; an invalid/unknown id logs an error and returns unit. |
|
||||||
|
|
||||||
|
An `ObjectInfo` exposes `.id`, `.x`, `.y`, `.name` (`""` if unnamed) and `.tags` (an `Array`). Note
|
||||||
|
it has **no** `has_tag()` or `glyph()` — those exist only on `Me`.
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
let dist_x = (Board.player_x - 30).abs();
|
let enemies = Board.tagged("enemy");
|
||||||
|
log(`there are ${enemies.len()} enemies`);
|
||||||
|
|
||||||
|
let door = Board.named("door");
|
||||||
|
if door != () { send(door.id, "open"); }
|
||||||
```
|
```
|
||||||
|
|
||||||
### `blocked(dir) -> bool`
|
---
|
||||||
|
|
||||||
Returns `true` if moving in `dir` from this object's current cell would be impossible — because
|
## Cell queries (free functions)
|
||||||
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
|
| Call | Returns | Description |
|
||||||
an object cannot accidentally block itself.
|
|------|---------|-------------|
|
||||||
|
| `blocked(dir)` | `bool` | Would the **caller** be unable to move one step in `dir` (off-board, a solid, or another object's already-queued move targets that cell). The caller's own queued moves don't count. |
|
||||||
|
| `can_push(x, y, dir)` | `bool` | Does a pushable chain starting at `(x, y)` end in open space when shoved in `dir`? `false` off-board. |
|
||||||
|
| `can_shift(x, y, dir)` | `bool` | Like `can_push` but checks only the **one** cell ahead (pushable source + empty-or-pushable neighbor). The right gate for `shift()`. |
|
||||||
|
| `passable(x, y)` | `bool` | Is `(x, y)` on-board and free of any solid (an enterable hole)? Distinguishes a hole from a blocked solid. |
|
||||||
|
| `combinable(x1, y1, x2, y2)` | `bool` | Can the solids at the two cells share a cell (one empty, or a grab thing + the player)? |
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
@@ -100,157 +202,93 @@ fn tick(dt) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `has_tag(tag) -> bool`
|
---
|
||||||
|
|
||||||
Returns `true` if this object has the named tag.
|
## Registry (shared key→value store)
|
||||||
|
|
||||||
|
Board-scoped, persists across board transitions. Stores primitive values.
|
||||||
|
|
||||||
|
| Call | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `Registry.get(key)` | Stored value, or `()` if absent. |
|
||||||
|
| `Registry.set(key, value)` | Store a primitive; passing `()` removes the key; unsupported types are silently ignored. |
|
||||||
|
| `Registry.get_or(key, default)` | Stored value **only if** it has the same type as `default`; otherwise `default`. |
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
if has_tag("enemy") { say("I'm an enemy!"); }
|
let count = Registry.get_or("visits", 0);
|
||||||
```
|
Registry.set("visits", count + 1);
|
||||||
|
|
||||||
### `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
|
## Write functions
|
||||||
|
|
||||||
Scripts do not mutate the world directly. Each write function appends an action to this object's
|
Scripts never mutate the world directly. Each write appends an `Action` to this object's **output
|
||||||
**output queue**. Actions are drained to the shared board queue between script calls and resolved
|
queue**; actions drain to a shared board queue between script calls and are resolved by the engine
|
||||||
by the engine after the batch.
|
after the batch. The *subject* of a write is implicit and varies by function (see the table).
|
||||||
|
|
||||||
### `move(dir)`
|
| Call | Subject | Cost | Description |
|
||||||
|
|------|---------|------|-------------|
|
||||||
|
| `move(dir)` | self | 0.25 s | Enqueue a one-cell move, plus a rate-limiting delay (~4/s). A blocked move still costs the cooldown. |
|
||||||
|
| `teleport(x, y)` | self | 0 | Jump to an arbitrary cell. Refused (error logged at resolve) if self is solid and the destination is occupied. |
|
||||||
|
| `push(x, y, dir)` | arbitrary cell | 0 | Shove the pushable chain at `(x, y)` one step in `dir`. |
|
||||||
|
| `shift([[x, y], …])` | arbitrary cells | 0 | Rotate cell contents: each listed cell moves to the next coordinate, last→first. Skips non-pushables and won't move into a non-vacated occupied cell. |
|
||||||
|
| `set_tile(n)` | self | 0 | Set glyph tile index (CP437 code point in the default font). |
|
||||||
|
| `set_fg(color)` | self | 0 | Set foreground color (hex string). |
|
||||||
|
| `set_bg(color)` | self | 0 | Set background color. |
|
||||||
|
| `set_color(fg, bg)` | self | 0 | Set both colors. |
|
||||||
|
| `set_tag(target_id, tag, present)` | any object | 0 | Add (`true`) / remove (`false`) a tag. Use `Me.id` for self. |
|
||||||
|
| `say(msg)` / `say(msg, secs)` | self | 0 | Speech bubble above this object; default 3 s, or `secs`. Replaces any current bubble. |
|
||||||
|
| `log(msg)` | log | 0 | Append a plain-text line to the game log. |
|
||||||
|
| `scroll(lines)` | UI | 0 | Open a full-screen overlay (see below). |
|
||||||
|
| `send(target_id, fn_name [, arg])` | any object | 0 | Call a handler function on another object; optional string/number arg. |
|
||||||
|
| `delay(secs)` | self | — | Insert an explicit pause; integer and float overloads. Adjacent delays merge. |
|
||||||
|
| `now()` | self | — | Move the **most recently enqueued** action to the front of the queue. |
|
||||||
|
| `add_gems(n)` | player | 0 | Change the player's gem count (clamped at 0). |
|
||||||
|
| `alter_health(dh)` | player | 0 | Change the player's health (clamped to `[0, max_health]`). |
|
||||||
|
| `set_key(color, present)` | player | 0 | Give/take a key color (`"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, `"orange"`, `"yellow"`, `"white"`). Unknown name logs and is ignored. |
|
||||||
|
| `die()` | self | 0 | Remove the calling object from the board. |
|
||||||
|
|
||||||
Enqueues a move one cell in `dir`. Also inserts a `Delay` of 0.25 s after the move, limiting this
|
### `scroll(lines)`
|
||||||
object to ~4 moves per second regardless of frame rate. A blocked move still costs the cooldown.
|
|
||||||
|
|
||||||
```rhai
|
Opens a full-screen scrollable overlay and pauses game ticks until the player closes it. Each element
|
||||||
fn tick(dt) {
|
of `lines` is either:
|
||||||
if !blocked(South) { move(South); }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `delay(secs)`
|
- a **string** — a plain text line, or
|
||||||
|
- a **2-element array** `[choice_key, display_text]` — a selectable choice.
|
||||||
|
|
||||||
Inserts an explicit pause of `secs` seconds into this object's queue. Adjacent delays are merged
|
When the player selects a choice, `choice_key` is dispatched back to the **source object** as a
|
||||||
(you never get two consecutive `Delay` entries). Use it to slow down an action sequence or pause
|
zero-arg function call named `choice_key`.
|
||||||
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
|
```rhai
|
||||||
fn bump(id) {
|
fn bump(id) {
|
||||||
say("Ouch!");
|
scroll([
|
||||||
now(); // show the bubble right away
|
"The muffin looks delicious.",
|
||||||
|
"",
|
||||||
|
["eat", "Eat it"],
|
||||||
|
["ignore", "Walk away"],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
```
|
fn eat() { say("Yeah it was poisoned."); alter_health(-2); }
|
||||||
|
fn ignore() { log("Wise."); }
|
||||||
### `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
|
## `Queue` object
|
||||||
|
|
||||||
Accessed via the `Queue` constant in scope. Provides inspection and control of this object's own
|
Inspection and control of this object's own pending-action queue.
|
||||||
pending-action queue.
|
|
||||||
|
|
||||||
| Method | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `Queue.length()` | `i64` | Number of actions currently in the queue. |
|
| `Queue.length()` | `i64` | Number of pending actions. |
|
||||||
| `Queue.clear()` | — | Discard all pending actions. |
|
| `Queue.clear()` | — | Discard all pending actions (including delays). |
|
||||||
|
| `Queue.peek()` | map or `()` | Front action as a map, or unit if empty. |
|
||||||
|
| `Queue.pop()` | map or `()` | Same, removing the front. |
|
||||||
|
|
||||||
Use `Queue.length() == 0` in `tick` to avoid queuing new moves while a previous one is in flight:
|
`peek`/`pop` return an untyped map with a `"type"` key (`"Move"`, `"SetTile"`, `"Say"`, …) plus
|
||||||
|
variant-specific payload keys (e.g. `"dir"`, `"tile"`, `"msg"`). `Scroll` and `Shift` report `type`
|
||||||
|
only — their payloads are not inspectable through this API.
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
@@ -266,18 +304,20 @@ fn tick(dt) {
|
|||||||
## Rate limiting and timing
|
## Rate limiting and timing
|
||||||
|
|
||||||
- `move(dir)` costs **0.25 s** of queue delay (~4 moves/second max).
|
- `move(dir)` costs **0.25 s** of queue delay (~4 moves/second max).
|
||||||
- `delay(secs)` adds an arbitrary pause.
|
- `delay(secs)` adds an arbitrary pause; `now()` can bypass a delay for a zero-cost action already in
|
||||||
- `now()` can bypass a delay for zero-cost actions already in the queue.
|
the queue.
|
||||||
- `Queue.clear()` cancels everything, including pending delays.
|
- `Queue.clear()` cancels everything, including pending delays.
|
||||||
- Adjacent delays are always merged: two `delay(0.5)` calls become one `Delay(1.0)` entry.
|
- Adjacent delays are always merged: two `delay(0.5)` calls become one `1.0 s` delay.
|
||||||
|
- All other writes (`set_tile`, `say`, `push`, `shift`, `teleport`, stat changes, `die`, …) are
|
||||||
|
zero-cost: a run of them resolves together, and at most one *timed* action (a `move`) is released
|
||||||
|
per object per cycle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Error handling
|
## Error handling
|
||||||
|
|
||||||
Script errors (compile-time or runtime) are logged to the game log as plain-text lines and do not
|
Script errors (compile-time or runtime) are appended 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
|
crash the game. A script that throws during `tick` logs the error and resumes on the next frame.
|
||||||
on the next frame.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -288,27 +328,78 @@ on the next frame.
|
|||||||
|
|
||||||
fn init() {
|
fn init() {
|
||||||
set_tile(2);
|
set_tile(2);
|
||||||
log(`${my_name()} standing watch`);
|
log(`${Me.name} standing watch`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
if Queue.length() > 0 { return; } // still moving
|
if Queue.length() > 0 { return; } // still moving
|
||||||
|
if !blocked(East) { move(East); }
|
||||||
if !blocked(East) {
|
else if !blocked(West) { move(West); }
|
||||||
move(East);
|
|
||||||
} else if !blocked(West) {
|
|
||||||
move(West);
|
|
||||||
}
|
|
||||||
// Stuck in a corner — do nothing this tick.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bump(id) {
|
fn bump(id) {
|
||||||
if id == -1 {
|
if id == -1 { say("Halt! Who goes there?"); now(); }
|
||||||
say("Halt! Who goes there?");
|
else { say("Watch it!"); now(); }
|
||||||
now();
|
|
||||||
} else {
|
|
||||||
say("Watch where you're going!");
|
|
||||||
now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design notes (rough edges)
|
||||||
|
|
||||||
|
Collected friction points in the current surface, as input to a redesign. None of these are bugs;
|
||||||
|
they are shape problems.
|
||||||
|
|
||||||
|
### Duplication — several ways to do one thing
|
||||||
|
|
||||||
|
- **Object introspection has two parallel types with different members.** Self uses `Me`
|
||||||
|
(`Me.has_tag()`, `Me.tags()`, `Me.glyph()`, plus getters); other objects come back as `ObjectInfo`
|
||||||
|
(`.id`, `.x`, `.y`, `.name`, `.tags`) which has **no** `has_tag()` and **no** `glyph()`. The same
|
||||||
|
conceptual "an object" is two incompatible types depending on whether it's you.
|
||||||
|
- **Player position/state is reachable three ways:** `Board.player_x` / `Board.player_y`,
|
||||||
|
`Board.get(-1).x` / `.y`, and the `Player` constant (stats only). Player *position* lives on
|
||||||
|
`Board`, player *stats* live on `Player` — the player is split across two surfaces.
|
||||||
|
- **Tag reads come in three shapes:** `Me.has_tag(t)` (method, bool), `Me.tags()` (method, array),
|
||||||
|
`ObjectInfo.tags` (property, array). Method-vs-property is inconsistent even for the same data.
|
||||||
|
|
||||||
|
### Inconsistency — naming and conventions
|
||||||
|
|
||||||
|
- **No consistent verb convention.** Setters by name (`set_tile`, `set_tag`, `set_fg`, `set_bg`,
|
||||||
|
`set_color`, `set_key`), delta verbs (`add_gems`, `alter_health`), and bare verbs (`move`, `die`,
|
||||||
|
`teleport`, `push`, `shift`, `scroll`, `say`, `log`) all coexist. Worse, the two "change by a
|
||||||
|
delta" operations use *different* verbs: `add_gems` vs `alter_health`.
|
||||||
|
- **The implicit subject of a write is unpredictable.** `set_tile`/`set_fg`/`say`/`die` act on
|
||||||
|
**self**; `add_gems`/`alter_health`/`set_key` act on the **player**; `set_tag`/`send` act on an
|
||||||
|
**arbitrary target**; `push`/`shift`/`teleport` act on **cells**. The `set_*` prefix in particular
|
||||||
|
means three different subjects.
|
||||||
|
- **Mixed addressing for movement.** `move`/`blocked`/`push` take a typed `Direction`, while
|
||||||
|
`teleport`/`passable`/`can_push`/`can_shift`/`shift` take raw `i64` coordinates. There's no cell or
|
||||||
|
position value type.
|
||||||
|
- **Self-mutation can't go through `Me`.** `Me` is read-only, so writing your own tag is
|
||||||
|
`set_tag(Me.id, …)` — you pass your own id back to a free function instead of `Me.set_tag(…)`.
|
||||||
|
- **Colors are stringly-typed** (`"#RRGGBB"`, with the named constants being strings) while tiles are
|
||||||
|
bare integers with no symbolic names; glyph reads hand back colors as hex strings.
|
||||||
|
- **Overload coverage is uneven.** `say` has a duration overload, `log` does not; `delay` has int and
|
||||||
|
float overloads, most numeric fns don't.
|
||||||
|
|
||||||
|
### Unwieldy — machinery that leaks
|
||||||
|
|
||||||
|
- **Queue introspection is stringly-typed.** `peek`/`pop` return untyped maps keyed by a `"type"`
|
||||||
|
string, and `Scroll`/`Shift` aren't introspectable at all (type only). There is no typed action
|
||||||
|
value.
|
||||||
|
- **The two-tier queue + `now()`/`delay()` model is subtle.** `now()` reorders by *enqueue
|
||||||
|
recency* ("most recently enqueued action to the front"), which is position-dependent and easy to
|
||||||
|
get wrong; the object-queue → board-queue promotion plus per-object ready timer is a lot of
|
||||||
|
implicit state to reason about for "do X, wait, do Y".
|
||||||
|
- **Two persistence mechanisms.** Tags and the `Registry` are separate stores with separate APIs;
|
||||||
|
cross-board state must pick one.
|
||||||
|
- **Stringly-typed dispatch.** `send` and scroll choices route by function *name*; a scroll choice
|
||||||
|
key is silently also a function name. Renaming a handler breaks callers with no signal.
|
||||||
|
- **Engine internals leak into the script API.** `combinable(x1,y1,x2,y2)` encodes a very specific
|
||||||
|
rule (a grab thing may share a cell with the player) that exists only to support `shift`/`swap`
|
||||||
|
edge cases.
|
||||||
|
- **Read/write asymmetry on player keys.** `set_key` writes keys, but no read exists — `Player`
|
||||||
|
exposes `gems`/`health`/`max_health` only (there is a literal `TODO` to expose keys).
|
||||||
|
- **Doc/name drift across the codebase.** The function is `shift([[x, y], …])`, but the engine's own
|
||||||
|
module docs and `CLAUDE.md` still describe a `swap([[src_x, src_y, dst_x, dst_y], …])`. The two
|
||||||
|
concepts (rotate-a-cycle vs. batch directed moves) are conflated under both names.
|
||||||
|
|||||||
+12
-5
@@ -30,7 +30,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- `Behavior` — plain data struct from `Archetype::behavior()`: `solid: bool`, `opaque: bool`, `pushable: Pushable`, `grab: bool` (the player walking onto a `solid + grab` thing isn't blocked — it moves onto it and the thing's `grab()` hook fires; this is the *only* grab trigger; `Gem` and `Heart` both set the flag).
|
- `Behavior` — plain data struct from `Archetype::behavior()`: `solid: bool`, `opaque: bool`, `pushable: Pushable`, `grab: bool` (the player walking onto a `solid + grab` thing isn't blocked — it moves onto it and the thing's `grab()` hook fires; this is the *only* grab trigger; `Gem` and `Heart` both set the flag).
|
||||||
- `ObjectId = u32` — stable identifier for board objects.
|
- `ObjectId = u32` — stable identifier for board objects.
|
||||||
- `Solid` — the single solid occupant of a cell, returned by `Board::solid_at`: `Player`, `Cell(Archetype)`, or `Object(ObjectId)`.
|
- `Solid` — the single solid occupant of a cell, returned by `Board::solid_at`: `Player`, `Cell(Archetype)`, or `Object(ObjectId)`.
|
||||||
- `Player { x: i32, y: i32 }` — current player position.
|
- `PlayerPos { x: i32, y: i32 }` — the player's current position on a board (held as `Board::player`). The broader player *state* (health, gems, keys) lives in [`player::Player`], not here.
|
||||||
- `PortalDef { x, y, target_map, target_entry }` — parsed from map files, not yet runtime-wired.
|
- `PortalDef { x, y, target_map, target_entry }` — parsed from map files, not yet runtime-wired.
|
||||||
|
|
||||||
**`kiln-core/src/object_def.rs`** — scripted objects (`pub(crate)`):
|
**`kiln-core/src/object_def.rs`** — scripted objects (`pub(crate)`):
|
||||||
@@ -59,16 +59,23 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- `in_bounds((i32, i32))` — bounds-checks a possibly-negative coord.
|
- `in_bounds((i32, i32))` — bounds-checks a possibly-negative coord.
|
||||||
- `is_valid()` / `load_errors()` / `report_error(msg)` — nonfatal load-error surface.
|
- `is_valid()` / `load_errors()` / `report_error(msg)` — nonfatal load-error surface.
|
||||||
|
|
||||||
|
**`kiln-core/src/player.rs`** — game-global player state (`pub`):
|
||||||
|
- `Player { health: i64, max_health: i64, keys: Keyring, gems: i64 }` — the player's persistent stats, owned by `GameState::player` (separate from the per-board `PlayerPos` position in `utils.rs`). `Default` starts `health = max_health = 5`, `gems = 0`, no keys. `alter_gems(delta) -> bool` adds to `gems` but refuses (returns `false`, no change) if it would go negative. `alter_health(delta)` adds to `health` clamped to `[0, max_health]`. A `Copy` snapshot is handed to each script hook and exposed to Rhai (read-only) as the `Player` constant.
|
||||||
|
|
||||||
|
**`kiln-core/src/keys.rs`** — key inventory and colors (`pub`):
|
||||||
|
- `KeyType` (`Red`/`Orange`/`Yellow`/`Green`/`Blue`/`Cyan`/`Purple`/`White`) — the eight key colors. `glyph() -> Glyph` returns the key tile (12, `♀`) in that color's fg — the single source of truth for key colors, used by `Keyring::colors` and by kiln-tui's editor menu. (Moved here from `game.rs`; `archetype.rs`'s `Key` builtin aliases must match these glyphs — see its `key_aliases_have_distinct_fg_colors` test.)
|
||||||
|
- `Keyring { red, orange, yellow, green, blue, cyan, purple, white: bool }` — the player's key inventory (one bool per color), held by `Player::keys`. `set_by_name(name, value) -> bool` sets a slot by color name (returns `false` for an unknown name); `colors() -> [(bool, Rgba8); 8]` lists all eight in display order paired with their render color.
|
||||||
|
|
||||||
**`kiln-core/src/game.rs`** — game-loop logic only:
|
**`kiln-core/src/game.rs`** — game-loop logic only:
|
||||||
- `SpeechBubble { object_id, text, remaining }` — an active speech bubble created by a script's `say(s)` call. `remaining` counts down in `GameState::tick`; when it reaches zero the bubble is removed. At most one bubble per object (a new `say()` replaces the old one).
|
- `SpeechBubble { object_id, text, remaining }` — an active speech bubble created by a script's `say(s)` call. `remaining` counts down in `GameState::tick`; when it reaches zero the bubble is removed. At most one bubble per object (a new `say()` replaces the old one).
|
||||||
- `SAY_DURATION: f64` — how long a bubble lives (3.0 seconds).
|
- `SAY_DURATION: f64` — how long a bubble lives (3.0 seconds).
|
||||||
- `Scroll { source: ObjectId, lines: Vec<ScrollLine> }` — an active scroll overlay opened by a script's `scroll(lines)` call. Lives on `GameState::active_scroll: Option<Scroll>`; front-ends pause ticks while it's `Some` and close it via `close_scroll(choice)`. `ScrollLine` is re-exported from `action.rs` through `game.rs` so front-ends import both from `kiln_core::game`.
|
- `Scroll { source: ObjectId, lines: Vec<ScrollLine> }` — an active scroll overlay opened by a script's `scroll(lines)` call. Lives on `GameState::active_scroll: Option<Scroll>`; front-ends pause ticks while it's `Some` and close it via `close_scroll(choice)`. `ScrollLine` is re-exported from `action.rs` through `game.rs` so front-ends import both from `kiln_core::game`.
|
||||||
- `GameState` — owns `world: World` (all boards as `Rc<RefCell<Board>>` + world scripts) and `current_board_name: String` (key of the active board). `pub log: Vec<LogLine>`, `scripts: ScriptHost`, `pub speech_bubbles: Vec<SpeechBubble>`, `pub active_scroll: Option<Scroll>`, `pub player_health: u32` (starts 5), `pub max_health: u32` (default 5), and `pub player_gems: u32` (starts 0) — game-global player stats that persist across board transitions. `player_gems` is changed at runtime by the `add_gems(n)` script fn (e.g. grabbing a gem); `player_health` is changed by `alter_health(dh)` (clamped to `[0, max_health]`, e.g. grabbing a heart). Front-ends reach the active board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (both look up `world.boards[current_board_name]`). `current_board_name() -> &str` returns the active key. **`from_world(world: World) -> Self`** is the primary constructor: clones the start board's `Rc<RefCell<Board>>` for the `ScriptHost`, compiles scripts from `world.scripts`. `new(board)` and `with_scripts(board, scripts)` are `#[cfg(test)]`-only sugar that build a minimal single-board `World`. `try_move(dir)` moves the player (pushing if possible) and fires `bump(-1)` on any solid object walked into; walking onto a `grab` thing (via `Board::grab_object_at`) instead moves onto it and fires `grab()` + an immediate `resolve()` so its `die()`/`add_gems()`/`alter_health()` apply before the call returns (no player+object overlap). **`try_move` is the only grab trigger** — a grab thing pushed or swapped into the player is an ordinary solid (it slides/blocks/refuses like any other). `run_init()` runs object `init()` hooks once at startup. `tick(dt)` advances cooldowns, expires speech bubbles, and runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue and applies each `Action` (`Log` → `log`, `SetTile` → source glyph, `Move(dir)` → `step_object`, `Say` → `speech_bubbles`, `Scroll` → `active_scroll`, `AddGems(n)` → `player_gems`, `AlterHealth(dh)` → `player_health` clamped to `[0, max_health]`, `Die` → `remove_object(source)`). Resolution is two-phase: mutate the board collecting `(bumped, bumper)` pairs, drop the borrow, then fire `run_bump` for each pair. `drain_errors()` moves script errors into `log`. `close_scroll(choice)` clears `active_scroll` and optionally fires `run_send(source, choice, None)` to dispatch the player's selection back to the object.
|
- `GameState` — owns `world: World` (all boards as `Rc<RefCell<Board>>` + world scripts) and `current_board_name: String` (key of the active board). `pub log: Vec<LogLine>`, `scripts: ScriptHost`, `pub speech_bubbles: Vec<SpeechBubble>`, `pub active_scroll: Option<Scroll>`, and `pub player: Player` — the game-global player state ([`player::Player`]: `health`/`max_health`/`gems`/`keys`) that persists across board transitions (it is *not* per-board; `Board::player` holds only the per-board `PlayerPos`). `player.gems` is changed at runtime by the `add_gems(n)` script fn (e.g. grabbing a gem); `player.health` is changed by `alter_health(dh)` (clamped to `[0, max_health]`, e.g. grabbing a heart); `player.keys` is changed by `set_key(color, present)`. Each script-hook call is handed a snapshot of `player` wrapped in a `ScriptState` bundle (read-only, exposed to Rhai as the `Player` constant — see `script.rs`). Front-ends reach the active board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (both look up `world.boards[current_board_name]`). `current_board_name() -> &str` returns the active key. **`from_world(world: World) -> Self`** is the primary constructor: clones the start board's `Rc<RefCell<Board>>` for the `ScriptHost`, compiles scripts from `world.scripts`. `new(board)` and `with_scripts(board, scripts)` are `#[cfg(test)]`-only sugar that build a minimal single-board `World`. `try_move(dir)` moves the player (pushing if possible) and fires `bump(-1)` on any solid object walked into; walking onto a `grab` thing (via `Board::grab_object_at`) instead moves onto it and fires `grab()` + an immediate `resolve()` so its `die()`/`add_gems()`/`alter_health()` apply before the call returns (no player+object overlap). **`try_move` is the only grab trigger** — a grab thing pushed or swapped into the player is an ordinary solid (it slides/blocks/refuses like any other). `run_init()` runs object `init()` hooks once at startup. `tick(dt)` advances cooldowns, expires speech bubbles, and runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue and applies each `Action` (`Log` → `log`, `SetTile` → source glyph, `Move(dir)` → `step_object`, `Say` → `speech_bubbles`, `Scroll` → `active_scroll`, `AddGems(n)` → `player.gems`, `AlterHealth(dh)` → `player.health` clamped to `[0, max_health]`, `Die` → `remove_object(source)`). Resolution is two-phase: mutate the board collecting `(bumped, bumper)` pairs, drop the borrow, then fire `run_bump` for each pair. `drain_errors()` moves script errors into `log`. `close_scroll(choice)` clears `active_scroll` and optionally fires `run_send(source, choice, None)` to dispatch the player's selection back to the object.
|
||||||
|
|
||||||
**`kiln-core/src/action.rs`** — the `Action` enum and its Rhai-facing conversion:
|
**`kiln-core/src/action.rs`** — the `Action` enum and its Rhai-facing conversion:
|
||||||
- `MOVE_COST: f64` — how long a move occupies an object before it can act again (0.25 s).
|
- `MOVE_COST: f64` — how long a move occupies an object before it can act again (0.25 s).
|
||||||
- `ScrollLine` (`pub`) — one line of content in a `Scroll` action: `Text(String)` (plain, word-wrapped) or `Choice { choice, display }` (selectable; `choice` is sent back to the source object when the player picks it).
|
- `ScrollLine` (`pub`) — one line of content in a `Scroll` action: `Text(String)` (plain, word-wrapped) or `Choice { choice, display }` (selectable; `choice` is sent back to the source object when the player picks it).
|
||||||
- `Action` (`pub(crate)`) — deferred mutation emitted by a script and applied by `GameState` after promotion onto the board queue: `Move(Direction)`, `SetTile(u32)`, `Log(LogLine)`, `SetTag { target, tag, present }`, `Say(String)`, `Delay(f64)` (never reaches the board queue — consumed by `ScriptHost::drain` to pace the object), `SetColor { fg, bg }`, `Send { target, fn_name, arg }`, `Scroll(Vec<ScrollLine>)`, `Teleport { x, y }`, `Push { x, y, dir }` (shove a chain at arbitrary coords; zero time cost), `Swap(Vec<(i32,i32,i32,i32)>)` (batch of simultaneous one-way solid moves; zero time cost), `AddGems(i64)` (change `GameState::player_gems`, clamped at 0; zero cost), `AlterHealth(i64)` (change `GameState::player_health`, clamped to `[0, max_health]`; zero cost), `Die` (remove the source object; zero cost).
|
- `Action` (`pub(crate)`) — deferred mutation emitted by a script and applied by `GameState` after promotion onto the board queue: `Move(Direction)`, `SetTile(u32)`, `Log(LogLine)`, `SetTag { target, tag, present }`, `Say(String)`, `Delay(f64)` (never reaches the board queue — consumed by `ScriptHost::drain` to pace the object), `SetColor { fg, bg }`, `Send { target, fn_name, arg }`, `Scroll(Vec<ScrollLine>)`, `Teleport { x, y }`, `Push { x, y, dir }` (shove a chain at arbitrary coords; zero time cost), `Swap(Vec<(i32,i32,i32,i32)>)` (batch of simultaneous one-way solid moves; zero time cost), `AddGems(i64)` (change `GameState::player.gems`, clamped at 0; zero cost), `AlterHealth(i64)` (change `GameState::player.health`, clamped to `[0, max_health]`; zero cost), `Die` (remove the source object; zero cost).
|
||||||
- `action_to_map(action) -> rhai::Map` — converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`. The map always has a `"type"` key; other keys carry the payload. `Scroll`/`Swap` emit `type` only (their payloads are not inspectable via the map API). `Log` is flattened to its first span's text.
|
- `action_to_map(action) -> rhai::Map` — converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`. The map always has a `"type"` key; other keys carry the payload. `Scroll`/`Swap` emit `type` only (their payloads are not inspectable via the map API). `Log` is flattened to its first span's text.
|
||||||
|
|
||||||
**`kiln-core/src/floor.rs`** — procedural floor generators:
|
**`kiln-core/src/floor.rs`** — procedural floor generators:
|
||||||
@@ -80,8 +87,8 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
|
|
||||||
**`kiln-core/src/script.rs`** — Rhai scripting runtime:
|
**`kiln-core/src/script.rs`** — Rhai scripting runtime:
|
||||||
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects, a per-object persistent `Scope` + output queue + ready timer, and the shared board queue. Built with `ScriptHost::new(&Rc<RefCell<Board>>, &HashMap<String, String>)`: the first arg is a shared ref to the active board (used by read-API closures), the second is the world-level script pool. Each object resolves to a `(key, source)` compiled once per key: the key is the object's `script_name` (a world-pool name for a named script, or a synthetic `BUILTIN_*` name assigned by `expand_builtin_archetypes` so identical built-ins, e.g. all pushers, share one AST); the source is the pool entry for that name, or the object's embedded `builtin_script` when present. Reports compile/unknown-script failures onto the error sink; runs nothing.
|
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects, a per-object persistent `Scope` + output queue + ready timer, and the shared board queue. Built with `ScriptHost::new(&Rc<RefCell<Board>>, &HashMap<String, String>)`: the first arg is a shared ref to the active board (used by read-API closures), the second is the world-level script pool. Each object resolves to a `(key, source)` compiled once per key: the key is the object's `script_name` (a world-pool name for a named script, or a synthetic `BUILTIN_*` name assigned by `expand_builtin_archetypes` so identical built-ins, e.g. all pushers, share one AST); the source is the pool entry for that name, or the object's embedded `builtin_script` when present. Reports compile/unknown-script failures onto the error sink; runs nothing.
|
||||||
- Lifecycle hooks per object: `init()` (zero-arg), `tick(dt)` (elapsed seconds as `f64`), `bump(id)` (the bumper's `ObjectId`, or `-1` for the player), and `grab()` (zero-arg; fired when the player walks onto a `grab` thing — the only grab trigger), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)` / `run_bump(object_id, bumper)` / `run_grab(object_id)`; runtime errors go to the error sink (drained to the log), not fatal.
|
- Lifecycle hooks per object: `init()` (zero-arg), `tick(dt)` (elapsed seconds as `f64`), `bump(id)` (the bumper's `ObjectId`, or `-1` for the player), and `grab()` (zero-arg; fired when the player walks onto a `grab` thing — the only grab trigger), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init(state)` / `run_tick(state, dt)` / `run_bump(state, object_id, bumper)` / `run_grab(state, object_id)` — each takes a `ScriptState(pub Player)` bundle (a `Copy` wrapper that exists so more host context can be threaded through later without re-touching every signature); the host pulls its `Player` out and sets it into the object's scope as the `Player` constant before the call; runtime errors go to the error sink (drained to the log), not fatal.
|
||||||
- **Reads (direct):** read getters (`player_x`, `player_y`, `width`, `height`) are registered on `Rc<RefCell<Board>>` (the `BoardRef` alias, exposed to Rhai under the type name `Board`) — getters only, so read-only by construction. A clone of the handle is pushed into each scope as the constant `Board`. `blocked(dir) -> bool` is also a read fn: true if the caller's target cell is off-board, already solid, or the destination of a pending move on the board queue (an object never sees its *own* move, which is pumped only after its script returns). `can_push(x, y, dir) -> bool` is a read fn mirroring `Board::can_push`: true if `(x, y)` holds a pushable whose chain can be shoved in `dir` (false off-board). `can_shift(x, y, dir) -> bool` mirrors `Board::can_shift`: like `can_push` but only checks the single cell ahead (pushable source + an empty-or-pushable next cell), the right gate for a simultaneous shift/rotation via `swap`. `passable(x, y) -> bool` mirrors `Board::is_passable`: true if `(x, y)` is on-board and holds no solid (off-board → false), letting a script tell a hole apart from a blocked solid (which `can_shift`/`can_push` alone cannot). `has_tag(s) -> bool` and `get_tags() -> Array` read the calling object's tag set; `objects_with_tag(s) -> Array` returns all object ids carrying that tag. `my_name() -> String` returns the calling object's name (or `""` if unnamed); `object_id_for_name(s) -> i64` looks up an object by name and returns its id, or `0` if not found. Each getter briefly borrows the shared `Board`.
|
- **Reads (direct):** read getters (`player_x`, `player_y`, `width`, `height`) are registered on `Rc<RefCell<Board>>` (the `BoardRef` alias, exposed to Rhai under the type name `Board`) — getters only, so read-only by construction. A clone of the handle is pushed into each scope as the constant `Board`. The player's game-global stats are likewise exposed read-only as the `Player` constant (a `Copy` snapshot, carried in by the `ScriptState` bundle and set into scope before each hook call; getters `Player.gems`, `Player.health`, `Player.max_health` — keys not yet exposed), via `register_player_type`. `blocked(dir) -> bool` is also a read fn: true if the caller's target cell is off-board, already solid, or the destination of a pending move on the board queue (an object never sees its *own* move, which is pumped only after its script returns). `can_push(x, y, dir) -> bool` is a read fn mirroring `Board::can_push`: true if `(x, y)` holds a pushable whose chain can be shoved in `dir` (false off-board). `can_shift(x, y, dir) -> bool` mirrors `Board::can_shift`: like `can_push` but only checks the single cell ahead (pushable source + an empty-or-pushable next cell), the right gate for a simultaneous shift/rotation via `swap`. `passable(x, y) -> bool` mirrors `Board::is_passable`: true if `(x, y)` is on-board and holds no solid (off-board → false), letting a script tell a hole apart from a blocked solid (which `can_shift`/`can_push` alone cannot). `has_tag(s) -> bool` and `get_tags() -> Array` read the calling object's tag set; `objects_with_tag(s) -> Array` returns all object ids carrying that tag. `my_name() -> String` returns the calling object's name (or `""` if unnamed); `object_id_for_name(s) -> i64` looks up an object by name and returns its id, or `0` if not found. Each getter briefly borrows the shared `Board`.
|
||||||
- **Actions & rate limiting (two-tier queues):** host fns `move(dir)`, `set_tile(n)`, `log(s)`, `say(s)`, `scroll(lines)`, `push(x, y, dir)`, `swap(pairs)`, `add_gems(n)` (adjust the player's gem count), `alter_health(dh)` (adjust the player's health, clamped to `[0, max_health]`), `die()` (remove the calling object) append an `Action` (`Move`/`SetTile`/`Log`/`Say`/`Scroll`/`Push`/`Swap`; `MOVE_COST = 0.25` s for `Move`, else 0 — `push`/`swap` add no delay) to the *issuing object's* output queue (routed by the per-call tag via a `HashMap<usize, ObjQueue>`). `scroll(lines)` takes a Rhai array where each element is a string (text line) or a two-element array `[choice_key, display_text]` (selectable choice). `push(x, y, dir)` shoves the pushable chain at arbitrary coords `(x, y)`. `swap(pairs)` takes an array of four-int `[src_x, src_y, dst_x, dst_y]` arrays (a malformed entry is skipped + logged) and moves all the named solids simultaneously (see `Board::apply_swap`). `ScriptHost::pump(i)` promotes ready actions onto the shared **board queue** (`Vec<BoardAction { source, action }>`): the leading run of zero-cost actions plus at most one timed action, which arms that object's **ready timer** and ends the pump. While a ready timer is `> 0` nothing is pulled (this caps object speed); `advance_timers(dt)` counts them down each frame. `GameState` drains the board queue with `take_board_queue()` and applies it after the batch — so scripts mutate without a `&mut GameState` borrow. Errors (compile/runtime) bypass the queues via the error sink (`take_errors()`).
|
- **Actions & rate limiting (two-tier queues):** host fns `move(dir)`, `set_tile(n)`, `log(s)`, `say(s)`, `scroll(lines)`, `push(x, y, dir)`, `swap(pairs)`, `add_gems(n)` (adjust the player's gem count), `alter_health(dh)` (adjust the player's health, clamped to `[0, max_health]`), `die()` (remove the calling object) append an `Action` (`Move`/`SetTile`/`Log`/`Say`/`Scroll`/`Push`/`Swap`; `MOVE_COST = 0.25` s for `Move`, else 0 — `push`/`swap` add no delay) to the *issuing object's* output queue (routed by the per-call tag via a `HashMap<usize, ObjQueue>`). `scroll(lines)` takes a Rhai array where each element is a string (text line) or a two-element array `[choice_key, display_text]` (selectable choice). `push(x, y, dir)` shoves the pushable chain at arbitrary coords `(x, y)`. `swap(pairs)` takes an array of four-int `[src_x, src_y, dst_x, dst_y]` arrays (a malformed entry is skipped + logged) and moves all the named solids simultaneously (see `Board::apply_swap`). `ScriptHost::pump(i)` promotes ready actions onto the shared **board queue** (`Vec<BoardAction { source, action }>`): the leading run of zero-cost actions plus at most one timed action, which arms that object's **ready timer** and ends the pump. While a ready timer is `> 0` nothing is pulled (this caps object speed); `advance_timers(dt)` counts them down each frame. `GameState` drains the board queue with `take_board_queue()` and applies it after the batch — so scripts mutate without a `&mut GameState` borrow. Errors (compile/runtime) bypass the queues via the error sink (`take_errors()`).
|
||||||
- The `Queue` object (a handle to the calling object's output queue) is pushed into each scope; scripts call `Queue.length()`, `Queue.clear()`, `Queue.peek()` (front action as a Rhai map, or `()` if empty), and `Queue.pop()` (same, removes the front). Safe to mutate from script because the host only touches output queues between calls (during `pump`).
|
- The `Queue` object (a handle to the calling object's output queue) is pushed into each scope; scripts call `Queue.length()`, `Queue.clear()`, `Queue.peek()` (front action as a Rhai map, or `()` if empty), and `Queue.pop()` (same, removes the front). Safe to mutate from script because the host only touches output queues between calls (during `pump`).
|
||||||
- **Sender identity:** `source` (which object issued an action) rides the per-call **tag** — `run` calls `call_fn_with_options(...with_tag(object_id)...)` and the host fns read it via `NativeCallContext::tag()` (decoded back to an `ObjectId` by `source_of`). Scripts write `move(North)` without naming themselves; `North`/`South`/`East`/`West` are `Direction` constants in scope, and `impl From<Direction> for (i32,i32)` gives the delta.
|
- **Sender identity:** `source` (which object issued an action) rides the per-call **tag** — `run` calls `call_fn_with_options(...with_tag(object_id)...)` and the host fns read it via `NativeCallContext::tag()` (decoded back to an `ObjectId` by `source_of`). Scripts write `move(North)` without naming themselves; `North`/`South`/`East`/`West` are `Direction` constants in scope, and `impl From<Direction> for (i32,i32)` gives the delta.
|
||||||
|
|||||||
@@ -77,16 +77,16 @@ pub(crate) enum Action {
|
|||||||
/// Zero time cost.
|
/// Zero time cost.
|
||||||
Shift(Vec<(i32, i32)>),
|
Shift(Vec<(i32, i32)>),
|
||||||
/// Add `n` to the player's gem count (negative subtracts; the count is
|
/// Add `n` to the player's gem count (negative subtracts; the count is
|
||||||
/// clamped at 0). Zero time cost. Applied to `GameState::player_gems`.
|
/// clamped at 0). Zero time cost. Applied to `GameState::player.gems`.
|
||||||
AddGems(i64),
|
AddGems(i64),
|
||||||
/// Add `dh` to the player's health (clamped to `[0, max_health]`). Zero time
|
/// Add `dh` to the player's health (clamped to `[0, max_health]`). Zero time
|
||||||
/// cost. Applied to `GameState::player_health`.
|
/// cost. Applied to `GameState::player.health`.
|
||||||
AlterHealth(i64),
|
AlterHealth(i64),
|
||||||
/// Give (`true`) or take (`false`) the named key color from the player.
|
/// Give (`true`) or take (`false`) the named key color from the player.
|
||||||
///
|
///
|
||||||
/// Color must be one of `"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`,
|
/// Color must be one of `"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`,
|
||||||
/// `"orange"`, `"yellow"`, `"white"`. An unrecognized name is logged and
|
/// `"orange"`, `"yellow"`, `"white"`. An unrecognized name is logged and
|
||||||
/// ignored. Zero time cost. Applied to `GameState::player_keys`.
|
/// ignored. Zero time cost. Applied to `GameState::player.keys`.
|
||||||
SetKey(String, bool),
|
SetKey(String, bool),
|
||||||
/// Remove the source object from the board. Zero time cost. Used by grab
|
/// Remove the source object from the board. Zero time cost. Used by grab
|
||||||
/// things (e.g. gems) to despawn themselves from their `grab()` hook.
|
/// things (e.g. gems) to despawn themselves from their `grab()` hook.
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ pub struct Board {
|
|||||||
/// top-down; solidity ([`Board::solid_at`]) scans every layer. Access a single
|
/// top-down; solidity ([`Board::solid_at`]) scans every layer. Access a single
|
||||||
/// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`.
|
/// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`.
|
||||||
pub(crate) layers: Vec<Layer>,
|
pub(crate) layers: Vec<Layer>,
|
||||||
/// Current player position. See [`Player`] for caveats about its future.
|
/// Current player position on this board. See [`PlayerPos`] for caveats
|
||||||
|
/// about its future. Game-global player *stats* live in [`crate::player::Player`].
|
||||||
pub player: PlayerPos,
|
pub player: PlayerPos,
|
||||||
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
|
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
|
||||||
/// (not a `Vec`) so an object can be removed without invalidating other
|
/// (not a `Vec`) so an object can be removed without invalidating other
|
||||||
|
|||||||
+15
-12
@@ -1,7 +1,7 @@
|
|||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::script::ScriptHost;
|
use crate::script::{ScriptHost, ScriptState};
|
||||||
use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg};
|
use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg};
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
use std::cell::{Ref, RefMut};
|
use std::cell::{Ref, RefMut};
|
||||||
@@ -67,7 +67,10 @@ pub struct GameState {
|
|||||||
/// by [`enter_board`](GameState::enter_board). Front-ends tick this down and
|
/// by [`enter_board`](GameState::enter_board). Front-ends tick this down and
|
||||||
/// may block input or show a visual effect while it is `Some(t)` where `t > 0`.
|
/// may block input or show a visual effect while it is `Some(t)` where `t > 0`.
|
||||||
pub board_transition: Option<f64>,
|
pub board_transition: Option<f64>,
|
||||||
/// The player's state
|
/// The game-global player state (health, gems, keys) — see [`Player`]. Not
|
||||||
|
/// per-board: it persists across board transitions, unlike the per-board
|
||||||
|
/// position in [`Board::player`](crate::board::Board::player). Scripts mutate
|
||||||
|
/// it via `add_gems`/`alter_health`/`set_key` and read a snapshot of it.
|
||||||
pub player: Player,
|
pub player: Player,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +162,7 @@ impl GameState {
|
|||||||
/// the game is about to start — never during map deserialization, since a script
|
/// the game is about to start — never during map deserialization, since a script
|
||||||
/// may inspect the board.
|
/// may inspect the board.
|
||||||
pub fn run_init(&mut self) {
|
pub fn run_init(&mut self) {
|
||||||
self.scripts.run_init(self.player);
|
self.scripts.run_init(ScriptState(self.player));
|
||||||
self.resolve();
|
self.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +176,7 @@ impl GameState {
|
|||||||
// this runs exactly once per player interaction with a scroll.
|
// this runs exactly once per player interaction with a scroll.
|
||||||
self.handle_scroll();
|
self.handle_scroll();
|
||||||
let secs = dt.as_secs_f64();
|
let secs = dt.as_secs_f64();
|
||||||
self.scripts.run_tick(self.player, secs);
|
self.scripts.run_tick(ScriptState(self.player), secs);
|
||||||
// Expire speech bubbles before resolving new actions so a fresh say()
|
// Expire speech bubbles before resolving new actions so a fresh say()
|
||||||
// this frame isn't immediately culled.
|
// this frame isn't immediately culled.
|
||||||
self.speech_bubbles.retain_mut(|b| {
|
self.speech_bubbles.retain_mut(|b| {
|
||||||
@@ -308,11 +311,11 @@ impl GameState {
|
|||||||
Action::Shift(cells) => {
|
Action::Shift(cells) => {
|
||||||
logs.extend(board.apply_shift(&cells));
|
logs.extend(board.apply_shift(&cells));
|
||||||
}
|
}
|
||||||
// Accumulated and applied to `self.player_gems` after the borrow drops.
|
// Accumulated and applied to `self.player.gems` after the borrow drops.
|
||||||
Action::AddGems(n) => gem_delta += n,
|
Action::AddGems(n) => gem_delta += n,
|
||||||
// Accumulated and applied to `self.player_health` after the borrow drops.
|
// Accumulated and applied to `self.player.health` after the borrow drops.
|
||||||
Action::AlterHealth(dh) => health_delta += dh,
|
Action::AlterHealth(dh) => health_delta += dh,
|
||||||
// Collected and applied to `self.player_keys` after the borrow drops.
|
// Collected and applied to `self.player.keys` after the borrow drops.
|
||||||
Action::SetKey(color, present) => key_changes.push((color, present)),
|
Action::SetKey(color, present) => key_changes.push((color, present)),
|
||||||
// A grab thing despawns itself from its grab() hook.
|
// A grab thing despawns itself from its grab() hook.
|
||||||
Action::Die => {
|
Action::Die => {
|
||||||
@@ -345,10 +348,10 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (bumped, bumper) in bumps {
|
for (bumped, bumper) in bumps {
|
||||||
self.scripts.run_bump(self.player, bumped, bumper);
|
self.scripts.run_bump(ScriptState(self.player), bumped, bumper);
|
||||||
}
|
}
|
||||||
for (target, fn_name, arg) in sends {
|
for (target, fn_name, arg) in sends {
|
||||||
self.scripts.run_send(self.player, target, &fn_name, arg);
|
self.scripts.run_send(ScriptState(self.player), target, &fn_name, arg);
|
||||||
}
|
}
|
||||||
self.drain_errors();
|
self.drain_errors();
|
||||||
}
|
}
|
||||||
@@ -364,7 +367,7 @@ impl GameState {
|
|||||||
if let Some(scroll) = self.active_scroll.take()
|
if let Some(scroll) = self.active_scroll.take()
|
||||||
&& let Some(choice) = scroll.choice
|
&& let Some(choice) = scroll.choice
|
||||||
{
|
{
|
||||||
self.scripts.run_send(self.player, scroll.source, &choice, None);
|
self.scripts.run_send(ScriptState(self.player), scroll.source, &choice, None);
|
||||||
self.drain_errors();
|
self.drain_errors();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,11 +474,11 @@ impl GameState {
|
|||||||
// Fire the grab hook and resolve it immediately so the grabbed thing's
|
// Fire the grab hook and resolve it immediately so the grabbed thing's
|
||||||
// die()/add_gems() apply now — no player+object overlap survives this call.
|
// die()/add_gems() apply now — no player+object overlap survives this call.
|
||||||
if let Some(id) = grabbed {
|
if let Some(id) = grabbed {
|
||||||
self.scripts.run_grab(self.player, id);
|
self.scripts.run_grab(ScriptState(self.player), id);
|
||||||
self.resolve();
|
self.resolve();
|
||||||
}
|
}
|
||||||
if let Some(idx) = bumped {
|
if let Some(idx) = bumped {
|
||||||
self.scripts.run_bump(self.player, idx, -1);
|
self.scripts.run_bump(ScriptState(self.player), idx, -1);
|
||||||
self.drain_errors();
|
self.drain_errors();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
use crate::keys::Keyring;
|
use crate::keys::Keyring;
|
||||||
|
|
||||||
|
/// The game-global player state: stats that follow the player across boards.
|
||||||
|
///
|
||||||
|
/// Owned by [`GameState::player`](crate::game::GameState) (a single value, not
|
||||||
|
/// per-board), so health, gems, and keys persist through board transitions.
|
||||||
|
/// Distinct from [`PlayerPos`](crate::utils::PlayerPos), which is the player's
|
||||||
|
/// position *on a particular board*. A `Copy` snapshot is handed to each script
|
||||||
|
/// hook and exposed to Rhai (read-only) as the `Player` constant.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
/// The player's current health. Game-global (not per-board), so it persists
|
/// The player's current health. Game-global (not per-board), so it persists
|
||||||
@@ -15,7 +22,7 @@ pub struct Player {
|
|||||||
pub keys: Keyring,
|
pub keys: Keyring,
|
||||||
|
|
||||||
/// The number of gems the player has collected. Game-global like
|
/// The number of gems the player has collected. Game-global like
|
||||||
/// [`player_health`](GameState::player_health); starts at `0`.
|
/// [`health`](Player::health); starts at `0`.
|
||||||
pub gems: i64,
|
pub gems: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-12
@@ -81,6 +81,12 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
|
|
||||||
|
/// The host-provided context handed to every script hook for the duration of one
|
||||||
|
/// call. Currently just the player snapshot, but it exists so more host state can
|
||||||
|
/// be threaded through the `run_*` methods without changing each signature again.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct ScriptState(pub Player);
|
||||||
|
|
||||||
/// An action promoted from an object's output queue onto the board queue, tagged
|
/// An action promoted from an object's output queue onto the board queue, tagged
|
||||||
/// with the object that issued it.
|
/// with the object that issued it.
|
||||||
pub(crate) struct BoardAction {
|
pub(crate) struct BoardAction {
|
||||||
@@ -318,18 +324,18 @@ impl ScriptHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `init()` on every scripted object that defines it and drains each queue.
|
/// Calls `init()` on every scripted object that defines it and drains each queue.
|
||||||
pub fn run_init(&mut self, player: Player) {
|
pub fn run_init(&mut self, state: ScriptState) {
|
||||||
self.run("init", |c| c.has_init, (), 0.0, player);
|
self.run("init", |c| c.has_init, (), 0.0, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `tick(dt)` on every scripted object that defines it, then drains queues.
|
/// Calls `tick(dt)` on every scripted object that defines it, then drains queues.
|
||||||
pub fn run_tick(&mut self, player: Player, dt: f64) {
|
pub fn run_tick(&mut self, state: ScriptState, dt: f64) {
|
||||||
self.run("tick", |c| c.has_tick, (dt,), dt, player);
|
self.run("tick", |c| c.has_tick, (dt,), dt, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines
|
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines
|
||||||
/// the hook. After the hook, drains the object's queue with `dt = 0`.
|
/// the hook. After the hook, drains the object's queue with `dt = 0`.
|
||||||
pub fn run_bump(&mut self, player: Player, object_id: ObjectId, bumper: i64) {
|
pub fn run_bump(&mut self, state: ScriptState, object_id: ObjectId, bumper: i64) {
|
||||||
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
|
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -349,7 +355,7 @@ impl ScriptHost {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let options = CallFnOptions::default().with_tag(object_id as i64);
|
let options = CallFnOptions::default().with_tag(object_id as i64);
|
||||||
obj.scope.set_or_push("Player", player);
|
obj.scope.set_or_push("Player", state.0);
|
||||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||||
options,
|
options,
|
||||||
&mut obj.scope,
|
&mut obj.scope,
|
||||||
@@ -372,7 +378,7 @@ impl ScriptHost {
|
|||||||
/// Fired when the player walks onto a grab object or a grab object is pushed
|
/// Fired when the player walks onto a grab object or a grab object is pushed
|
||||||
/// into the player (see [`GameState`](crate::game::GameState)). The hook
|
/// into the player (see [`GameState`](crate::game::GameState)). The hook
|
||||||
/// typically increments a player stat and removes the object via `die()`.
|
/// typically increments a player stat and removes the object via `die()`.
|
||||||
pub fn run_grab(&mut self, player: Player, object_id: ObjectId) {
|
pub fn run_grab(&mut self, state: ScriptState, object_id: ObjectId) {
|
||||||
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
|
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -392,7 +398,7 @@ impl ScriptHost {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let options = CallFnOptions::default().with_tag(object_id as i64);
|
let options = CallFnOptions::default().with_tag(object_id as i64);
|
||||||
obj.scope.set_or_push("Player", player);
|
obj.scope.set_or_push("Player", state.0);
|
||||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||||
options,
|
options,
|
||||||
&mut obj.scope,
|
&mut obj.scope,
|
||||||
@@ -415,7 +421,7 @@ impl ScriptHost {
|
|||||||
/// If the function accepts zero parameters (or arg is `None`), it is called with
|
/// If the function accepts zero parameters (or arg is `None`), it is called with
|
||||||
/// no args. If neither arity exists, the call is silently skipped.
|
/// no args. If neither arity exists, the call is silently skipped.
|
||||||
/// After the call, drains the object's queue with `dt = 0`.
|
/// After the call, drains the object's queue with `dt = 0`.
|
||||||
pub(crate) fn run_send(&mut self, player: Player, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
|
pub(crate) fn run_send(&mut self, state: ScriptState, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
|
||||||
let Some(i) = self.objects.iter().position(|o| o.object_id == target_id) else {
|
let Some(i) = self.objects.iter().position(|o| o.object_id == target_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -442,7 +448,7 @@ impl ScriptHost {
|
|||||||
.any(|f| f.name == fn_name && f.params.is_empty());
|
.any(|f| f.name == fn_name && f.params.is_empty());
|
||||||
|
|
||||||
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
||||||
obj.scope.set_or_push("Player", player);
|
obj.scope.set_or_push("Player", state.0);
|
||||||
let result = if has_1 {
|
let result = if has_1 {
|
||||||
// Call with arg (or unit if arg is absent).
|
// Call with arg (or unit if arg is absent).
|
||||||
let dyn_arg: Dynamic = match &arg {
|
let dyn_arg: Dynamic = match &arg {
|
||||||
@@ -530,7 +536,7 @@ impl ScriptHost {
|
|||||||
defined: fn(&CompiledScript) -> bool,
|
defined: fn(&CompiledScript) -> bool,
|
||||||
args: A,
|
args: A,
|
||||||
drain_dt: f64,
|
drain_dt: f64,
|
||||||
player: Player
|
state: ScriptState,
|
||||||
) {
|
) {
|
||||||
for i in 0..self.objects.len() {
|
for i in 0..self.objects.len() {
|
||||||
{
|
{
|
||||||
@@ -546,7 +552,7 @@ impl ScriptHost {
|
|||||||
&& defined(compiled)
|
&& defined(compiled)
|
||||||
{
|
{
|
||||||
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
||||||
obj.scope.set_or_push("Player", player);
|
obj.scope.set_or_push("Player", state.0);
|
||||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||||
options,
|
options,
|
||||||
&mut obj.scope,
|
&mut obj.scope,
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ Renders the board as text: each `Glyph.tile` index is reinterpreted as a charact
|
|||||||
- `board_screen_pos(area, board, bx, by) -> Option<(u16, u16)>` — converts a board cell coordinate to terminal screen coordinates, returning `None` if off-screen. Used by bubble placement.
|
- `board_screen_pos(area, board, bx, by) -> Option<(u16, u16)>` — converts a board cell coordinate to terminal screen coordinates, returning `None` if off-screen. Used by bubble placement.
|
||||||
|
|
||||||
**`kiln-tui/src/status.rs`** — play-mode status sidebar widget:
|
**`kiln-tui/src/status.rs`** — play-mode status sidebar widget:
|
||||||
- `StatusSidebarWidget` — a `ratatui::widgets::Widget` built with `new(health, gems)` that draws the player's stats in a bordered `Block` titled `Status`: a `Health:` label above a row of `MAX_HEARTS` (5, local constant; the game ceiling is `GameState::max_health`) `♥` glyphs (first `health` bright red, rest dark red) and a `Gems: ♦ N` line. The gem indicator is rendered from `Archetype::Builtin(Builtin::Gem, "gem").default_glyph()` (char via `cp437::tile_to_char`, color via `rgba8_to_color`) so it matches gems on the board — not a hardcoded glyph/color. Purely presentational — reads the values handed in, never mutates game state. Drawn by `draw_play` when `ui.show_status` is set; `Tab` toggles it.
|
- `StatusSidebarWidget(pub Player)` — a `ratatui::widgets::Widget` wrapping a `kiln_core::player::Player` snapshot that draws the player's stats in a bordered `Block` titled `Status`: a `Health:` label above a row of `player.max_health` `♥` glyphs (first `player.health` bright red, rest dark red), a `Gems: ♦ N` line, and a `Keys:` row of 8 colored key glyphs (colored when held, near-black when absent, via `player.keys.colors()`). The gem indicator is rendered from `Archetype::Builtin(Builtin::Gem, "gem").default_glyph()` (char via `cp437::tile_to_char`, color via `rgba8_to_color`) so it matches gems on the board — not a hardcoded glyph/color. Purely presentational — reads the values handed in, never mutates game state. Drawn by `draw_play` when `ui.show_status` is set; `Tab` toggles it.
|
||||||
|
|
||||||
**`kiln-tui/src/log.rs`** — log panel widget:
|
**`kiln-tui/src/log.rs`** — log panel widget:
|
||||||
- `LogState { open, height, scroll }` — panel visibility, height in rows, and scroll offset. `toggle()`, `scroll_by(delta, log_len)`, `resize(target, total)`.
|
- `LogState { open, height, scroll }` — panel visibility, height in rows, and scroll offset. `toggle()`, `scroll_by(delta, log_len)`, `resize(target, total)`.
|
||||||
|
|||||||
Reference in New Issue
Block a user