removed queue.push
This commit is contained in:
@@ -87,6 +87,12 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- `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>`. 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. `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`). 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>`. 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. `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`). 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:
|
||||||
|
- `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).
|
||||||
|
- `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>)`.
|
||||||
|
- `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` emits `type` only (lines are not inspectable via the map API). `Log` is flattened to its first span's text.
|
||||||
|
|
||||||
**`kiln-core/src/floor.rs`** — the visual floor layer:
|
**`kiln-core/src/floor.rs`** — the visual floor layer:
|
||||||
- The floor is a cosmetic per-cell background drawn wherever a cell would render as `Archetype::Empty` (it is *how* `Empty` is drawn; the cell's own `Empty` glyph is ignored). Computed once at load and cached on `Board::floor`, so there is no per-frame cost / flicker; the raw `FloorSpec` is also kept (`Board::floor_spec`) so `map_file::save` round-trips it.
|
- The floor is a cosmetic per-cell background drawn wherever a cell would render as `Archetype::Empty` (it is *how* `Empty` is drawn; the cell's own `Empty` glyph is ignored). Computed once at load and cached on `Board::floor`, so there is no per-frame cost / flicker; the raw `FloorSpec` is also kept (`Board::floor_spec`) so `map_file::save` round-trips it.
|
||||||
- `FloorGenerator` (`Grass`/`Dirt`/`Stone`) — procedural textures differing only in color scheme and texture-char probability. `from_name(&str)` parses the map-file name; `generate(&mut StdRand)` picks a dark, low-saturation ground color (green/brown/gray) and, with the generator's probability, scatters a lighter texture char (grass `, . \` '`; dirt `. : , ;`; stone `. ,`). Uses `tinyrand` (already a workspace dep, WASM-safe) seeded with a fixed `FLOOR_SEED` for deterministic, testable output.
|
- `FloorGenerator` (`Grass`/`Dirt`/`Stone`) — procedural textures differing only in color scheme and texture-char probability. `from_name(&str)` parses the map-file name; `generate(&mut StdRand)` picks a dark, low-saturation ground color (green/brown/gray) and, with the generator's probability, scatters a lighter texture char (grass `, . \` '`; dirt `. : , ;`; stone `. ,`). Uses `tinyrand` (already a workspace dep, WASM-safe) seeded with a fixed `FLOOR_SEED` for deterministic, testable output.
|
||||||
@@ -101,7 +107,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- Lifecycle hooks per object: `init()` (zero-arg), `tick(dt)` (elapsed seconds as `f64`), and `bump(id)` (the bumper's `ObjectId`, or `-1` for the player), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)` / `run_bump(object_id, bumper)`; 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`), and `bump(id)` (the bumper's `ObjectId`, or `-1` for the player), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)` / `run_bump(object_id, bumper)`; 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). `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`. `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). `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)` append an `Action` (`Move`/`SetTile`/`Log`/`Say`/`Scroll`; `MOVE_COST = 0.25` s for `Move`, else 0) 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). `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)` append an `Action` (`Move`/`SetTile`/`Log`/`Say`/`Scroll`; `MOVE_COST = 0.25` s for `Move`, else 0) 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). `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()` and `Queue.clear()`. 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.
|
||||||
- `GameState` (hence the `Engine`/`Scope`) is single-threaded / not `Send`; fine for kiln-tui.
|
- `GameState` (hence the `Engine`/`Scope`) is single-threaded / not `Send`; fine for kiln-tui.
|
||||||
- **Sender identity uses stable ids:** `BoardAction.source`, `ObjectRuntime.object_id`, and the `QueueMap` keys are all the object's `ObjectId` (matching `Board::objects`' `BTreeMap` keys), so they survive object spawn/destroy/reorder. The per-call tag carries the id as an `i64` (id 0 — never valid — is the no-source fallback). `ScriptHost::objects` is still a `Vec<ObjectRuntime>` built by iterating the board map, so it stays in ascending-id order.
|
- **Sender identity uses stable ids:** `BoardAction.source`, `ObjectRuntime.object_id`, and the `QueueMap` keys are all the object's `ObjectId` (matching `Board::objects`' `BTreeMap` keys), so they survive object spawn/destroy/reorder. The per-call tag carries the id as an `i64` (id 0 — never valid — is the no-source fallback). `ScriptHost::objects` is still a `Vec<ObjectRuntime>` built by iterating the board map, so it stays in ascending-id order.
|
||||||
|
|||||||
+21
-277
@@ -1,30 +1,14 @@
|
|||||||
//! The [`Action`] enum and its Rhai-facing conversions.
|
//! The [`Action`] enum and its Rhai-facing conversions.
|
||||||
//!
|
//!
|
||||||
//! This module owns the `Action` type (which scripts enqueue via write host
|
//! This module owns the `Action` type (which scripts enqueue via write host
|
||||||
//! functions), the rate-limiting delay constant, and the Rhai bridge that
|
//! functions), the rate-limiting delay constant, and [`action_to_map`] which
|
||||||
//! lets scripts introspect and compose action queues via
|
//! converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`.
|
||||||
//! `Queue.peek()`/`Queue.pop()`/`Queue.push()`.
|
|
||||||
//!
|
|
||||||
//! ## Rhai `"Action"` module
|
|
||||||
//!
|
|
||||||
//! [`rhai_action_module`] builds a static Rhai module registered as `"Action"`
|
|
||||||
//! on the engine. Scripts can import it and call constructor functions:
|
|
||||||
//!
|
|
||||||
//! ```rhai
|
|
||||||
//! import "Action" as A;
|
|
||||||
//! Queue.push(A::Move(North));
|
|
||||||
//! Queue.push(A::Say("Hello!"));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Constructors return Rhai maps (`#{ type: "...", ... }`). [`action_to_map`]
|
|
||||||
//! converts in the same direction for `Queue.peek()` / `Queue.pop()`.
|
|
||||||
//! [`TryFrom<rhai::Map>`] converts back, used by `Queue.push()`.
|
|
||||||
|
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::map_file::{color_to_hex, parse_color};
|
use crate::map_file::color_to_hex;
|
||||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use rhai::{Dynamic, ImmutableString, Module};
|
use rhai::Dynamic;
|
||||||
|
|
||||||
/// How long a move occupies an object before it can act again, in seconds.
|
/// How long a move occupies an object before it can act again, in seconds.
|
||||||
pub(crate) const MOVE_COST: f64 = 0.25;
|
pub(crate) const MOVE_COST: f64 = 0.25;
|
||||||
@@ -70,7 +54,7 @@ pub(crate) enum Action {
|
|||||||
Scroll(Vec<ScrollLine>),
|
Scroll(Vec<ScrollLine>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Direction helpers ─────────────────────────────────────────────────────────
|
// ── Direction helper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn dir_to_str(d: Direction) -> &'static str {
|
fn dir_to_str(d: Direction) -> &'static str {
|
||||||
match d {
|
match d {
|
||||||
@@ -81,59 +65,51 @@ fn dir_to_str(d: Direction) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn str_to_dir(s: &str) -> Option<Direction> {
|
|
||||||
match s {
|
|
||||||
"North" => Some(Direction::North),
|
|
||||||
"South" => Some(Direction::South),
|
|
||||||
"East" => Some(Direction::East),
|
|
||||||
"West" => Some(Direction::West),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── action_to_map ─────────────────────────────────────────────────────────────
|
// ── action_to_map ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Converts an [`Action`] to the Rhai map form used by `Queue.peek()` / `Queue.pop()`.
|
/// Converts an [`Action`] to a Rhai map for `Queue.peek()` / `Queue.pop()`.
|
||||||
///
|
///
|
||||||
/// The map always has a `"type"` key naming the variant; other keys carry the
|
/// The map always has a `"type"` key naming the variant; other keys carry the
|
||||||
/// payload. [`Action::Log`] is flattened to its first span's text.
|
/// payload. [`Action::Log`] is flattened to its first span's text.
|
||||||
pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||||
|
// Dynamic::from(String) avoids rhai 1.x's From<&str> lifetime restriction.
|
||||||
|
let ds = |v: &'static str| Dynamic::from(v.to_string());
|
||||||
let mut m = rhai::Map::new();
|
let mut m = rhai::Map::new();
|
||||||
match action {
|
match action {
|
||||||
Action::Move(dir) => {
|
Action::Move(dir) => {
|
||||||
ins_str(&mut m, "type", "Move");
|
m.insert("type".into(), ds("Move"));
|
||||||
ins_str(&mut m, "dir", dir_to_str(*dir));
|
m.insert("dir".into(), ds(dir_to_str(*dir)));
|
||||||
}
|
}
|
||||||
Action::SetTile(n) => {
|
Action::SetTile(n) => {
|
||||||
ins_str(&mut m, "type", "SetTile");
|
m.insert("type".into(), ds("SetTile"));
|
||||||
m.insert("tile".into(), Dynamic::from(*n as i64));
|
m.insert("tile".into(), Dynamic::from(*n as i64));
|
||||||
}
|
}
|
||||||
Action::Log(line) => {
|
Action::Log(line) => {
|
||||||
let text = line.spans.first().map(|s| s.text.as_str()).unwrap_or("").to_string();
|
let text = line.spans.first().map(|s| s.text.as_str()).unwrap_or("").to_string();
|
||||||
ins_str(&mut m, "type", "Log");
|
m.insert("type".into(), ds("Log"));
|
||||||
m.insert("msg".into(), Dynamic::from(text));
|
m.insert("msg".into(), Dynamic::from(text));
|
||||||
}
|
}
|
||||||
Action::SetTag { target, tag, present } => {
|
Action::SetTag { target, tag, present } => {
|
||||||
ins_str(&mut m, "type", "SetTag");
|
m.insert("type".into(), ds("SetTag"));
|
||||||
m.insert("target".into(), Dynamic::from(*target as i64));
|
m.insert("target".into(), Dynamic::from(*target as i64));
|
||||||
m.insert("tag".into(), Dynamic::from(tag.clone()));
|
m.insert("tag".into(), Dynamic::from(tag.clone()));
|
||||||
m.insert("present".into(), Dynamic::from(*present));
|
m.insert("present".into(), Dynamic::from(*present));
|
||||||
}
|
}
|
||||||
Action::Say(text) => {
|
Action::Say(text) => {
|
||||||
ins_str(&mut m, "type", "Say");
|
m.insert("type".into(), ds("Say"));
|
||||||
m.insert("msg".into(), Dynamic::from(text.clone()));
|
m.insert("msg".into(), Dynamic::from(text.clone()));
|
||||||
}
|
}
|
||||||
Action::Delay(secs) => {
|
Action::Delay(secs) => {
|
||||||
ins_str(&mut m, "type", "Delay");
|
m.insert("type".into(), ds("Delay"));
|
||||||
m.insert("secs".into(), Dynamic::from(*secs));
|
m.insert("secs".into(), Dynamic::from(*secs));
|
||||||
}
|
}
|
||||||
Action::SetColor { fg, bg } => {
|
Action::SetColor { fg, bg } => {
|
||||||
ins_str(&mut m, "type", "SetColor");
|
m.insert("type".into(), ds("SetColor"));
|
||||||
if let Some(c) = fg { m.insert("fg".into(), Dynamic::from(color_to_hex(*c))); }
|
if let Some(c) = fg { m.insert("fg".into(), Dynamic::from(color_to_hex(*c))); }
|
||||||
if let Some(c) = bg { m.insert("bg".into(), Dynamic::from(color_to_hex(*c))); }
|
if let Some(c) = bg { m.insert("bg".into(), Dynamic::from(color_to_hex(*c))); }
|
||||||
}
|
}
|
||||||
Action::Send { target, fn_name, arg } => {
|
Action::Send { target, fn_name, arg } => {
|
||||||
ins_str(&mut m, "type", "Send");
|
m.insert("type".into(), ds("Send"));
|
||||||
m.insert("target".into(), Dynamic::from(*target as i64));
|
m.insert("target".into(), Dynamic::from(*target as i64));
|
||||||
m.insert("name".into(), Dynamic::from(fn_name.clone()));
|
m.insert("name".into(), Dynamic::from(fn_name.clone()));
|
||||||
if let Some(a) = arg {
|
if let Some(a) = arg {
|
||||||
@@ -144,242 +120,10 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
|||||||
m.insert("arg".into(), dyn_arg);
|
m.insert("arg".into(), dyn_arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scroll lines are not round-trippable via the queue map API; emit type only.
|
// Scroll lines are not inspectable via the queue map API; emit type only.
|
||||||
Action::Scroll(_) => {
|
Action::Scroll(_) => {
|
||||||
ins_str(&mut m, "type", "Scroll");
|
m.insert("type".into(), ds("Scroll"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TryFrom<rhai::Map> ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
impl TryFrom<rhai::Map> for Action {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
/// Converts a Rhai map (as produced by the `"Action"` module constructors or
|
|
||||||
/// [`action_to_map`]) back into a Rust [`Action`]. Returns `Err(msg)` when
|
|
||||||
/// the map is structurally invalid or names an unknown variant.
|
|
||||||
fn try_from(map: rhai::Map) -> Result<Self, Self::Error> {
|
|
||||||
let type_str = map
|
|
||||||
.get("type")
|
|
||||||
.ok_or_else(|| "missing 'type' key".to_string())?
|
|
||||||
.clone()
|
|
||||||
.into_string()
|
|
||||||
.map_err(|_| "'type' must be a string".to_string())?;
|
|
||||||
|
|
||||||
match type_str.as_str() {
|
|
||||||
"Move" => {
|
|
||||||
let dir_str = get_str(&map, "dir", "Move")?;
|
|
||||||
let dir = str_to_dir(&dir_str)
|
|
||||||
.ok_or_else(|| format!("Move: unknown direction '{dir_str}'"))?;
|
|
||||||
Ok(Action::Move(dir))
|
|
||||||
}
|
|
||||||
"SetTile" => {
|
|
||||||
let n = get_int(&map, "tile", "SetTile")?;
|
|
||||||
Ok(Action::SetTile(n as u32))
|
|
||||||
}
|
|
||||||
"Log" => {
|
|
||||||
let msg = get_str(&map, "msg", "Log")?;
|
|
||||||
Ok(Action::Log(LogLine::raw(msg)))
|
|
||||||
}
|
|
||||||
"Say" => {
|
|
||||||
let msg = get_str(&map, "msg", "Say")?;
|
|
||||||
Ok(Action::Say(msg))
|
|
||||||
}
|
|
||||||
"Delay" => {
|
|
||||||
let secs = get_float(&map, "secs", "Delay")?;
|
|
||||||
Ok(Action::Delay(secs))
|
|
||||||
}
|
|
||||||
"SetTag" => {
|
|
||||||
let target = get_int(&map, "target", "SetTag")?;
|
|
||||||
let tag = get_str(&map, "tag", "SetTag")?;
|
|
||||||
let present = get_bool(&map, "present", "SetTag")?;
|
|
||||||
Ok(Action::SetTag { target: target as ObjectId, tag, present })
|
|
||||||
}
|
|
||||||
"SetColor" => {
|
|
||||||
let fg = opt_color(&map, "fg", "SetColor")?;
|
|
||||||
let bg = opt_color(&map, "bg", "SetColor")?;
|
|
||||||
Ok(Action::SetColor { fg, bg })
|
|
||||||
}
|
|
||||||
"Send" => {
|
|
||||||
let target = get_int(&map, "target", "Send")?;
|
|
||||||
let fn_name = get_str(&map, "name", "Send")?;
|
|
||||||
// Accept int, float, or string as the optional arg.
|
|
||||||
let arg = if let Some(v) = map.get("arg") {
|
|
||||||
let v = v.clone();
|
|
||||||
if let Ok(n) = v.clone().as_int() {
|
|
||||||
Some(ScriptArg::Num(n as f64))
|
|
||||||
} else if let Ok(f) = v.clone().as_float() {
|
|
||||||
Some(ScriptArg::Num(f))
|
|
||||||
} else if let Ok(s) = v.into_string() {
|
|
||||||
Some(ScriptArg::Str(s))
|
|
||||||
} else {
|
|
||||||
None // unrecognised arg type — ignored
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(Action::Send { target: target as ObjectId, fn_name, arg })
|
|
||||||
}
|
|
||||||
"Scroll" => Err("Scroll actions cannot be constructed from maps".to_string()),
|
|
||||||
other => Err(format!("unknown action type: '{other}'")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Rhai "Action" module ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Builds the `"Action"` Rhai static module.
|
|
||||||
///
|
|
||||||
/// Register on the engine with
|
|
||||||
/// `engine.register_static_module("Action", rhai_action_module().into())`.
|
|
||||||
/// Scripts can then write `import "Action" as A; Queue.push(A::Move(North));`.
|
|
||||||
pub(crate) fn rhai_action_module() -> Module {
|
|
||||||
let mut m = Module::new();
|
|
||||||
|
|
||||||
m.set_native_fn("Move", |dir: Direction| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Move");
|
|
||||||
ins_str(&mut map, "dir", dir_to_str(dir));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m.set_native_fn("Delay", |secs: f64| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Delay");
|
|
||||||
map.insert("secs".into(), Dynamic::from(secs));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
// Integer overload so A::Delay(2) works as well as A::Delay(2.0).
|
|
||||||
m.set_native_fn("Delay", |secs: i64| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Delay");
|
|
||||||
map.insert("secs".into(), Dynamic::from(secs as f64));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m.set_native_fn("SetTile", |n: i64| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "SetTile");
|
|
||||||
map.insert("tile".into(), Dynamic::from(n));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m.set_native_fn("Log", |msg: ImmutableString| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Log");
|
|
||||||
map.insert("msg".into(), Dynamic::from(msg));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m.set_native_fn("Say", |msg: ImmutableString| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Say");
|
|
||||||
map.insert("msg".into(), Dynamic::from(msg));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
// SetColor — fg only
|
|
||||||
m.set_native_fn("SetColor", |fg: ImmutableString| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "SetColor");
|
|
||||||
map.insert("fg".into(), Dynamic::from(fg));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
// SetColor — fg + bg
|
|
||||||
m.set_native_fn("SetColor", |fg: ImmutableString, bg: ImmutableString| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "SetColor");
|
|
||||||
map.insert("fg".into(), Dynamic::from(fg));
|
|
||||||
map.insert("bg".into(), Dynamic::from(bg));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m.set_native_fn("SetTag", |target: i64, tag: ImmutableString, present: bool| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "SetTag");
|
|
||||||
map.insert("target".into(), Dynamic::from(target));
|
|
||||||
map.insert("tag".into(), Dynamic::from(tag));
|
|
||||||
map.insert("present".into(), Dynamic::from(present));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send — no arg
|
|
||||||
m.set_native_fn("Send", |target: i64, name: ImmutableString| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Send");
|
|
||||||
map.insert("target".into(), Dynamic::from(target));
|
|
||||||
map.insert("name".into(), Dynamic::from(name));
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send — with arg
|
|
||||||
m.set_native_fn("Send", |target: i64, name: ImmutableString, arg: Dynamic| {
|
|
||||||
let mut map = rhai::Map::new();
|
|
||||||
ins_str(&mut map, "type", "Send");
|
|
||||||
map.insert("target".into(), Dynamic::from(target));
|
|
||||||
map.insert("name".into(), Dynamic::from(name));
|
|
||||||
map.insert("arg".into(), arg);
|
|
||||||
Ok(Dynamic::from(map))
|
|
||||||
});
|
|
||||||
|
|
||||||
m
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Small helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Inserts a `&'static str` value into a [`rhai::Map`] by converting via `String`.
|
|
||||||
/// Using `String` avoids the `From<&str>` lifetime restriction in rhai 1.x.
|
|
||||||
fn ins_str(m: &mut rhai::Map, key: &'static str, value: &'static str) {
|
|
||||||
m.insert(key.into(), Dynamic::from(value.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_str(map: &rhai::Map, key: &str, ctx: &str) -> Result<String, String> {
|
|
||||||
map.get(key)
|
|
||||||
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
|
|
||||||
.clone()
|
|
||||||
.into_string()
|
|
||||||
.map_err(|_| format!("{ctx}: '{key}' must be a string"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_int(map: &rhai::Map, key: &str, ctx: &str) -> Result<i64, String> {
|
|
||||||
map.get(key)
|
|
||||||
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
|
|
||||||
.clone()
|
|
||||||
.as_int()
|
|
||||||
.map_err(|_| format!("{ctx}: '{key}' must be an integer"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_float(map: &rhai::Map, key: &str, ctx: &str) -> Result<f64, String> {
|
|
||||||
let v = map.get(key)
|
|
||||||
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
|
|
||||||
.clone();
|
|
||||||
// Accept an integer literal passed where a float is expected.
|
|
||||||
v.clone()
|
|
||||||
.as_float()
|
|
||||||
.or_else(|_| v.as_int().map(|n| n as f64))
|
|
||||||
.map_err(|_| format!("{ctx}: '{key}' must be a number"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bool(map: &rhai::Map, key: &str, ctx: &str) -> Result<bool, String> {
|
|
||||||
map.get(key)
|
|
||||||
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
|
|
||||||
.clone()
|
|
||||||
.as_bool()
|
|
||||||
.map_err(|_| format!("{ctx}: '{key}' must be a bool"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `Some(Rgba8)` if `key` is present in the map, or `None` if absent.
|
|
||||||
fn opt_color(map: &rhai::Map, key: &str, ctx: &str) -> Result<Option<Rgba8>, String> {
|
|
||||||
match map.get(key) {
|
|
||||||
None => Ok(None),
|
|
||||||
Some(v) => {
|
|
||||||
let s = v.clone()
|
|
||||||
.into_string()
|
|
||||||
.map_err(|_| format!("{ctx}: '{key}' must be a string"))?;
|
|
||||||
Ok(Some(parse_color(&s)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-25
@@ -43,9 +43,9 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Queue API
|
//! ## Queue API
|
||||||
//!
|
//!
|
||||||
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`, `Queue.push(map)`
|
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`
|
||||||
|
|
||||||
use crate::action::{action_to_map, rhai_action_module, Action, ScrollLine, MOVE_COST};
|
use crate::action::{action_to_map, Action, ScrollLine, MOVE_COST};
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::map_file::{color_to_hex, parse_color};
|
use crate::map_file::{color_to_hex, parse_color};
|
||||||
@@ -173,10 +173,9 @@ impl ScriptHost {
|
|||||||
let errors: ErrorSink = Rc::new(RefCell::new(Vec::new()));
|
let errors: ErrorSink = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
engine.register_static_module("Action", rhai_action_module().into());
|
|
||||||
register_read_api(&mut engine, board_cell, &board_queue, &errors);
|
register_read_api(&mut engine, board_cell, &board_queue, &errors);
|
||||||
register_write_api(&mut engine, &queues);
|
register_write_api(&mut engine, &queues);
|
||||||
register_queue_api(&mut engine, &errors);
|
register_queue_api(&mut engine);
|
||||||
register_me_type(&mut engine);
|
register_me_type(&mut engine);
|
||||||
register_object_info_type(&mut engine);
|
register_object_info_type(&mut engine);
|
||||||
register_glyph_type(&mut engine);
|
register_glyph_type(&mut engine);
|
||||||
@@ -717,7 +716,7 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
|||||||
|
|
||||||
// ── Queue API ─────────────────────────────────────────────────────────────────
|
// ── Queue API ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn register_queue_api(engine: &mut Engine, errors: &ErrorSink) {
|
fn register_queue_api(engine: &mut Engine) {
|
||||||
engine.register_type_with_name::<ObjQueue>("Queue");
|
engine.register_type_with_name::<ObjQueue>("Queue");
|
||||||
engine.register_fn("length", |q: &mut ObjQueue| q.borrow().len() as i64);
|
engine.register_fn("length", |q: &mut ObjQueue| q.borrow().len() as i64);
|
||||||
engine.register_fn("clear", |q: &mut ObjQueue| q.borrow_mut().clear());
|
engine.register_fn("clear", |q: &mut ObjQueue| q.borrow_mut().clear());
|
||||||
@@ -738,26 +737,6 @@ fn register_queue_api(engine: &mut Engine, errors: &ErrorSink) {
|
|||||||
.map(|a| Dynamic::from(action_to_map(a)))
|
.map(|a| Dynamic::from(action_to_map(a)))
|
||||||
.unwrap_or(Dynamic::UNIT)
|
.unwrap_or(Dynamic::UNIT)
|
||||||
});
|
});
|
||||||
|
|
||||||
// push(map): parse the map into an Action and enqueue; log an error on failure.
|
|
||||||
let errs = errors.clone();
|
|
||||||
// We need the queues map here to route push to the right object's queue.
|
|
||||||
// Since push() is called on the Queue handle directly, we use that handle.
|
|
||||||
engine.register_fn("push", move |q: &mut ObjQueue, map: Dynamic| {
|
|
||||||
let rhai_map = match map.try_cast::<rhai::Map>() {
|
|
||||||
Some(m) => m,
|
|
||||||
None => {
|
|
||||||
errs.borrow_mut().push(LogLine::error(
|
|
||||||
"Queue.push: argument must be an Action map".to_string(),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match Action::try_from(rhai_map) {
|
|
||||||
Ok(action) => q.borrow_mut().push_back(action),
|
|
||||||
Err(msg) => errs.borrow_mut().push(LogLine::error(format!("Queue.push: {msg}"))),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scope construction ────────────────────────────────────────────────────────
|
// ── Scope construction ────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user