Compare commits

..

2 Commits

Author SHA1 Message Date
randrews e43dae54a8 add_gems -> alter_gems 2026-07-07 23:58:51 -05:00
randrews 23b5bf2afb no need for scriptstate any more 2026-07-07 23:55:27 -05:00
22 changed files with 164 additions and 192 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ content = """
Palette `kind` values: the meta-kinds `empty` / `floor` / `object` / `portal` / `player`, or any **archetype name** directly (`wall`, `crate`, `hcrate`, `vcrate`, `pusher_north|south|east|west`, `spinner_cw|spinner_ccw`, `gem`, `heart`). For archetype/floor/object kinds, `tile`/`fg`/`bg` are optional and fall back to that kind's default glyph. An unknown `kind` becomes a visible `ErrorBlock` (logged). A floor entry uses `generator` for a procedural texture or `tile`/`fg`/`bg` for a fixed glyph. Palette `kind` values: the meta-kinds `empty` / `floor` / `object` / `portal` / `player`, or any **archetype name** directly (`wall`, `crate`, `hcrate`, `vcrate`, `pusher_north|south|east|west`, `spinner_cw|spinner_ccw`, `gem`, `heart`). For archetype/floor/object kinds, `tile`/`fg`/`bg` are optional and fall back to that kind's default glyph. An unknown `kind` becomes a visible `ErrorBlock` (logged). A floor entry uses `generator` for a procedural texture or `tile`/`fg`/`bg` for a fixed glyph.
Colors are `"#RRGGBB"` hex strings. The player is placed by a `kind = "player"` char, which must appear **exactly once** across all layers (missing → `(0,0)` + error; multiple → first + error); that cell is transparent. `tile` accepts a single-character string (`tile = " "`) or an integer (`tile = 35`). Each layer's `content` multi-line string has its leading newline trimmed by TOML and must match `width × height` (the only hard error); the `fill`/`sparse` forms are always exactly sized. An unknown `kind` produces an `ErrorBlock` and a logged warning. An object is spawned once per occurrence of its char (uppercase by convention) in reading order. **Loading is best-effort and nonfatal** (only a layer grid-dimension mismatch is a hard error): each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player wins its cell**: it silently clears solid terrain under it (on any layer) and drops any conflicting solid object. Script source lives in the world-level `[scripts]` table; objects reference a script by `script_name`. Scripts may define optional `init(me, state)`, `tick(me, state, dt)`, `bump(me, state, id)`, and `grab(me, state)` functions — every hook receives `me` (this object, an `ObjectInfo`) and `state` (the world). They **read** the world through `state.player.*` (e.g. `state.player.x`, `.health`, `.keys.red`), `state.board.*` (`.width`, `.height`, `.can_push(x, y, dir)`, `.passable(x, y)`, `.get(id)`, `.named(name)`, `.tagged(tag)`), and `me.*` (`me.x`, `me.y`, `me.has_tag(s)`, `me.blocked(dir)`, `me.can_push(dir)`, `me.waiting`, `me.queue`), and **write** via host functions `move(dir)`, `set_tile(n)`, `set_color(fg, bg)`, `log(s)`, `say(s)`, `scroll(lines)`, `send(target, fn [, arg])`, `set_tag(target, tag, present)`, `teleport(x, y)`, `push(x, y, dir)` (shove a chain at arbitrary coords), `shift([[x, y], …])` (rotate a ring of cells one step), `add_gems(n)` / `alter_health(dh)` / `set_key(color, present)` (change the player's gems / health / keys), and `die()` (remove the calling object). Writes don't take effect immediately: each is queued and **at most one `move` resolves per 250 ms** per object. When two objects move into the same cell, **lowest id wins** and the bumped object receives `bump`. The full scripting reference lives in [`docs/script-api.md`](docs/script-api.md). Colors are `"#RRGGBB"` hex strings. The player is placed by a `kind = "player"` char, which must appear **exactly once** across all layers (missing → `(0,0)` + error; multiple → first + error); that cell is transparent. `tile` accepts a single-character string (`tile = " "`) or an integer (`tile = 35`). Each layer's `content` multi-line string has its leading newline trimmed by TOML and must match `width × height` (the only hard error); the `fill`/`sparse` forms are always exactly sized. An unknown `kind` produces an `ErrorBlock` and a logged warning. An object is spawned once per occurrence of its char (uppercase by convention) in reading order. **Loading is best-effort and nonfatal** (only a layer grid-dimension mismatch is a hard error): each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player wins its cell**: it silently clears solid terrain under it (on any layer) and drops any conflicting solid object. Script source lives in the world-level `[scripts]` table; objects reference a script by `script_name`. Scripts may define optional `init(me, state)`, `tick(me, state, dt)`, `bump(me, state, id)`, and `grab(me, state)` functions — every hook receives `me` (this object, an `ObjectInfo`) and `state` (the world). They **read** the world through `state.player.*` (e.g. `state.player.x`, `.health`, `.keys.red`), `state.board.*` (`.width`, `.height`, `.can_push(x, y, dir)`, `.passable(x, y)`, `.get(id)`, `.named(name)`, `.tagged(tag)`), and `me.*` (`me.x`, `me.y`, `me.has_tag(s)`, `me.blocked(dir)`, `me.can_push(dir)`, `me.waiting`, `me.queue`), and **write** via host functions `move(dir)`, `set_tile(n)`, `set_color(fg, bg)`, `log(s)`, `say(s)`, `scroll(lines)`, `send(target, fn [, arg])`, `set_tag(target, tag, present)`, `teleport(x, y)`, `push(x, y, dir)` (shove a chain at arbitrary coords), `shift([[x, y], …])` (rotate a ring of cells one step), `alter_gems(n)` / `alter_health(dh)` / `set_key(color, present)` (change the player's gems / health / keys), and `die()` (remove the calling object). Writes don't take effect immediately: each is queued and **at most one `move` resolves per 250 ms** per object. When two objects move into the same cell, **lowest id wins** and the bumped object receives `bump`. The full scripting reference lives in [`docs/script-api.md`](docs/script-api.md).
**Script state across board transitions**: Rhai `Scope` local variables reset when the `ScriptHost` is rebuilt on board entry. Board-side state (object positions, tags, glyph) is preserved because all boards are held as `Rc<RefCell<Board>>` in `World::boards`. Scripts that need to persist information across transitions should encode it in board data (e.g. `set_tag(me.id, "visited", true)`) or the per-board `Registry` (which also persists across transitions). **Script state across board transitions**: Rhai `Scope` local variables reset when the `ScriptHost` is rebuilt on board entry. Board-side state (object positions, tags, glyph) is preserved because all boards are held as `Rc<RefCell<Board>>` in `World::boards`. Scripts that need to persist information across transitions should encode it in board data (e.g. `set_tag(me.id, "visited", true)`) or the per-board `Registry` (which also persists across transitions).
+6 -6
View File
@@ -67,11 +67,11 @@ fn bump(me, state, id) {
Called when the **player walks onto** this object and the object is a *grab* thing (gems, hearts — 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 `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()` / 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. `alter_gems()` / `alter_health()` applies before the player's move returns.
```rhai ```rhai
fn grab(me, state) { fn grab(me, state) {
add_gems(1); alter_gems(1);
die(); die();
} }
``` ```
@@ -276,7 +276,7 @@ engine after the batch. The *subject* of a write is implicit and varies by funct
| `send(target_id, fn_name [, arg])` | any object | 0 | Call a handler function on another object; optional string/number arg. | | `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. | | `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. | | `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_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]`). | | `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. | | `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. | | `die()` | self | 0 | Remove the calling object from the board. |
@@ -397,11 +397,11 @@ they are shape problems.
### Inconsistency — naming and conventions ### Inconsistency — naming and conventions
- **No consistent verb convention.** Setters by name (`set_tile`, `set_tag`, `set_fg`, `set_bg`, - **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`, `set_color`, `set_key`), delta verbs (`alter_gems`, `alter_health`), and bare verbs (`move`, `die`,
`teleport`, `push`, `shift`, `scroll`, `say`, `log`) all coexist. Worse, the two "change by a `teleport`, `push`, `shift`, `scroll`, `say`, `log`) all coexist. Worse, the two "change by a
delta" operations use *different* verbs: `add_gems` vs `alter_health`. delta" operations use *different* verbs: `alter_gems` vs `alter_health`.
- **The implicit subject of a write is unpredictable.** `set_tile`/`set_fg`/`say`/`die` act on - **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 **self**; `alter_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 **arbitrary target**; `push`/`shift`/`teleport` act on **cells**. The `set_*` prefix in particular
means three different subjects. means three different subjects.
- **Reads are methods/getters on handles, but writes are free functions.** `me.blocked(dir)` reads, - **Reads are methods/getters on handles, but writes are free functions.** `me.blocked(dir)` reads,
+3 -3
View File
@@ -69,7 +69,7 @@ fn bump(id) {
} }
fn grab() { fn grab() {
add_gems(1); alter_gems(1);
die(); die();
} }
``` ```
@@ -254,7 +254,7 @@ fn tick(dt) {
]); ]);
} }
} }
fn yes() { say("Brave soul!"); add_gems(5); } fn yes() { say("Brave soul!"); alter_gems(5); }
fn no() { say("Coward."); } fn no() { say("Coward."); }
``` ```
@@ -299,7 +299,7 @@ swap([[3, 3, 3, 4], [3, 4, 3, 3]]);
| Call | Effect | | Call | Effect |
|---|---| |---|---|
| `add_gems(n)` | Add `n` to the player's gem count (negative subtracts; clamped at 0). | | `alter_gems(n)` | Add `n` to the player's gem count (negative subtracts; clamped at 0). |
| `die()` | Remove the calling object from the board. Zero cost. | | `die()` | Remove the calling object from the board. Zero cost. |
--- ---
+2 -2
View File
@@ -70,7 +70,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
- `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>`, 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 `ScriptState` bundle — a `Copy` snapshot of `player` plus a shared handle to the active board — passed to the hook as its `state` parameter (Rhai type `State`, read via `state.player`/`state.board`; see `api::state`). 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`/`SetColor` → 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]`, `SetKey``player.keys`, `Shift``apply_shift`, `Push`/`Teleport` → board moves, `Send``run_send`, `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 `alter_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 `ScriptState` bundle — a `Copy` snapshot of `player` plus a shared handle to the active board — passed to the hook as its `state` parameter (Rhai type `State`, read via `state.player`/`state.board`; see `api::state`). 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()`/`alter_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`/`SetColor` → 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]`, `SetKey``player.keys`, `Shift``apply_shift`, `Push`/`Teleport` → board moves, `Send``run_send`, `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 supporting types: **`kiln-core/src/action.rs`** — the `Action` enum and its supporting types:
- `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).
@@ -92,7 +92,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
- Lifecycle hooks, each taking `me` (an `ObjectInfo`) and `state` (a `ScriptState`) before any hook-specific arg: `init(me, state)`, `tick(me, state, dt)`, `bump(me, state, id)` (the bumper's `ObjectId`, or `-1` for the player), and `grab(me, state)` (fired when the player walks onto a `grab` thing — the only grab trigger). All optional — detected via `AST::iter_functions()` by name **and arity** (2/3/3/2). Driven by `run_init(state)` / `run_tick(state, dt)` / `run_bump(state, id, bumper)` / `run_grab(state, id)`, all funneling through `run_hook_on_one`: it builds a fresh `ObjectInfo` for the object, pushes `[me, state, (arg)]`, and calls the hook tagged with the object's id. After the call — **whether or not the hook is defined** — it always drains the object's queue, so a delay still advances on an object that has no `tick`. Runtime errors go to the error sink (drained to the log), not fatal. - Lifecycle hooks, each taking `me` (an `ObjectInfo`) and `state` (a `ScriptState`) before any hook-specific arg: `init(me, state)`, `tick(me, state, dt)`, `bump(me, state, id)` (the bumper's `ObjectId`, or `-1` for the player), and `grab(me, state)` (fired when the player walks onto a `grab` thing — the only grab trigger). All optional — detected via `AST::iter_functions()` by name **and arity** (2/3/3/2). Driven by `run_init(state)` / `run_tick(state, dt)` / `run_bump(state, id, bumper)` / `run_grab(state, id)`, all funneling through `run_hook_on_one`: it builds a fresh `ObjectInfo` for the object, pushes `[me, state, (arg)]`, and calls the hook tagged with the object's id. After the call — **whether or not the hook is defined** — it always drains the object's queue, so a delay still advances on an object that has no `tick`. Runtime errors go to the error sink (drained to the log), not fatal.
- `run_send(state, id, fn_name, arg)` — calls an arbitrary named function on an object (backs the `send()` action and scroll-choice dispatch). Picks the param list by the function's arity: 3 → `(me, state, arg)`, 2 → `(me, state)`, 1 → `(arg)`, 0 → `()`; a missing arg is `Dynamic::UNIT`. Errors if no function of that name exists. - `run_send(state, id, fn_name, arg)` — calls an arbitrary named function on an object (backs the `send()` action and scroll-choice dispatch). Picks the param list by the function's arity: 3 → `(me, state, arg)`, 2 → `(me, state)`, 1 → `(arg)`, 0 → `()`; a missing arg is `Dynamic::UNIT`. Errors if no function of that name exists.
- **Reads** are methods/getters on the `api` types handed to the hook — `state.player.*`, `state.board.*`, `me.*` (see the `api/` section). There are **no read free functions** anymore. - **Reads** are methods/getters on the `api` types handed to the hook — `state.player.*`, `state.board.*`, `me.*` (see the `api/` section). There are **no read free functions** anymore.
- **Write API** (`register_write_api`) — free functions that enqueue an `Action` onto the **issuing object's** `queue` (resolved from the per-call tag): `move(dir)` (enqueues `Move` then a `MOVE_COST` delay), `delay(secs)`/`now()` (queue pacing), `set_tile(n)`, `set_fg`/`set_bg`/`set_color`, `set_tag(target, tag, present)`, `log(s)`, `say(s)`/`say(s, dur)`, `scroll(lines)`, `send(target, fn [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, `shift([[x, y], …])`, `add_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`. `scroll(lines)` takes a Rhai array whose elements are a string (text line) or a two-element `[choice_key, display_text]` (selectable choice). `shift` takes an array of two-int `[x, y]` arrays (a malformed entry logs an error) and rotates that ring of cells via `Board::apply_shift`. - **Write API** (`register_write_api`) — free functions that enqueue an `Action` onto the **issuing object's** `queue` (resolved from the per-call tag): `move(dir)` (enqueues `Move` then a `MOVE_COST` delay), `delay(secs)`/`now()` (queue pacing), `set_tile(n)`, `set_fg`/`set_bg`/`set_color`, `set_tag(target, tag, present)`, `log(s)`, `say(s)`/`say(s, dur)`, `scroll(lines)`, `send(target, fn [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, `shift([[x, y], …])`, `alter_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`. `scroll(lines)` takes a Rhai array whose elements are a string (text line) or a two-element `[choice_key, display_text]` (selectable choice). `shift` takes an array of two-int `[x, y]` arrays (a malformed entry logs an error) and rotates that ring of cells via `Board::apply_shift`.
- **Queue draining (two-tier):** each object's actions sit in its own `ObjQueue` until `ObjectInfo::drain(board_queue, dt)` promotes them onto the shared **board queue** (`Vec<BoardAction>`): it first eats leading `Delay` actions with `dt` (a delay longer than `dt` is decremented and stops the drain — this caps object speed), then moves the run of ready actions across. `GameState` takes the board queue with `take_board_queue()` and applies it after the batch, so scripts mutate without a `&mut GameState` borrow. Errors bypass the queues via the error sink (`take_errors()`). - **Queue draining (two-tier):** each object's actions sit in its own `ObjQueue` until `ObjectInfo::drain(board_queue, dt)` promotes them onto the shared **board queue** (`Vec<BoardAction>`): it first eats leading `Delay` actions with `dt` (a delay longer than `dt` is decremented and stops the drain — this caps object speed), then moves the run of ready actions across. `GameState` takes the board queue with `take_board_queue()` and applies it after the batch, so scripts mutate without a `&mut GameState` borrow. Errors bypass the queues via the error sink (`take_errors()`).
- **Constants in scope:** only `Registry` is pushed into each object's scope (the `me`/`state` views are hook *parameters* now). `register_global_constants` registers the `North`/`South`/`East`/`West` `Direction` values and the 16 named colors as a **global module**, visible at any call depth (including Rhai→Rhai calls and helper functions) — unlike scope constants such as `Registry`. - **Constants in scope:** only `Registry` is pushed into each object's scope (the `me`/`state` views are hook *parameters* now). `register_global_constants` registers the `North`/`South`/`East`/`West` `Direction` values and the 16 named colors as a **global module**, visible at any call depth (including Rhai→Rhai calls and helper functions) — unlike scope constants such as `Registry`.
- `Registry` (a `Board`-handle wrapper) — `Registry.get(key)` / `set(key, value)` / `get_or(key, default)` read and write the board's `registry: HashMap<String, RegistryValue>`, which **persists across board transitions**. `set(key, ())` removes a key; unsupported value types are ignored; `get_or` only returns the stored value when it matches the default's Rhai type. - `Registry` (a `Board`-handle wrapper) — `Registry.get(key)` / `set(key, value)` / `get_or(key, default)` read and write the board's `registry: HashMap<String, RegistryValue>`, which **persists across board transitions**. `set(key, ())` removes a key; unsupported value types are ignored; `get_or` only returns the stored value when it matches the default's Rhai type.
-1
View File
@@ -1,4 +1,3 @@
pub mod state;
pub mod player; pub mod player;
pub mod board; pub mod board;
pub mod object_info; pub mod object_info;
+11 -10
View File
@@ -1,21 +1,22 @@
use rhai::Engine; use rhai::Engine;
use crate::player::Player; use crate::api::board::BoardRef;
use crate::player::PlayerRef;
use crate::script::Registerable; use crate::script::Registerable;
use crate::utils::{ErrorSink, PlayerPos}; use crate::utils::ErrorSink;
/// GameState stores player state but Board stores its position, and we want one /// GameState stores player state but Board stores its position, and we want one
/// object to register with Rhai /// object to register with Rhai
#[derive(Copy, Clone)] #[derive(Clone)]
pub struct PlayerWithPos(pub Player, pub PlayerPos); pub struct PlayerWithPos(pub PlayerRef, pub BoardRef);
impl Registerable for PlayerWithPos { impl Registerable for PlayerWithPos {
fn register(engine: &mut Engine, _error_sink: ErrorSink) { fn register(engine: &mut Engine, _error_sink: ErrorSink) {
engine.register_type_with_name::<PlayerWithPos>("Player") engine.register_type_with_name::<PlayerWithPos>("Player")
.register_get("gems", |player: &mut PlayerWithPos| player.0.gems) .register_get("gems", |player: &mut PlayerWithPos| player.0.borrow().gems)
.register_get("health", |player: &mut PlayerWithPos| player.0.health) .register_get("health", |player: &mut PlayerWithPos| player.0.borrow().health)
.register_get("max_health", |player: &mut PlayerWithPos| player.0.max_health) .register_get("max_health", |player: &mut PlayerWithPos| player.0.borrow().max_health)
.register_get("keys", |player: &mut PlayerWithPos| player.0.keys) .register_get("keys", |player: &mut PlayerWithPos| player.0.borrow().keys)
.register_get("x", |player: &mut PlayerWithPos| player.1.x) .register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player.x)
.register_get("y", |player: &mut PlayerWithPos| player.1.y); .register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player.y);
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::utils::{ErrorSink, RegistryValue};
pub struct Registry(pub BoardRef); pub struct Registry(pub BoardRef);
impl Registerable for Registry { impl Registerable for Registry {
fn register(engine: &mut Engine, error_sink: ErrorSink) { fn register(engine: &mut Engine, _error_sink: ErrorSink) {
engine.register_type_with_name::<Registry>("Registry"); engine.register_type_with_name::<Registry>("Registry");
// Registry.get(key) -> Dynamic — returns () if the key is absent. // Registry.get(key) -> Dynamic — returns () if the key is absent.
-32
View File
@@ -1,32 +0,0 @@
use rhai::Engine;
use crate::api::board::BoardRef;
use crate::api::player::PlayerWithPos;
use crate::game::GameState;
use crate::script::Registerable;
use crate::utils::ErrorSink;
/// 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(Clone)]
pub struct ScriptState {
pub player: PlayerWithPos,
pub board: BoardRef,
}
impl ScriptState {
pub fn from_game_state(game_state: &GameState) -> Self {
Self {
player: PlayerWithPos(game_state.player, game_state.board().player),
board: game_state.board_rc(),
}
}
}
impl Registerable for ScriptState {
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
engine.register_type_with_name::<ScriptState>("State")
.register_get("player", |state: &mut ScriptState| state.player)
.register_get("board", |state: &mut ScriptState| state.board.clone());
}
}
+35 -34
View File
@@ -13,8 +13,7 @@ pub const SAY_DURATION: f64 = 3.0;
// Re-export ScrollLine so kiln-tui can pattern-match scroll content without // Re-export ScrollLine so kiln-tui can pattern-match scroll content without
// accessing the private `action` module directly. // accessing the private `action` module directly.
pub use crate::action::ScrollLine; pub use crate::action::ScrollLine;
use crate::api::state::ScriptState; use crate::player::{Player, PlayerRef};
use crate::player::Player;
/// An active scroll overlay opened by a scripted object via `scroll()`. /// An active scroll overlay opened by a scripted object via `scroll()`.
/// ///
@@ -71,8 +70,8 @@ pub struct GameState {
/// The game-global player state (health, gems, keys) — see [`Player`]. Not /// The game-global player state (health, gems, keys) — see [`Player`]. Not
/// per-board: it persists across board transitions, unlike the per-board /// per-board: it persists across board transitions, unlike the per-board
/// position in [`Board::player`](crate::board::Board::player). Scripts mutate /// position in [`Board::player`](crate::board::Board::player). Scripts mutate
/// it via `add_gems`/`alter_health`/`set_key` and read a snapshot of it. /// it via `alter_gems`/`alter_health`/`set_key` and read a snapshot of it.
pub player: Player, pub player: PlayerRef,
} }
impl GameState { impl GameState {
@@ -84,11 +83,13 @@ impl GameState {
/// Compile errors are surfaced into the log immediately. /// Compile errors are surfaced into the log immediately.
pub fn from_world(world: World) -> Self { pub fn from_world(world: World) -> Self {
let name = world.start.clone(); let name = world.start.clone();
let player = Player::new_ref();
let host = ScriptHost::new( let host = ScriptHost::new(
world world
.boards .boards
.get(&name) .get(&name)
.expect("world::load guarantees start board exists"), .expect("world::load guarantees start board exists").clone(),
player.clone(),
&world.scripts, &world.scripts,
); );
let mut state = Self { let mut state = Self {
@@ -99,7 +100,7 @@ impl GameState {
speech_bubbles: Vec::new(), speech_bubbles: Vec::new(),
active_scroll: None, active_scroll: None,
board_transition: None, board_transition: None,
player: Player::default() player
}; };
state.drain_errors(); state.drain_errors();
state state
@@ -163,7 +164,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(ScriptState::from_game_state(self)); self.scripts.run_init();
self.resolve(); self.resolve();
} }
@@ -177,7 +178,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(ScriptState::from_game_state(self), secs); self.scripts.run_tick(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| {
@@ -338,23 +339,22 @@ impl GameState {
} }
// Apply the net gem change (clamped at 0, since the count is unsigned). // Apply the net gem change (clamped at 0, since the count is unsigned).
if gem_delta != 0 { if gem_delta != 0 {
self.player.alter_gems(gem_delta); self.player.borrow_mut().alter_gems(gem_delta);
} }
// Apply the net health change (clamped to [0, max_health]). // Apply the net health change (clamped to [0, max_health]).
if health_delta != 0 { if health_delta != 0 {
self.player.alter_health(health_delta); self.player.borrow_mut().alter_health(health_delta);
} }
for (color, present) in key_changes { for (color, present) in key_changes {
if !self.player.keys.set_by_name(&color, present) { if !self.player.borrow_mut().keys.set_by_name(&color, present) {
self.log.push(LogLine::error(format!("set_key: unknown color {color:?}"))); self.log.push(LogLine::error(format!("set_key: unknown color {color:?}")));
} }
} }
let state = ScriptState::from_game_state(self);
for (bumped, bumper) in bumps { for (bumped, bumper) in bumps {
self.scripts.run_bump(state.clone(), bumped, bumper); self.scripts.run_bump(bumped, bumper);
} }
for (target, fn_name, arg) in sends { for (target, fn_name, arg) in sends {
self.scripts.run_send(state.clone(), target, &fn_name, arg); self.scripts.run_send(target, &fn_name, arg);
} }
self.drain_errors(); self.drain_errors();
} }
@@ -370,7 +370,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(ScriptState::from_game_state(self), scroll.source, &choice, SendArg::None); self.scripts.run_send(scroll.source, &choice, SendArg::None);
self.drain_errors(); self.drain_errors();
} }
} }
@@ -416,7 +416,8 @@ impl GameState {
self.board_mut().clear_all_queues(); self.board_mut().clear_all_queues();
// Rebuild the script host for the new board's objects. // Rebuild the script host for the new board's objects.
self.scripts = ScriptHost::new( self.scripts = ScriptHost::new(
&self.world.boards[&self.current_board_name], self.world.boards[&self.current_board_name].clone(),
self.player.clone(),
&self.world.scripts, &self.world.scripts,
); );
// Stub hook for the front-end transition animation (1 second). // Stub hook for the front-end transition animation (1 second).
@@ -476,14 +477,13 @@ impl GameState {
return; return;
} }
// 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()/alter_gems() apply now — no player+object overlap survives this call.
let state = ScriptState::from_game_state(self);
if let Some(id) = grabbed { if let Some(id) = grabbed {
self.scripts.run_grab(state.clone(), id); self.scripts.run_grab(id);
self.resolve(); self.resolve();
} }
if let Some(idx) = bumped { if let Some(idx) = bumped {
self.scripts.run_bump(state, idx, -1); self.scripts.run_bump(idx, -1);
self.drain_errors(); self.drain_errors();
} }
} }
@@ -539,7 +539,7 @@ mod tests {
game.try_move(Direction::East); game.try_move(Direction::East);
// The gem was grabbed: gem count up, gem object gone, player on its cell. // The gem was grabbed: gem count up, gem object gone, player on its cell.
assert_eq!(game.player.gems, 1); assert_eq!(game.player.borrow().gems, 1);
assert!(game.board().objects.is_empty()); assert!(game.board().objects.is_empty());
assert_eq!((game.board().player.x, game.board().player.y), (1, 0)); assert_eq!((game.board().player.x, game.board().player.y), (1, 0));
} }
@@ -567,7 +567,7 @@ mod tests {
game.tick(Duration::from_millis(16)); game.tick(Duration::from_millis(16));
// No grab: the gem is untouched and the player never moved. // No grab: the gem is untouched and the player never moved.
assert_eq!(game.player.gems, 0); assert_eq!(game.player.borrow().gems, 0);
assert!(game.board().objects.values().any(|o| o.grab)); assert!(game.board().objects.values().any(|o| o.grab));
assert_eq!((game.board().player.x, game.board().player.y), (2, 0)); assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
} }
@@ -591,7 +591,7 @@ mod tests {
// crate moves to (3,0), empty moves back to (2,0). // crate moves to (3,0), empty moves back to (2,0).
let mut game = game_with_object_script( let mut game = game_with_object_script(
5, 5,
"fn tick(m,s,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }", "fn tick(m,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }",
); );
game.tick(Duration::from_millis(16)); game.tick(Duration::from_millis(16));
let b = game.board(); let b = game.board();
@@ -609,10 +609,10 @@ mod tests {
obj.script_name = Some("s".to_string()); obj.script_name = Some("s".to_string());
let mut board = open_board(4, 1, (3, 0), vec![obj]); let mut board = open_board(4, 1, (3, 0), vec![obj]);
crate_at(&mut board, 2, 0); crate_at(&mut board, 2, 0);
let src = "fn init(m,s) { \ let src = "fn init(m) { \
log(if s.board.passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \ log(if Board.passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \
log(if s.board.passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \ log(if Board.passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \
log(if s.board.passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }"; log(if Board.passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }";
let scripts = HashMap::from([("s".to_string(), src.to_string())]); let scripts = HashMap::from([("s".to_string(), src.to_string())]);
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
@@ -727,14 +727,15 @@ mod tests {
let board = open_board(2, 1, (1, 0), vec![sobj]); let board = open_board(2, 1, (1, 0), vec![sobj]);
let scripts = HashMap::from([( let scripts = HashMap::from([(
"s".to_string(), "s".to_string(),
"fn init(m, s) { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(), "fn init(m) { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(),
)]); )]);
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
assert!(game.player.keys.blue); let keys = game.player.borrow().keys;
assert!(game.player.keys.red); assert!(keys.blue);
assert!(!game.player.keys.cyan); // cyan was not set by the script assert!(keys.red);
assert!(!keys.cyan); // cyan was not set by the script
// A second script can take a key. // A second script can take a key.
let mut sobj2 = ObjectDef::new(0, 0); let mut sobj2 = ObjectDef::new(0, 0);
@@ -743,12 +744,12 @@ mod tests {
let board2 = open_board(2, 1, (1, 0), vec![sobj2]); let board2 = open_board(2, 1, (1, 0), vec![sobj2]);
let scripts2 = HashMap::from([( let scripts2 = HashMap::from([(
"t".to_string(), "t".to_string(),
"fn init(m, s) { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(), "fn init(m) { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(),
)]); )]);
let mut game2 = GameState::with_scripts(board2, scripts2); let mut game2 = GameState::with_scripts(board2, scripts2);
game2.run_init(); game2.run_init();
assert!(!game2.player.keys.blue); assert!(!game2.player.borrow().keys.blue);
} }
#[test] #[test]
@@ -759,7 +760,7 @@ mod tests {
let board = open_board(2, 1, (1, 0), vec![sobj]); let board = open_board(2, 1, (1, 0), vec![sobj]);
let scripts = HashMap::from([( let scripts = HashMap::from([(
"s".to_string(), "s".to_string(),
r#"fn init(m,s) { set_key("chartreuse", true); }"#.to_string(), r#"fn init(m) { set_key("chartreuse", true); }"#.to_string(),
)]); )]);
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
+9
View File
@@ -1,3 +1,5 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::keys::Keyring; use crate::keys::Keyring;
/// The game-global player state: stats that follow the player across boards. /// The game-global player state: stats that follow the player across boards.
@@ -26,7 +28,14 @@ pub struct Player {
pub gems: i64, pub gems: i64,
} }
pub type PlayerRef = Rc<RefCell<Player>>;
impl Player { impl Player {
/// Create a new PlayerRef from a default player
pub fn new_ref() -> PlayerRef {
Rc::new(RefCell::from(Player::default()))
}
/// Attempt to modify the gem total by the given amount, but maintain a minimum of zero. /// Attempt to modify the gem total by the given amount, but maintain a minimum of zero.
/// If the modification would take up below zero, leave it alone and return false. /// If the modification would take up below zero, leave it alone and return false.
pub fn alter_gems(&mut self, delta: i64) -> bool { pub fn alter_gems(&mut self, delta: i64) -> bool {
+38 -44
View File
@@ -28,14 +28,14 @@
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`, //! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`, //! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
//! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, //! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`,
//! `shift([[x, y], …])`, `add_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()` //! `shift([[x, y], …])`, `alter_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`
use crate::action::{Action, BoardAction, ScrollLine, SendArg, MOVE_COST}; use crate::action::{Action, BoardAction, ScrollLine, SendArg, MOVE_COST};
use crate::game::SAY_DURATION; use crate::game::SAY_DURATION;
use crate::log::LogLine; use crate::log::LogLine;
use crate::map_file::parse_color; use crate::map_file::parse_color;
use crate::object_def::ObjectDef; use crate::object_def::ObjectDef;
use crate::utils::{Direction, ErrorSink, Hook, ObjectId, RegistryValue}; use crate::utils::{Direction, ErrorSink, Hook, ObjectId};
use rhai::{ use rhai::{
Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext, Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext,
Scope, AST, Scope, AST,
@@ -48,9 +48,9 @@ use crate::api::object_info::ObjectInfo;
use crate::api::player::PlayerWithPos; use crate::api::player::PlayerWithPos;
use crate::api::queue::ObjQueue; use crate::api::queue::ObjQueue;
use crate::api::registry::Registry; use crate::api::registry::Registry;
use crate::api::state::ScriptState;
use crate::glyph::Glyph; use crate::glyph::Glyph;
use crate::keys::Keyring; use crate::keys::Keyring;
use crate::player::PlayerRef;
/// Types which can be registered to be sent to Rhai /// Types which can be registered to be sent to Rhai
pub trait Registerable { pub trait Registerable {
@@ -100,6 +100,7 @@ pub struct ScriptHost {
scopes: HashMap<ObjectId, Scope<'static>>, scopes: HashMap<ObjectId, Scope<'static>>,
board_queue: BoardQueue, board_queue: BoardQueue,
errors: ErrorSink, errors: ErrorSink,
board: BoardRef
} }
impl ScriptHost { impl ScriptHost {
@@ -110,7 +111,7 @@ impl ScriptHost {
/// ///
/// `scripts` is the world-level script pool (script name → Rhai source); it is /// `scripts` is the world-level script pool (script name → Rhai source); it is
/// read only during construction and not retained afterward. /// read only during construction and not retained afterward.
pub fn new(board_cell: &BoardRef, script_sources: &HashMap<String, String>) -> Self { pub fn new(board_ref: BoardRef, player: PlayerRef, script_sources: &HashMap<String, String>) -> Self {
let board_queue: BoardQueue = Rc::new(RefCell::new(Vec::new())); let board_queue: BoardQueue = Rc::new(RefCell::new(Vec::new()));
let errors = ErrorSink::new(); let errors = ErrorSink::new();
let mut scopes = HashMap::new(); let mut scopes = HashMap::new();
@@ -120,16 +121,15 @@ impl ScriptHost {
PlayerWithPos::register(&mut engine, errors.clone()); PlayerWithPos::register(&mut engine, errors.clone());
Keyring::register(&mut engine, errors.clone()); Keyring::register(&mut engine, errors.clone());
BoardRef::register(&mut engine, errors.clone()); BoardRef::register(&mut engine, errors.clone());
ScriptState::register(&mut engine, errors.clone());
ObjectInfo::register(&mut engine, errors.clone()); ObjectInfo::register(&mut engine, errors.clone());
Glyph::register(&mut engine, errors.clone()); Glyph::register(&mut engine, errors.clone());
ObjQueue::register(&mut engine, errors.clone()); ObjQueue::register(&mut engine, errors.clone());
Registry::register(&mut engine, errors.clone()); Registry::register(&mut engine, errors.clone());
register_write_api(&mut engine, board_cell.clone()); register_write_api(&mut engine, board_ref.clone());
register_global_constants(&mut engine, board_cell.clone()); register_global_constants(&mut engine, board_ref.clone(), player.clone());
let board = board_cell.borrow(); let board = board_ref.borrow();
// Compile each referenced script once, keyed by `script_key` (the object's // Compile each referenced script once, keyed by `script_key` (the object's
// `script_name`: a world-pool name for named scripts, or a synthetic // `script_name`: a world-pool name for named scripts, or a synthetic
@@ -166,10 +166,10 @@ impl ScriptHost {
scripts.insert( scripts.insert(
key, key,
CompiledScript { CompiledScript {
has_init: defines("init", 2), has_init: defines("init", 1),
has_tick: defines("tick", 3), has_tick: defines("tick", 2),
has_bump: defines("bump", 3), has_bump: defines("bump", 2),
has_grab: defines("grab", 2), has_grab: defines("grab", 1),
ast, ast,
}, },
); );
@@ -199,43 +199,41 @@ impl ScriptHost {
board_queue, board_queue,
errors, errors,
scopes, scopes,
board: board_ref
} }
} }
/// 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, state: ScriptState, dt: f64) { pub fn run_tick(&mut self, dt: f64) {
self.run_hook_on_all(Hook::Tick, state, dt); self.run_hook_on_all(Hook::Tick, dt);
} }
pub fn run_init(&mut self, state: ScriptState) { pub fn run_init(&mut self) {
self.run_hook_on_all(Hook::Init, state, 0.0) self.run_hook_on_all(Hook::Init, 0.0)
} }
/// Run the given hook on every object that defines it. For hooks other than /// Run the given hook on every object that defines it. For hooks other than
/// `Tick`, dt should just be 0.0 /// `Tick`, dt should just be 0.0
fn run_hook_on_all(&mut self, hook: Hook, state: ScriptState, dt: f64) { fn run_hook_on_all(&mut self, hook: Hook, dt: f64) {
let all_ids = state.board.borrow().all_ids(); let all_ids = self.board.borrow().all_ids();
let arg = match hook { let arg = match hook {
Hook::Tick => Some(Dynamic::from(dt)), Hook::Tick => Some(Dynamic::from(dt)),
_ => None, _ => None,
}; };
for id in all_ids { for id in all_ids {
self.run_hook_on_one(hook, state.clone(), id, arg.clone(), dt) self.run_hook_on_one(hook, id, arg.clone(), dt)
} }
} }
fn run_hook_on_one(&mut self, hook: Hook, state: ScriptState, id: ObjectId, arg: Option<Dynamic>, dt: f64) { fn run_hook_on_one(&mut self, hook: Hook, id: ObjectId, arg: Option<Dynamic>, dt: f64) {
if let Some(mut info) = ObjectInfo::from_id(id, state.board.clone()) { if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) {
if let Some(script_key) = info.script_name.as_ref() if let Some(script_key) = info.script_name.as_ref()
&& let Some(script) = self.scripts.get(script_key) && let Some(script) = self.scripts.get(script_key)
&& let Some(scope) = self.scopes.get_mut(&id) { && let Some(scope) = self.scopes.get_mut(&id) {
if script.has(hook) { if script.has(hook) {
let mut args = vec![ let mut args = vec![Dynamic::from(info.clone())];
Dynamic::from(info.clone()),
Dynamic::from(state.clone())
];
if let Some(d) = arg { args.push(d) } if let Some(d) = arg { args.push(d) }
// Call this with opts tagging this call as our id. The write API will read // Call this with opts tagging this call as our id. The write API will read
@@ -261,8 +259,8 @@ impl ScriptHost {
/// 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, state: ScriptState, id: ObjectId, bumper: i64) { pub fn run_bump(&mut self, id: ObjectId, bumper: i64) {
self.run_hook_on_one(Hook::Bump, state, id, Some(Dynamic::from(bumper)), 0.0); self.run_hook_on_one(Hook::Bump, id, Some(Dynamic::from(bumper)), 0.0);
} }
/// Calls `grab()` on the object with [`ObjectId`] `object_id`, if it defines the /// Calls `grab()` on the object with [`ObjectId`] `object_id`, if it defines the
@@ -271,21 +269,20 @@ 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, state: ScriptState, object_id: ObjectId) { pub fn run_grab(&mut self, object_id: ObjectId) {
self.run_hook_on_one(Hook::Grab, state, object_id, None, 0.0); self.run_hook_on_one(Hook::Grab, object_id, None, 0.0);
} }
/// Calls the named function on the object with [`ObjectId`] `target_id`. /// Calls the named function on the object with [`ObjectId`] `target_id`.
/// ///
/// What we pass depends on arity, in this order: /// What we pass depends on arity, in this order:
/// - If it has arity 3, we pass `[ObjectInfo, ScriptState, SendArg]` /// - If it has arity 2, we pass `[ObjectInfo, arg]`
/// - If it has arity 2, we pass `[ObjectInfo, ScriptState]` /// - If it has arity 1, we pass the ObjectInfo alone
/// - If it has arity 1, we pass the arg
/// - If it has arity 0, we pass nothing /// - If it has arity 0, we pass nothing
/// ///
/// In cases where the arg isn't provided but we have the arity, we pass `Dynamic::UNIT` /// In cases where the arg isn't provided but we have the arity, we pass `Dynamic::UNIT`
pub(crate) fn run_send(&mut self, state: ScriptState, id: ObjectId, fn_name: &str, arg: SendArg) { pub(crate) fn run_send(&mut self, id: ObjectId, fn_name: &str, arg: SendArg) {
if let Some(mut info) = ObjectInfo::from_id(id, state.board.clone()) { if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) {
if let Some(script_key) = info.script_name.as_ref() if let Some(script_key) = info.script_name.as_ref()
&& let Some(script) = self.scripts.get(script_key) && let Some(script) = self.scripts.get(script_key)
&& let Some(scope) = self.scopes.get_mut(&id) { && let Some(scope) = self.scopes.get_mut(&id) {
@@ -307,15 +304,11 @@ impl ScriptHost {
// Assemble the args // Assemble the args
let mut args = vec![]; let mut args = vec![];
if arities.contains(&3) { if arities.contains(&2) {
args.push(Dynamic::from(info.clone())); args.push(Dynamic::from(info.clone()));
args.push(Dynamic::from(state.clone()));
args.push(arg.into()); args.push(arg.into());
} else if arities.contains(&2) {
args.push(Dynamic::from(info.clone()));
args.push(Dynamic::from(state.clone()));
} else if arities.contains(&1) { } else if arities.contains(&1) {
args.push(arg.into()); args.push(Dynamic::from(info.clone()));
} }
// Call this with opts tagging this call as our id. The write API will read // Call this with opts tagging this call as our id. The write API will read
@@ -384,9 +377,9 @@ fn register_write_api(engine: &mut Engine, board: BoardRef) {
emit(&b, source_of(&ctx), Action::SetTile(tile as u32)); emit(&b, source_of(&ctx), Action::SetTile(tile as u32));
}); });
// add_gems(n): change the player's gem count (negative subtracts; clamped at 0). // alter_gems(n): change the player's gem count (negative subtracts; clamped at 0).
let b = board.clone(); let b = board.clone();
engine.register_fn("add_gems", move |ctx: NativeCallContext, n: i64| { engine.register_fn("alter_gems", move |ctx: NativeCallContext, n: i64| {
emit(&b, source_of(&ctx), Action::AddGems(n)); emit(&b, source_of(&ctx), Action::AddGems(n));
}); });
@@ -614,9 +607,10 @@ fn read_coord_array(arr: &Array) -> Result<Vec<(i64, i64)>, ()> {
/// Registers direction and color constants as a global Rhai module so they are /// Registers direction and color constants as a global Rhai module so they are
/// visible to every function at any call depth, including Rhai-to-Rhai calls. /// visible to every function at any call depth, including Rhai-to-Rhai calls.
/// (Scope-level constants are only visible to the top-level function Rust calls.) /// (Scope-level constants are only visible to the top-level function Rust calls.)
fn register_global_constants(engine: &mut Engine, board: BoardRef) { fn register_global_constants(engine: &mut Engine, board: BoardRef, player: PlayerRef) {
let mut m = Module::new(); let mut m = Module::new();
m.set_var("Board", board); m.set_var("Board", board.clone());
m.set_var("Player", PlayerWithPos(player.clone(), board));
m.set_var("North", Direction::North); m.set_var("North", Direction::North);
m.set_var("South", Direction::South); m.set_var("South", Direction::South);
m.set_var("East", Direction::East); m.set_var("East", Direction::East);
+2 -2
View File
@@ -3,7 +3,7 @@
// A gem is a grabbable collectible: walking onto it (or pushing it into the // A gem is a grabbable collectible: walking onto it (or pushing it into the
// player) fires this `grab()` hook instead of blocking. We bump the player's gem // player) fires this `grab()` hook instead of blocking. We bump the player's gem
// count and remove ourselves from the board. // count and remove ourselves from the board.
fn grab(me, state) { fn grab(me) {
add_gems(1); alter_gems(1);
die(); die();
} }
+1 -1
View File
@@ -2,7 +2,7 @@
// //
// A heart is a grabbable collectible: walking onto it fires `grab()` instead // A heart is a grabbable collectible: walking onto it fires `grab()` instead
// of blocking. It restores 1 health and removes itself from the board. // of blocking. It restores 1 health and removes itself from the board.
fn grab(me, state) { fn grab(me) {
alter_health(1); alter_health(1);
die(); die();
} }
+1 -1
View File
@@ -3,7 +3,7 @@
// A gem is a grabbable collectible: walking onto it (or pushing it into the // A gem is a grabbable collectible: walking onto it (or pushing it into the
// player) fires this `grab()` hook instead of blocking. We bump the player's gem // player) fires this `grab()` hook instead of blocking. We bump the player's gem
// count and remove ourselves from the board. // count and remove ourselves from the board.
fn grab(me, state) { fn grab(me) {
let colors = [ let colors = [
"red", "red",
"orange", "orange",
+1 -1
View File
@@ -6,7 +6,7 @@
// pusher shares one compiled copy and learns its direction from the // pusher shares one compiled copy and learns its direction from the
// `BUILTIN_pusher_<dir>` tag the map loader attached to it. // `BUILTIN_pusher_<dir>` tag the map loader attached to it.
fn tick(me, state, dt) { fn tick(me, dt) {
// Only queue a step when idle. `move` shoves the chain ahead via step_object // Only queue a step when idle. `move` shoves the chain ahead via step_object
// and is a no-op when blocked; the delay pads each step out to ~0.5s (move // and is a no-op when blocked; the delay pads each step out to ~0.5s (move
// itself costs 0.25s), matching the old global pusher heartbeat. // itself costs 0.25s), matching the old global pusher heartbeat.
+1 -1
View File
@@ -5,7 +5,7 @@
// engine and the script just hands it the ring. The spin direction comes from the // engine and the script just hands it the ring. The spin direction comes from the
// `BUILTIN_spinner_*` tag the map loader attaches (it defaults to clockwise when no // `BUILTIN_spinner_*` tag the map loader attaches (it defaults to clockwise when no
// tag is present). // tag is present).
fn tick(me, state, dt) { fn tick(me, dt) {
// Only start a new rotation when the previous one (and its delay) has drained, // Only start a new rotation when the previous one (and its delay) has drained,
// exactly like the built-in pusher's pacing. // exactly like the built-in pusher's pacing.
if me.waiting { return; } if me.waiting { return; }
+11 -11
View File
@@ -8,7 +8,7 @@ use std::time::Duration;
fn move_command_relocates_the_source_object() { fn move_command_relocates_the_source_object() {
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "m")]); let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "m")]);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")]));
game.run_init(); game.run_init();
let b = game.board(); let b = game.board();
// East increments x by one; the object started at (2, 1). // East increments x by one; the object started at (2, 1).
@@ -20,7 +20,7 @@ fn move_into_a_wall_or_edge_is_a_noop() {
// Object at the west edge moving west: out of bounds, ignored. // Object at the west edge moving west: out of bounds, ignored.
let board = open_board(5, 3, (0, 0), vec![scripted_object(0, 1, "m")]); let board = open_board(5, 3, (0, 0), vec![scripted_object(0, 1, "m")]);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(West); }")])); GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(West); }")]));
game.run_init(); game.run_init();
assert_eq!(game.board().objects[&1].x, 0); assert_eq!(game.board().objects[&1].x, 0);
} }
@@ -29,7 +29,7 @@ fn move_into_a_wall_or_edge_is_a_noop() {
fn set_tile_command_changes_the_source_glyph() { fn set_tile_command_changes_the_source_glyph() {
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "s")]); let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "s")]);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("s", "fn init(m,s) { set_tile(7); }")])); GameState::with_scripts(board, scripts_from(&[("s", "fn init(m) { set_tile(7); }")]));
game.run_init(); game.run_init();
assert_eq!(game.board().objects[&1].glyph.tile, 7); assert_eq!(game.board().objects[&1].glyph.tile, 7);
} }
@@ -40,7 +40,7 @@ fn object_pushes_crate_on_init() {
let mut board = open_board(4, 1, (0, 0), vec![scripted_object(1, 0, "m")]); let mut board = open_board(4, 1, (0, 0), vec![scripted_object(1, 0, "m")]);
crate_at(&mut board, 2, 0); crate_at(&mut board, 2, 0);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")]));
game.run_init(); game.run_init();
let b = game.board(); let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0));
@@ -53,7 +53,7 @@ fn object_push_into_player() {
// A scripted object moving into the player pushes the player when there's room. // A scripted object moving into the player pushes the player when there's room.
let board = open_board(5, 1, (2, 0), vec![scripted_object(1, 0, "m")]); let board = open_board(5, 1, (2, 0), vec![scripted_object(1, 0, "m")]);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")]));
game.run_init(); game.run_init();
{ {
let b = game.board(); let b = game.board();
@@ -65,7 +65,7 @@ fn object_push_into_player() {
let mut board = open_board(4, 1, (2, 0), vec![scripted_object(1, 0, "m")]); let mut board = open_board(4, 1, (2, 0), vec![scripted_object(1, 0, "m")]);
wall_at(&mut board, 3, 0); wall_at(&mut board, 3, 0);
let mut game = let mut game =
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")]));
game.run_init(); game.run_init();
let b = game.board(); let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0));
@@ -79,7 +79,7 @@ fn move_cost_rate_limits_repeated_moves() {
let board = open_board(5, 1, (0, 0), vec![scripted_object(1, 0, "m")]); let board = open_board(5, 1, (0, 0), vec![scripted_object(1, 0, "m")]);
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[("m", "fn init(m,s) { move(East); move(East); }")]), scripts_from(&[("m", "fn init(m) { move(East); move(East); }")]),
); );
game.run_init(); game.run_init();
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2) assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
@@ -102,7 +102,7 @@ fn inline_delay_paces_subsequent_moves() {
wall_at(&mut board, 2, 1); wall_at(&mut board, 2, 1);
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[("m", "fn init(m,s) { move(East); move(South); }")]), scripts_from(&[("m", "fn init(m) { move(East); move(South); }")]),
); );
game.run_init(); game.run_init();
// First (eastward) move is blocked by the wall: object hasn't moved. // First (eastward) move is blocked by the wall: object hasn't moved.
@@ -136,7 +136,7 @@ fn queue_length_reports_pending_actions() {
board, board,
scripts_from(&[( scripts_from(&[(
"q", "q",
"fn init(m,s) { set_tile(5); set_tile(6); log(`len=${m.queue.length}`); }", "fn init(m) { set_tile(5); set_tile(6); log(`len=${m.queue.length}`); }",
)]), )]),
); );
game.run_init(); game.run_init();
@@ -166,7 +166,7 @@ fn blocked_reports_solid_and_clear() {
board, board,
scripts_from(&[( scripts_from(&[(
"b", "b",
"fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", "fn init(m) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }",
)]), )]),
); );
game.run_init(); game.run_init();
@@ -178,7 +178,7 @@ fn blocked_reports_solid_and_clear() {
board, board,
scripts_from(&[( scripts_from(&[(
"b", "b",
"fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", "fn init(m) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }",
)]), )]),
); );
game.run_init(); game.run_init();
+2 -2
View File
@@ -18,11 +18,11 @@ fn collision_priority_resolves_in_array_order_and_bumps() {
scripts_from(&[ scripts_from(&[
( (
"e", "e",
"fn init(m,s) { move(East); } fn bump(m,s,id) { log(`o0 by ${id}`); }", "fn init(m) { move(East); } fn bump(m,id) { log(`o0 by ${id}`); }",
), ),
( (
"w", "w",
"fn init(m,s) { move(West); } fn bump(m,s,id) { log(`o1 by ${id}`); }", "fn init(m) { move(West); } fn bump(m,id) { log(`o1 by ${id}`); }",
), ),
]), ]),
); );
+20 -20
View File
@@ -8,7 +8,7 @@ use std::time::Duration;
fn init_runs_only_on_run_init_not_at_construction() { fn init_runs_only_on_run_init_not_at_construction() {
let (board, scripts) = board_with_object( let (board, scripts) = board_with_object(
Some("greet"), Some("greet"),
&[("greet", r#"fn init(m,s) { log("hello"); }"#)], &[("greet", r#"fn init(m) { log("hello"); }"#)],
); );
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
// init must not fire during construction / deserialization. // init must not fire during construction / deserialization.
@@ -22,7 +22,7 @@ fn init_runs_only_on_run_init_not_at_construction() {
fn tick_calls_script_tick_with_elapsed_seconds() { fn tick_calls_script_tick_with_elapsed_seconds() {
let (board, scripts) = board_with_object( let (board, scripts) = board_with_object(
Some("t"), Some("t"),
&[("t", r#"fn tick(m,s,dt) { log(dt.to_string()); }"#)], &[("t", r#"fn tick(m,dt) { log(dt.to_string()); }"#)],
); );
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
@@ -69,7 +69,7 @@ fn script_reads_board_through_view() {
let board = open_board(5, 3, (3, 1), vec![scripted_object(2, 1, "r")]); let board = open_board(5, 3, (3, 1), vec![scripted_object(2, 1, "r")]);
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[("r", "fn init(m,s) { log(s.player.x.to_string()); }")]), scripts_from(&[("r", "fn init(m) { log(Player.x.to_string()); }")]),
); );
game.run_init(); game.run_init();
assert_eq!(log_texts(&game), vec!["3"]); assert_eq!(log_texts(&game), vec!["3"]);
@@ -88,8 +88,8 @@ fn commands_are_routed_to_their_own_source_object() {
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[ scripts_from(&[
("e", "fn init(m,s) { move(East); }"), ("e", "fn init(m) { move(East); }"),
("w", "fn init(m,s) { move(West); }"), ("w", "fn init(m) { move(West); }"),
]), ]),
); );
game.run_init(); game.run_init();
@@ -127,7 +127,7 @@ fn set_tag_adds_and_removes_via_my_id() {
// present on the object afterward. // present on the object afterward.
let (board, scripts) = board_with_object( let (board, scripts) = board_with_object(
Some("t"), Some("t"),
&[("t", r#"fn init(m,s) { set_tag(m.id, "active", true); }"#)], &[("t", r#"fn init(m) { set_tag(m.id, "active", true); }"#)],
); );
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
@@ -136,7 +136,7 @@ fn set_tag_adds_and_removes_via_my_id() {
// A script removes a pre-existing tag. // A script removes a pre-existing tag.
let (mut board2, scripts2) = board_with_object( let (mut board2, scripts2) = board_with_object(
Some("t2"), Some("t2"),
&[("t2", r#"fn init(m,s) { set_tag(m.id, "active", false); }"#)], &[("t2", r#"fn init(m) { set_tag(m.id, "active", false); }"#)],
); );
// Seed the tag before construction. // Seed the tag before construction.
board2 board2
@@ -158,8 +158,8 @@ fn has_tag_reads_own_tags() {
Some("t"), Some("t"),
&[( &[(
"t", "t",
r#"fn init(m,s) { set_tag(m.id, "active", true); } r#"fn init(m) { set_tag(m.id, "active", true); }
fn tick(m,s,dt) { log(m.has_tag("active").to_string()); }"#, fn tick(m,dt) { log(m.has_tag("active").to_string()); }"#,
)], )],
); );
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
@@ -182,8 +182,8 @@ fn objects_with_tag_returns_matching_ids() {
scripts_from(&[ scripts_from(&[
( (
"q", "q",
r#"fn init(m,s) { r#"fn init(m) {
let infos = s.board.tagged("enemy"); let infos = Board.tagged("enemy");
log(infos.len().to_string()); log(infos.len().to_string());
log(infos[0].id.to_string()); log(infos[0].id.to_string());
}"#, }"#,
@@ -201,7 +201,7 @@ fn objects_with_tag_returns_matching_ids() {
fn my_name_returns_name_or_empty_string() { fn my_name_returns_name_or_empty_string() {
// An object with a name set on its ObjectDef should see it via my_name(). // An object with a name set on its ObjectDef should see it via my_name().
let (mut board, scripts) = let (mut board, scripts) =
board_with_object(Some("n"), &[("n", r#"fn init(m,s) { log(m.name); }"#)]); board_with_object(Some("n"), &[("n", r#"fn init(m) { log(m.name); }"#)]);
board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string()); board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string());
let mut game = GameState::with_scripts(board, scripts); let mut game = GameState::with_scripts(board, scripts);
game.run_init(); game.run_init();
@@ -209,7 +209,7 @@ fn my_name_returns_name_or_empty_string() {
// An unnamed object should get (). // An unnamed object should get ().
let (board2, scripts2) = let (board2, scripts2) =
board_with_object(Some("n"), &[("n", r#"fn init(m,s) { if m.name == () { log("null"); }}"#)]); board_with_object(Some("n"), &[("n", r#"fn init(m) { if m.name == () { log("null"); }}"#)]);
let mut game2 = GameState::with_scripts(board2, scripts2); let mut game2 = GameState::with_scripts(board2, scripts2);
game2.run_init(); game2.run_init();
assert_eq!(log_texts(&game2), vec!["null"]); assert_eq!(log_texts(&game2), vec!["null"]);
@@ -228,9 +228,9 @@ fn object_id_for_name_finds_by_name() {
scripts_from(&[ scripts_from(&[
( (
"q", "q",
r#"fn init(m,s) { r#"fn init(m) {
log(s.board.named("target").id.to_string()); log(Board.named("target").id.to_string());
let miss = s.board.named("missing"); let miss = Board.named("missing");
log(if miss == () { "not_found" } else { miss.id.to_string() }); log(if miss == () { "not_found" } else { miss.id.to_string() });
}"#, }"#,
), ),
@@ -251,7 +251,7 @@ fn player_bump_fires_with_negative_one() {
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "b")]); let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "b")]);
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[("b", "fn bump(m,s,id) { log(`bumped by ${id}`); }")]), scripts_from(&[("b", "fn bump(m,id) { log(`bumped by ${id}`); }")]),
); );
game.run_init(); game.run_init();
game.try_move(Direction::East); game.try_move(Direction::East);
@@ -271,7 +271,7 @@ fn scroll_opens_on_player_bump() {
board, board,
scripts_from(&[( scripts_from(&[(
"s", "s",
r#"fn bump(m,s,id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#, r#"fn bump(m,id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#,
)]), )]),
); );
game.run_init(); game.run_init();
@@ -295,7 +295,7 @@ fn handle_scroll_without_choice_clears_it() {
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]); let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
let mut game = GameState::with_scripts( let mut game = GameState::with_scripts(
board, board,
scripts_from(&[("s", r#"fn bump(m,s,id) { scroll(["Hello"]); }"#)]), scripts_from(&[("s", r#"fn bump(m,id) { scroll(["Hello"]); }"#)]),
); );
game.run_init(); game.run_init();
game.try_move(Direction::East); game.try_move(Direction::East);
@@ -317,7 +317,7 @@ fn handle_scroll_with_choice_dispatches_send_to_source() {
scripts_from(&[( scripts_from(&[(
"s", "s",
r#" r#"
fn bump(m,s,id) { scroll(["Muffin?", ["eat", "Eat it"]]); } fn bump(m,id) { scroll(["Muffin?", ["eat", "Eat it"]]); }
fn eat() { log("eaten"); } fn eat() { log("eaten"); }
"#, "#,
)]), )]),
+1 -1
View File
@@ -342,7 +342,7 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
.spacing(Spacing::Overlap(1)) .spacing(Spacing::Overlap(1))
.areas(frame.area()); .areas(frame.area());
frame.render_widget( frame.render_widget(
StatusSidebarWidget(game.player), StatusSidebarWidget(game.player.clone()),
sidebar_area, sidebar_area,
); );
rest rest
+3 -3
View File
@@ -15,7 +15,7 @@ use ratatui::style::{Color, Style};
use ratatui::symbols::merge::MergeStrategy; use ratatui::symbols::merge::MergeStrategy;
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Widget}; use ratatui::widgets::{Block, Paragraph, Widget};
use kiln_core::player::Player; use kiln_core::player::PlayerRef;
/// A filled heart: bright red. /// A filled heart: bright red.
const HEART_FULL: Color = Color::Rgb(255, 0, 0); const HEART_FULL: Color = Color::Rgb(255, 0, 0);
@@ -27,11 +27,11 @@ const HEART_EMPTY: Color = Color::Rgb(90, 0, 0);
/// Shows a `Health:` label above a row of [`MAX_HEARTS`] heart glyphs, a /// Shows a `Health:` label above a row of [`MAX_HEARTS`] heart glyphs, a
/// `Gems: ♦ N` line, and a `Keys:` row of 8 `♀` glyphs colored when held /// `Gems: ♦ N` line, and a `Keys:` row of 8 `♀` glyphs colored when held
/// and dark gray when absent. /// and dark gray when absent.
pub struct StatusSidebarWidget(pub Player); pub struct StatusSidebarWidget(pub PlayerRef);
impl Widget for StatusSidebarWidget { impl Widget for StatusSidebarWidget {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let player = self.0; let player = self.0.borrow();
// One bright-red span per filled heart, dark-red for the rest, so a // One bright-red span per filled heart, dark-red for the rest, so a
// partly-depleted bar reads at a glance. // partly-depleted bar reads at a glance.
let filled = player.health; let filled = player.health;
+15 -15
View File
@@ -6,15 +6,15 @@ start = "start"
# Objects reference a script by name via `script_name`. # Objects reference a script by name via `script_name`.
[scripts] [scripts]
greeter = """ greeter = """
fn init(me, state) { fn init(me) {
log(`hello from object player at ${state.player.x}, ${state.player.y}`); log(`hello from object player at ${Player.x}, ${Player.y}`);
set_tile(2); // change my glyph to proves the write path set_tile(2); // change my glyph to proves the write path
say("Hello there,\\ntraveller!"); say("Hello there,\\ntraveller!");
log(`Player health: ${state.player.health}`); log(`Player health: ${Player.health}`);
} }
fn tick(me, state, dt) { fn tick(me, dt) {
if state.player.x == me.x && state.player.y == me.y && !me.waiting { if Player.x == me.x && Player.y == me.y && !me.waiting {
say("Hey! Get offa me!"); say("Hey! Get offa me!");
me.delay(5.0); me.delay(5.0);
} }
@@ -22,7 +22,7 @@ fn tick(me, state, dt) {
""" """
mover = """ mover = """
fn init(me, state) { fn init(me) {
log(`q: ${me.queue.length}`); log(`q: ${me.queue.length}`);
if Board.registry.get("dancer_x") != () { if Board.registry.get("dancer_x") != () {
let new_x = Board.registry.get("dancer_x"); let new_x = Board.registry.get("dancer_x");
@@ -58,14 +58,14 @@ fn dance(me) {
} }
// Fires when something steps into this object; id is the bumper's object id, // Fires when something steps into this object; id is the bumper's object id,
// or -1 for the player. // or -1 for the player.
fn bump(me, state, id) { fn bump(me, id) {
log(`mover bumped by ${id}`); log(`mover bumped by ${id}`);
say("Ow!"); say("Ow!");
} }
""" """
muffin = """ muffin = """
fn bump(me, state, _id) { fn bump(me, _id) {
scroll([ scroll([
"You find a small muffin on the ground, your favorite kind.", "You find a small muffin on the ground, your favorite kind.",
"It smells of cinnamon and warm mornings.", "It smells of cinnamon and warm mornings.",
@@ -73,7 +73,7 @@ fn bump(me, state, _id) {
["ignore", "This is obviously a trap — ignore it."] ["ignore", "This is obviously a trap — ignore it."]
]); ]);
} }
fn eat(me, state) { fn eat(me) {
say("Yeah it was poisoned."); say("Yeah it was poisoned.");
alter_health(-2); alter_health(-2);
} }
@@ -84,7 +84,7 @@ fn ignore() {
""" """
noticeboard = """ noticeboard = """
fn bump(me, state, id) { fn bump(me, id) {
scroll([ scroll([
" TOWN NOTICE BOARD", " TOWN NOTICE BOARD",
" ", " ",
@@ -117,7 +117,7 @@ fn bump(me, state, id) {
""" """
bookshelf = """ bookshelf = """
fn bump(me, state, id) { fn bump(me, id) {
scroll([ scroll([
" THE BOOKSHELF", " THE BOOKSHELF",
" ", " ",
@@ -136,13 +136,13 @@ fn bump(me, state, id) {
""" """
fireplace = """ fireplace = """
fn bump(me, state, id) { fn bump(me, id) {
say("The fire crackles warmly.\\nYou feel at ease."); say("The fire crackles warmly.\\nYou feel at ease.");
} }
""" """
chest = """ chest = """
fn bump(me, state, id) { fn bump(me, id) {
scroll([ scroll([
" THE CHEST", " THE CHEST",
" ", " ",
@@ -160,7 +160,7 @@ fn bump(me, state, id) {
""" """
yammerer = """ yammerer = """
fn tick(me, state, dt) { fn tick(me, dt) {
if !me.waiting { if !me.waiting {
say("blahblahblah"); say("blahblahblah");
delay(1); delay(1);
@@ -169,7 +169,7 @@ fn tick(me, state, dt) {
""" """
shifter = """ shifter = """
fn tick(me, state, dt) { fn tick(me, dt) {
let x = me.x; let x = me.x;
let y = me.y; let y = me.y;