2026-06-09 22:38:17 -05:00
|
|
|
//! The [`Action`] enum and its Rhai-facing conversions.
|
|
|
|
|
//!
|
|
|
|
|
//! This module owns the `Action` type (which scripts enqueue via write host
|
2026-06-13 01:47:11 -05:00
|
|
|
//! functions), the rate-limiting delay constant, and [`action_to_map`] which
|
|
|
|
|
//! converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`.
|
2026-06-09 22:38:17 -05:00
|
|
|
|
|
|
|
|
use crate::log::LogLine;
|
2026-06-13 01:47:11 -05:00
|
|
|
use crate::map_file::color_to_hex;
|
2026-06-09 22:38:17 -05:00
|
|
|
use crate::utils::{Direction, ObjectId, ScriptArg};
|
|
|
|
|
use color::Rgba8;
|
2026-06-13 01:47:11 -05:00
|
|
|
use rhai::Dynamic;
|
2026-06-09 22:38:17 -05:00
|
|
|
|
|
|
|
|
/// How long a move occupies an object before it can act again, in seconds.
|
|
|
|
|
pub(crate) const MOVE_COST: f64 = 0.25;
|
|
|
|
|
|
2026-06-10 01:42:33 -05:00
|
|
|
/// One line of content in a [`Action::Scroll`] overlay.
|
|
|
|
|
///
|
|
|
|
|
/// Each element of the array passed to the `scroll()` Rhai function becomes a
|
|
|
|
|
/// `ScrollLine`: a plain string becomes [`Text`](ScrollLine::Text); a two-element
|
|
|
|
|
/// array `[choice, display]` becomes a [`Choice`](ScrollLine::Choice).
|
2026-06-15 09:15:30 -05:00
|
|
|
#[derive(Clone)]
|
2026-06-10 01:42:33 -05:00
|
|
|
pub enum ScrollLine {
|
|
|
|
|
/// Plain text, word-wrapped to the scroll window width.
|
|
|
|
|
Text(String),
|
|
|
|
|
/// A selectable option: `choice` is the event name sent back to the source
|
|
|
|
|
/// object via `send`, and `display` is the label shown to the player.
|
|
|
|
|
Choice { choice: String, display: String },
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 22:38:17 -05:00
|
|
|
/// One deferred mutation emitted by a script, applied by [`crate::game::GameState`]
|
|
|
|
|
/// after it is promoted onto the board queue.
|
|
|
|
|
///
|
|
|
|
|
/// [`Action::Delay`] is the exception: it is **never** promoted to the board queue.
|
|
|
|
|
/// It lives only in the per-object output queue and is consumed by `ScriptHost::drain`
|
|
|
|
|
/// to pace how quickly other actions are released.
|
|
|
|
|
pub(crate) enum Action {
|
|
|
|
|
/// Move the source object one cell in a direction (subject to passability).
|
|
|
|
|
Move(Direction),
|
|
|
|
|
/// Set the source object's glyph tile index.
|
|
|
|
|
SetTile(u32),
|
|
|
|
|
/// Append a styled line to the game log.
|
|
|
|
|
Log(LogLine),
|
|
|
|
|
/// Add (`present = true`) or remove (`present = false`) `tag` on `target`.
|
2026-06-15 23:35:18 -05:00
|
|
|
SetTag {
|
|
|
|
|
target: ObjectId,
|
|
|
|
|
tag: String,
|
|
|
|
|
present: bool,
|
|
|
|
|
},
|
2026-06-13 15:33:04 -05:00
|
|
|
/// Display a speech bubble above the source object for `duration` seconds.
|
|
|
|
|
Say(String, f64),
|
2026-06-09 22:38:17 -05:00
|
|
|
/// Pause draining this object's queue for `secs` seconds. Never reaches the board queue.
|
|
|
|
|
Delay(f64),
|
|
|
|
|
/// Set the source object's foreground and/or background color.
|
2026-06-15 23:35:18 -05:00
|
|
|
SetColor {
|
|
|
|
|
fg: Option<Rgba8>,
|
|
|
|
|
bg: Option<Rgba8>,
|
|
|
|
|
},
|
2026-06-09 22:38:17 -05:00
|
|
|
/// Call `fn_name` on `target` object, optionally passing `arg`.
|
2026-06-15 23:35:18 -05:00
|
|
|
Send {
|
|
|
|
|
target: ObjectId,
|
|
|
|
|
fn_name: String,
|
|
|
|
|
arg: Option<ScriptArg>,
|
|
|
|
|
},
|
2026-06-10 01:42:33 -05:00
|
|
|
/// Open a scrollable text overlay. Pauses game ticks while shown; a choice
|
|
|
|
|
/// selection dispatches [`Send`](Action::Send) to the source object.
|
|
|
|
|
Scroll(Vec<ScrollLine>),
|
2026-06-13 17:58:04 -05:00
|
|
|
/// Teleport the source object to `(x, y)`. Blocked (with a logged error) if
|
|
|
|
|
/// the source is solid and the destination already has a solid occupant.
|
|
|
|
|
/// Zero time cost — multiple teleports may fire in one frame.
|
|
|
|
|
Teleport { x: i32, y: i32 },
|
2026-06-21 01:32:47 -05:00
|
|
|
/// Shove the pushable chain starting at `(x, y)` one step in `dir` (acts on
|
|
|
|
|
/// arbitrary cells, not the source object). Zero time cost.
|
|
|
|
|
Push { x: i32, y: i32, dir: Direction },
|
2026-06-21 22:04:10 -05:00
|
|
|
/// Shift a set of cells, given as `(x, y)` coordinates: each cell moves to the
|
|
|
|
|
/// next coordinate in the list, unless it can't move, or that cell is blocked.
|
|
|
|
|
/// Rotates the contents of the last cell back to the beginning.
|
|
|
|
|
/// Zero time cost.
|
|
|
|
|
Shift(Vec<(i32, i32)>),
|
2026-06-21 18:27:45 -05:00
|
|
|
/// Add `n` to the player's gem count (negative subtracts; the count is
|
|
|
|
|
/// clamped at 0). Zero time cost. Applied to `GameState::player_gems`.
|
|
|
|
|
AddGems(i64),
|
|
|
|
|
/// Remove the source object from the board. Zero time cost. Used by grab
|
|
|
|
|
/// things (e.g. gems) to despawn themselves from their `grab()` hook.
|
|
|
|
|
Die,
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 01:47:11 -05:00
|
|
|
// ── Direction helper ──────────────────────────────────────────────────────────
|
2026-06-09 22:38:17 -05:00
|
|
|
|
|
|
|
|
fn dir_to_str(d: Direction) -> &'static str {
|
|
|
|
|
match d {
|
|
|
|
|
Direction::North => "North",
|
|
|
|
|
Direction::South => "South",
|
2026-06-15 23:35:18 -05:00
|
|
|
Direction::East => "East",
|
|
|
|
|
Direction::West => "West",
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── action_to_map ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-13 01:47:11 -05:00
|
|
|
/// Converts an [`Action`] to a Rhai map for `Queue.peek()` / `Queue.pop()`.
|
2026-06-09 22:38:17 -05:00
|
|
|
///
|
|
|
|
|
/// 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.
|
|
|
|
|
pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
2026-06-13 01:47:11 -05:00
|
|
|
// Dynamic::from(String) avoids rhai 1.x's From<&str> lifetime restriction.
|
|
|
|
|
let ds = |v: &'static str| Dynamic::from(v.to_string());
|
2026-06-09 22:38:17 -05:00
|
|
|
let mut m = rhai::Map::new();
|
|
|
|
|
match action {
|
|
|
|
|
Action::Move(dir) => {
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("Move"));
|
2026-06-15 23:35:18 -05:00
|
|
|
m.insert("dir".into(), ds(dir_to_str(*dir)));
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
Action::SetTile(n) => {
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("SetTile"));
|
2026-06-09 22:38:17 -05:00
|
|
|
m.insert("tile".into(), Dynamic::from(*n as i64));
|
|
|
|
|
}
|
|
|
|
|
Action::Log(line) => {
|
2026-06-15 23:35:18 -05:00
|
|
|
let text = line
|
|
|
|
|
.spans
|
|
|
|
|
.first()
|
|
|
|
|
.map(|s| s.text.as_str())
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string();
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("Log"));
|
2026-06-15 23:35:18 -05:00
|
|
|
m.insert("msg".into(), Dynamic::from(text));
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
2026-06-15 23:35:18 -05:00
|
|
|
Action::SetTag {
|
|
|
|
|
target,
|
|
|
|
|
tag,
|
|
|
|
|
present,
|
|
|
|
|
} => {
|
|
|
|
|
m.insert("type".into(), ds("SetTag"));
|
|
|
|
|
m.insert("target".into(), Dynamic::from(*target as i64));
|
|
|
|
|
m.insert("tag".into(), Dynamic::from(tag.clone()));
|
2026-06-09 22:38:17 -05:00
|
|
|
m.insert("present".into(), Dynamic::from(*present));
|
|
|
|
|
}
|
2026-06-13 15:33:04 -05:00
|
|
|
Action::Say(text, dur) => {
|
2026-06-15 23:35:18 -05:00
|
|
|
m.insert("type".into(), ds("Say"));
|
|
|
|
|
m.insert("msg".into(), Dynamic::from(text.clone()));
|
2026-06-13 15:33:04 -05:00
|
|
|
m.insert("duration".into(), Dynamic::from(*dur));
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
Action::Delay(secs) => {
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("Delay"));
|
2026-06-09 22:38:17 -05:00
|
|
|
m.insert("secs".into(), Dynamic::from(*secs));
|
|
|
|
|
}
|
|
|
|
|
Action::SetColor { fg, bg } => {
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("SetColor"));
|
2026-06-15 23:35:18 -05:00
|
|
|
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)));
|
|
|
|
|
}
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
2026-06-15 23:35:18 -05:00
|
|
|
Action::Send {
|
|
|
|
|
target,
|
|
|
|
|
fn_name,
|
|
|
|
|
arg,
|
|
|
|
|
} => {
|
|
|
|
|
m.insert("type".into(), ds("Send"));
|
2026-06-09 22:38:17 -05:00
|
|
|
m.insert("target".into(), Dynamic::from(*target as i64));
|
2026-06-15 23:35:18 -05:00
|
|
|
m.insert("name".into(), Dynamic::from(fn_name.clone()));
|
2026-06-09 22:38:17 -05:00
|
|
|
if let Some(a) = arg {
|
|
|
|
|
let dyn_arg = match a {
|
|
|
|
|
ScriptArg::Str(s) => Dynamic::from(s.clone()),
|
|
|
|
|
ScriptArg::Num(n) => Dynamic::from(*n),
|
|
|
|
|
};
|
|
|
|
|
m.insert("arg".into(), dyn_arg);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-13 01:47:11 -05:00
|
|
|
// Scroll lines are not inspectable via the queue map API; emit type only.
|
2026-06-10 01:42:33 -05:00
|
|
|
Action::Scroll(_) => {
|
2026-06-13 01:47:11 -05:00
|
|
|
m.insert("type".into(), ds("Scroll"));
|
2026-06-10 01:42:33 -05:00
|
|
|
}
|
2026-06-13 17:58:04 -05:00
|
|
|
Action::Teleport { x, y } => {
|
|
|
|
|
m.insert("type".into(), ds("Teleport"));
|
|
|
|
|
m.insert("x".into(), Dynamic::from(*x as i64));
|
|
|
|
|
m.insert("y".into(), Dynamic::from(*y as i64));
|
|
|
|
|
}
|
2026-06-21 01:32:47 -05:00
|
|
|
Action::Push { x, y, dir } => {
|
|
|
|
|
m.insert("type".into(), ds("Push"));
|
|
|
|
|
m.insert("x".into(), Dynamic::from(*x as i64));
|
|
|
|
|
m.insert("y".into(), Dynamic::from(*y as i64));
|
|
|
|
|
m.insert("dir".into(), ds(dir_to_str(*dir)));
|
|
|
|
|
}
|
2026-06-21 22:04:10 -05:00
|
|
|
// Shift cells are not inspectable via the queue map API; emit type only.
|
|
|
|
|
Action::Shift(_) => {
|
|
|
|
|
m.insert("type".into(), ds("Shift"));
|
2026-06-21 01:32:47 -05:00
|
|
|
}
|
2026-06-21 18:27:45 -05:00
|
|
|
Action::AddGems(n) => {
|
|
|
|
|
m.insert("type".into(), ds("AddGems"));
|
|
|
|
|
m.insert("n".into(), Dynamic::from(*n));
|
|
|
|
|
}
|
|
|
|
|
Action::Die => {
|
|
|
|
|
m.insert("type".into(), ds("Die"));
|
|
|
|
|
}
|
2026-06-09 22:38:17 -05:00
|
|
|
}
|
|
|
|
|
m
|
|
|
|
|
}
|