Big API refactor
This commit is contained in:
@@ -9,3 +9,4 @@ rhai = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tinyrand = "0.5"
|
||||
toml = { version = "0.8", features = ["preserve_order"] }
|
||||
log = "0.4.33"
|
||||
|
||||
+79
-13
@@ -4,9 +4,10 @@
|
||||
//! functions), the rate-limiting delay constant, and [`action_to_map`] which
|
||||
//! converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use crate::log::LogLine;
|
||||
use crate::map_file::color_to_hex;
|
||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||
use crate::utils::{Direction, ObjectId};
|
||||
use color::Rgba8;
|
||||
use rhai::Dynamic;
|
||||
|
||||
@@ -27,13 +28,47 @@ pub enum ScrollLine {
|
||||
Choice { choice: String, display: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum SendArg {
|
||||
None,
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl From<Dynamic> for SendArg {
|
||||
fn from(value: Dynamic) -> Self {
|
||||
if value.is_int() {
|
||||
Self::Int(value.as_int().unwrap())
|
||||
} else if value.is_float() {
|
||||
Self::Float(value.as_float().unwrap())
|
||||
} else if value.is_string() {
|
||||
Self::String(value.into_string().unwrap())
|
||||
} else {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Dynamic> for SendArg {
|
||||
fn into(self) -> Dynamic {
|
||||
match self {
|
||||
SendArg::None => Dynamic::UNIT,
|
||||
SendArg::Int(value) => Dynamic::from(value),
|
||||
SendArg::Float(value) => Dynamic::from(value),
|
||||
SendArg::String(value) => Dynamic::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
#[derive(Clone)]
|
||||
pub enum Action {
|
||||
/// Move the source object one cell in a direction (subject to passability).
|
||||
Move(Direction),
|
||||
/// Set the source object's glyph tile index.
|
||||
@@ -59,7 +94,7 @@ pub(crate) enum Action {
|
||||
Send {
|
||||
target: ObjectId,
|
||||
fn_name: String,
|
||||
arg: Option<ScriptArg>,
|
||||
arg: SendArg,
|
||||
},
|
||||
/// Open a scrollable text overlay. Pauses game ticks while shown; a choice
|
||||
/// selection dispatches [`Send`](Action::Send) to the source object.
|
||||
@@ -67,15 +102,15 @@ pub(crate) enum Action {
|
||||
/// 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 },
|
||||
Teleport { x: i64, y: i64 },
|
||||
/// 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 },
|
||||
Push { x: i64, y: i64, dir: Direction },
|
||||
/// 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)>),
|
||||
Shift(Vec<(i64, i64)>),
|
||||
/// 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),
|
||||
@@ -93,6 +128,29 @@ pub(crate) enum Action {
|
||||
Die,
|
||||
}
|
||||
|
||||
impl Debug for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Action::Move(dir) => write!(f, "Move({:?})", dir),
|
||||
Action::SetTile(i) => write!(f, "SetTile({i})"),
|
||||
Action::Log(msg) => write!(f, "Log({:?})", msg),
|
||||
Action::SetTag { .. } => write!(f, "SetTag"),
|
||||
Action::Say(_, _) => write!(f, "Say"),
|
||||
Action::Delay(t) => write!(f, "Delay({t})"),
|
||||
Action::SetColor { .. } => write!(f, "SetColor"),
|
||||
Action::Send { .. } => write!(f, "Send"),
|
||||
Action::Scroll(_) => write!(f, "Scroll"),
|
||||
Action::Teleport { .. } => write!(f, "Teleport"),
|
||||
Action::Push { .. } => write!(f, "Push"),
|
||||
Action::Shift(_) => write!(f, "Shift"),
|
||||
Action::AddGems(n) => write!(f, "AddGems({n}"),
|
||||
Action::AlterHealth(health) => write!(f, "AlterHealth({health})"),
|
||||
Action::SetKey(_, _) => write!(f, "SetKey"),
|
||||
Action::Die => write!(f, "Die")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Direction helper ──────────────────────────────────────────────────────────
|
||||
|
||||
fn dir_to_str(d: Direction) -> &'static str {
|
||||
@@ -169,13 +227,12 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||
m.insert("type".into(), ds("Send"));
|
||||
m.insert("target".into(), Dynamic::from(*target as i64));
|
||||
m.insert("name".into(), Dynamic::from(fn_name.clone()));
|
||||
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);
|
||||
}
|
||||
match arg {
|
||||
SendArg::Int(i) => m.insert("arg".into(), Dynamic::from(*i)),
|
||||
SendArg::Float(f) => m.insert("arg".into(), Dynamic::from(*f)),
|
||||
SendArg::String(s) => m.insert("arg".into(), Dynamic::from(s.clone())),
|
||||
SendArg::None => m.insert("arg".into(), Dynamic::UNIT)
|
||||
};
|
||||
}
|
||||
// Scroll lines are not inspectable via the queue map API; emit type only.
|
||||
Action::Scroll(_) => {
|
||||
@@ -215,3 +272,12 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// An action promoted from an object's output queue onto the board queue, tagged
|
||||
/// with the object that issued it.
|
||||
pub struct BoardAction {
|
||||
/// The stable [`ObjectId`] of the object that issued the action.
|
||||
pub(crate) source: ObjectId,
|
||||
/// The action to apply.
|
||||
pub(crate) action: Action,
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! ## Board read API (`state.board.*`)
|
||||
//!
|
||||
//! - `board.tagged(tag) -> Array[ObjectInfo]` — objects carrying a tag
|
||||
//! - `board.named(name) -> ObjectInfo | ()` — object with that name (or unit)
|
||||
//! - `board.get(id) -> ObjectInfo | ()` — look up by id; `-1` returns player info
|
||||
//! - `board.width` - Board width
|
||||
//! - `board.height` - Board height
|
||||
//!
|
||||
//! ## Cell queries
|
||||
//!
|
||||
//! - `board.can_push(x, y, dir) -> bool` — is the pushable chain at `(x, y)` shovable in `dir`
|
||||
//! - `board.passable(x, y) -> bool` — is `(x, y)` on-board and free of any solid (a hole)
|
||||
//! cell (one empty, or a grab thing and the player)
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use rhai::{Dynamic, Engine, ImmutableString};
|
||||
use crate::{Board, Direction};
|
||||
use crate::api::object_info::ObjectInfo;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A read-only handle to the world, exposed to scripts as `Board`.
|
||||
pub type BoardRef = Rc<RefCell<Board>>;
|
||||
|
||||
impl Registerable for BoardRef {
|
||||
fn register(engine: &mut Engine, error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<BoardRef>("Board");
|
||||
engine.register_get("width", |b: &mut BoardRef| b.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardRef| b.borrow().height as i64);
|
||||
|
||||
// can_push(x, y, dir) — true if (x, y) holds a pushable whose chain can be
|
||||
// shoved one step in dir (the read-only half of push()). False off-board.
|
||||
engine.register_fn("can_push", move |board: BoardRef, x: i64, y: i64, dir: Direction| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.can_push(x as usize, y as usize, dir)
|
||||
});
|
||||
|
||||
// passable(x, y) — true if (x, y) is on-board and holds no solid (an empty cell
|
||||
// a mover could enter). Off-board is not passable. Lets a script distinguish a
|
||||
// hole from a blocked solid (which can_shift/can_push alone cannot).
|
||||
engine.register_fn("passable", move |board: BoardRef, x: i64, y: i64| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.is_passable(x as usize, y as usize)
|
||||
});
|
||||
|
||||
// Board.get(id) -> ObjectInfo | () (unknown id logs error)
|
||||
engine.register_fn("get", move |board: &mut BoardRef, id: i64| -> Dynamic {
|
||||
if id <= 0 {
|
||||
error_sink.error(format!("Board.get: invalid id {id}"));
|
||||
return Dynamic::UNIT;
|
||||
}
|
||||
|
||||
if let Some(obj) = ObjectInfo::from_id(id as ObjectId, board.clone()) {
|
||||
Dynamic::from(obj)
|
||||
} else {
|
||||
error_sink.error(format!("Board.get: no object with id {id}"));
|
||||
Dynamic::UNIT
|
||||
}
|
||||
});
|
||||
|
||||
// Board.named(name) -> ObjectInfo | ()
|
||||
engine.register_fn("named", move |board_ref: &mut BoardRef, name: ImmutableString| -> Dynamic {
|
||||
let board = board_ref.borrow();
|
||||
board
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(_id, def)| {
|
||||
if def.name.as_deref() == Some(name.as_str()) {
|
||||
Some(Dynamic::from(ObjectInfo::from_def(def, board_ref.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
},
|
||||
);
|
||||
|
||||
// Board.tagged(tag) -> Array[ObjectInfo]
|
||||
engine.register_fn("tagged", move |board_ref: &mut BoardRef, tag: ImmutableString| -> rhai::Array {
|
||||
let board = board_ref.borrow();
|
||||
board
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(_id, def)| {
|
||||
if def.tags.contains(tag.as_str()) {
|
||||
Some(Dynamic::from(ObjectInfo::from_def(def, board_ref.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod state;
|
||||
pub mod player;
|
||||
pub mod board;
|
||||
pub mod object_info;
|
||||
pub mod queue;
|
||||
@@ -0,0 +1,137 @@
|
||||
//! ## Object Rhai API
|
||||
//!
|
||||
//! ### Getters
|
||||
//!
|
||||
//! - x, y -> Board location of object
|
||||
//! - id -> The object's board-unique id
|
||||
//! - name -> The object's name, or ()
|
||||
//! - waiting -> bool for whether or not the front of this object's queue is a delay action
|
||||
//! - queue -> the action queue for this object
|
||||
//! - tags -> Array of tags for this object
|
||||
//! - glyph -> The object's current glyph
|
||||
//!
|
||||
//! ### Functions
|
||||
//!
|
||||
//! - has_tag(tag) -> Whether the object has this tag
|
||||
//! - delay(secs) -> Queue a delay action
|
||||
//! - can_push(dir) -> Whether a push in this direction would succeed
|
||||
//! - blocked(dir) -> Whether a solid neighbors me in this direction
|
||||
//!
|
||||
//! Note on blocking: can_push and blocked only take into account the contents of the board
|
||||
//! at the start of the call; if another thing moves before your actions are flushed to the
|
||||
//! gamestate, a move might still fail. This is a TODO.
|
||||
|
||||
use rhai::{Dynamic, Engine};
|
||||
use crate::api::board::BoardRef;
|
||||
use crate::api::queue::ObjQueue;
|
||||
use crate::Direction;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::script::{BoardQueue, Registerable};
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
||||
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
||||
/// It also contains things like script_name used by ScriptHost, since we can't keep a live
|
||||
/// borrow of the board while doing anything: we create one of these loose and then
|
||||
/// it borrows the board only for the duration of what it needs.
|
||||
#[derive(Clone)]
|
||||
pub struct ObjectInfo {
|
||||
pub id: ObjectId,
|
||||
pub x: i64,
|
||||
pub y: i64,
|
||||
pub board: BoardRef,
|
||||
pub script_name: Option<String>,
|
||||
pub queue: ObjQueue
|
||||
}
|
||||
|
||||
impl ObjectInfo {
|
||||
pub fn from_id(id: ObjectId, board: BoardRef) -> Option<ObjectInfo> {
|
||||
let b = board.borrow();
|
||||
let obj = b.objects.get(&id)?;
|
||||
Some(ObjectInfo {
|
||||
id,
|
||||
x: obj.x as i64,
|
||||
y: obj.y as i64,
|
||||
board: board.clone(),
|
||||
script_name: obj.script_name.clone(),
|
||||
queue: obj.queue.clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_def(obj: &ObjectDef, board: BoardRef) -> ObjectInfo {
|
||||
Self {
|
||||
id: obj.id,
|
||||
x: obj.x as i64,
|
||||
y: obj.y as i64,
|
||||
board: board.clone(),
|
||||
script_name: obj.script_name.clone(),
|
||||
queue: obj.queue.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain(&mut self, target: BoardQueue, dt: f64) {
|
||||
let mut b = self.board.borrow_mut();
|
||||
if let Some(def) = b.objects.get_mut(&self.id) {
|
||||
def.queue.drain(self.id, target, dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for ObjectInfo {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<ObjectInfo>("ObjectInfo")
|
||||
.register_get("x", |obj: &mut ObjectInfo| obj.x)
|
||||
.register_get("y", |obj: &mut ObjectInfo| obj.y)
|
||||
.register_get("id", |obj: &mut ObjectInfo| obj.id as i64)
|
||||
.register_get("waiting", |obj: &mut ObjectInfo| obj.queue.waiting())
|
||||
.register_get("queue", |obj: &mut ObjectInfo| obj.queue.clone());
|
||||
|
||||
engine.register_get("name", |o: &mut ObjectInfo| {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id);
|
||||
if let Some(ObjectDef { name: Some(name), ..}) = obj {
|
||||
Dynamic::from(name.clone())
|
||||
} else {
|
||||
Dynamic::UNIT
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_get("tags", |o: &mut ObjectInfo| -> rhai::Array {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id);
|
||||
if let Some(ObjectDef { tags, .. }) = obj {
|
||||
tags.iter().map(|t| Dynamic::from(t.clone())).collect()
|
||||
} else {
|
||||
rhai::Array::new()
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_get("glyph", |o: &mut ObjectInfo| {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id).unwrap();
|
||||
obj.glyph
|
||||
});
|
||||
|
||||
engine.register_fn("has_tag", |o: &mut ObjectInfo, t: String| {
|
||||
if let Some(ObjectDef { tags, .. }) = o.board.borrow().objects.get(&o.id) {
|
||||
tags.contains(&t)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_fn("delay", |o: &mut ObjectInfo, dt: f64| o.queue.delay(dt));
|
||||
|
||||
engine.register_fn("can_push", move |o: &mut ObjectInfo, dir: Direction| {
|
||||
let board = o.board.borrow();
|
||||
board.in_bounds((o.x, o.y)) && board.can_push(o.x as usize, o.y as usize, dir)
|
||||
});
|
||||
|
||||
engine.register_fn("blocked", move |o: &mut ObjectInfo, dir: Direction| -> bool {
|
||||
let board = o.board.borrow();
|
||||
let tx = o.x + dir.dx();
|
||||
let ty = o.y + dir.dy();
|
||||
board.in_bounds((o.x, o.y)) && !board.is_passable(tx as usize, ty as usize)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use rhai::Engine;
|
||||
use crate::player::Player;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::{ErrorSink, PlayerPos};
|
||||
|
||||
/// GameState stores player state but Board stores its position, and we want one
|
||||
/// object to register with Rhai
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PlayerWithPos(pub Player, pub PlayerPos);
|
||||
|
||||
impl Registerable for PlayerWithPos {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<PlayerWithPos>("Player")
|
||||
.register_get("gems", |player: &mut PlayerWithPos| player.0.gems)
|
||||
.register_get("health", |player: &mut PlayerWithPos| player.0.health)
|
||||
.register_get("max_health", |player: &mut PlayerWithPos| player.0.max_health)
|
||||
.register_get("keys", |player: &mut PlayerWithPos| player.0.keys)
|
||||
.register_get("x", |player: &mut PlayerWithPos| player.1.x)
|
||||
.register_get("y", |player: &mut PlayerWithPos| player.1.y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! ## Queue API
|
||||
//!
|
||||
//! `queue.length`, `queue.clear()`, `queue.peek()`, `queue.pop()`, `queue.delay()`
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use rhai::{Dynamic, Engine};
|
||||
use crate::action::{action_to_map, Action, BoardAction};
|
||||
use crate::script::{BoardQueue, Registerable};
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A single object's output queue.
|
||||
#[derive(Clone)]
|
||||
pub struct ObjQueue(Rc<RefCell<VecDeque<Action>>>);
|
||||
|
||||
impl ObjQueue {
|
||||
pub fn new() -> Self {
|
||||
Self(Rc::new(RefCell::new(VecDeque::new())))
|
||||
}
|
||||
|
||||
/// Add a delay to the tail of the queue, or alter the value already there by the given amount.
|
||||
/// A delay action's value can never be less than 0.0
|
||||
/// TODO This really ought to be Duration
|
||||
pub fn delay(&mut self, delay: f64) {
|
||||
let mut q = self.0.borrow_mut();
|
||||
if let Some(Action::Delay(r)) = q.back_mut() {
|
||||
*r = (*r + delay).max(0.0)
|
||||
} else {
|
||||
q.push_back(Action::Delay(delay.max(0.0)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an action to the tail of the queue
|
||||
pub fn act(&mut self, action: Action) {
|
||||
self.0.borrow_mut().push_back(action);
|
||||
}
|
||||
|
||||
/// promote the most recently enqueued action to the front.
|
||||
pub fn now(&mut self) {
|
||||
let mut q = self.0.borrow_mut();
|
||||
if let Some(back) = q.pop_back() {
|
||||
q.push_front(back);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains object `i`'s output queue onto a target queue: first advances
|
||||
/// leading `Delay` actions by dt, then drains actions into the target
|
||||
/// queue until we run out (or hit another delay)
|
||||
pub fn drain(&mut self, source: ObjectId, target: BoardQueue, mut dt: f64) {
|
||||
let mut queue = self.0.borrow_mut();
|
||||
loop {
|
||||
match queue.front_mut() {
|
||||
None => break, // No more actions, we're done
|
||||
Some(Action::Delay(time)) => { // A delay, decrease it by dt
|
||||
if dt < *time { // Delay is too long, bail out
|
||||
*time -= dt;
|
||||
break;
|
||||
} else { // dt eats the delay, pop it and continue
|
||||
dt -= *time;
|
||||
queue.pop_front();
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
let action = queue.pop_front().unwrap();
|
||||
target.borrow_mut().push(BoardAction { source, action });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether this queue has an `Action::Delay` in the front
|
||||
pub fn waiting(&mut self) -> bool {
|
||||
matches!(self.0.borrow().front(), Some(Action::Delay(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for ObjQueue {
|
||||
fn register(engine: &mut Engine, error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<ObjQueue>("Queue");
|
||||
engine.register_get("length", |q: &mut ObjQueue| q.0.borrow().len() as i64);
|
||||
engine.register_fn("clear", |q: &mut ObjQueue| q.0.borrow_mut().clear());
|
||||
|
||||
// peek(): front action as a Rhai map, or () if empty.
|
||||
engine.register_fn("peek", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.0.borrow()
|
||||
.front()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// pop(): same, but removes the front.
|
||||
engine.register_fn("pop", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.0.borrow_mut()
|
||||
.pop_front()
|
||||
.as_ref()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// Appends a [`Action::Delay`] to the back of `queue`, merging with an existing
|
||||
// trailing delay to prevent adjacent delays from accumulating.
|
||||
engine.register_fn("delay", move |q: &mut ObjQueue, secs: Dynamic| {
|
||||
let secs: Result<f64, &str> = if secs.is_float() {
|
||||
secs.as_float()
|
||||
} else if secs.is_int() {
|
||||
secs.as_int().map(|i| i as f64)
|
||||
} else {
|
||||
Err("delay() must be an int or float")
|
||||
};
|
||||
|
||||
match secs {
|
||||
Ok(secs) => q.delay(secs),
|
||||
Err(msg) => error_sink.error(msg.to_string())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
+20
-48
@@ -80,6 +80,11 @@ impl Board {
|
||||
self.layers.len()
|
||||
}
|
||||
|
||||
/// Return a list of all `ObjectId`s currently on the board.
|
||||
pub fn all_ids(&self) -> Vec<ObjectId> {
|
||||
self.objects.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns a reference to the cell at `(x, y)` on layer `z`.
|
||||
///
|
||||
/// The cell is a `(Glyph, Archetype)` tuple. Panics if `z`, `x`, or `y` are
|
||||
@@ -98,7 +103,7 @@ impl Board {
|
||||
|
||||
/// Replace the solid (if any) at `(x, y)` with `Empty`
|
||||
pub fn clear_solid(&mut self, x: usize, y: usize) {
|
||||
if self.in_bounds((x as i32, y as i32)) {
|
||||
if self.in_bounds((x as i64, y as i64)) {
|
||||
if let Some(z) = self.solid_cell_layer(x, y) {
|
||||
*self.get_mut(z, x, y) = (Archetype::Empty.default_glyph(), Archetype::Empty)
|
||||
}
|
||||
@@ -121,7 +126,7 @@ impl Board {
|
||||
/// Panics if out of bounds.
|
||||
pub fn glyph_at(&self, x: usize, y: usize) -> Glyph {
|
||||
// The player is rendered above the whole stack (see the `Player` notes).
|
||||
if self.player.x == x as i32 && self.player.y == y as i32 {
|
||||
if self.player.x == x as i64 && self.player.y == y as i64 {
|
||||
return Glyph::player();
|
||||
}
|
||||
|
||||
@@ -169,7 +174,7 @@ impl Board {
|
||||
///
|
||||
/// Takes signed coords so callers can pass a raw `pos + delta` without first
|
||||
/// checking for negatives.
|
||||
pub fn in_bounds(&self, pos: (i32, i32)) -> bool {
|
||||
pub fn in_bounds(&self, pos: (i64, i64)) -> bool {
|
||||
let (x, y) = pos;
|
||||
x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
|
||||
}
|
||||
@@ -195,7 +200,7 @@ impl Board {
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn solid_at(&self, x: usize, y: usize) -> Option<Solid> {
|
||||
// The player wins its cell (load-time invariant), so it is the solid there.
|
||||
if self.player.x == x as i32 && self.player.y == y as i32 {
|
||||
if self.player.x == x as i64 && self.player.y == y as i64 {
|
||||
return Some(Solid::player_at(x, y));
|
||||
}
|
||||
// A solid object shadows the cell it sits on; capture its behavior now.
|
||||
@@ -236,39 +241,6 @@ impl Board {
|
||||
self.solid_at(x, y).is_none()
|
||||
}
|
||||
|
||||
/// Returns `true` if the solids at `(x1, y1)` and `(x2, y2)` may share a cell —
|
||||
/// i.e. moving one onto the other wouldn't break the "one solid per cell"
|
||||
/// invariant. That holds when at least one cell is empty, or one holds the
|
||||
/// player and the other a **grab** thing (the player collects it).
|
||||
///
|
||||
/// Off-board coordinates are never combinable (returns `false` rather than
|
||||
/// panicking). Backs the script-facing `combinable(x1, y1, x2, y2)` fn.
|
||||
pub fn is_combinable(&self, x1: usize, y1: usize, x2: usize, y2: usize) -> bool {
|
||||
// Are either out of bounds?
|
||||
if !self.in_bounds((x1 as i32, y1 as i32)) || !self.in_bounds((x2 as i32, y2 as i32)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Grab the solids
|
||||
let solid1 = self.solid_at(x1, y1);
|
||||
let solid2 = self.solid_at(x2, y2);
|
||||
|
||||
// Is one cell empty?
|
||||
if solid1.is_none() || solid2.is_none() { return true }
|
||||
|
||||
// They're both present, unwrap them:
|
||||
let solid1 = solid1.unwrap();
|
||||
let solid2 = solid2.unwrap();
|
||||
|
||||
// This is probably disallowed then, but let's check for a player coexisting with a grab:
|
||||
if solid1.player() && solid2.grab() || solid2.player() && solid1.grab() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Nope, two solids that can't coexist:
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether the cell's single solid occupant (if any) can be pushed in `dir`.
|
||||
///
|
||||
/// Non-solid things are never pushable: `pushable` only matters for solids.
|
||||
@@ -289,14 +261,14 @@ impl Board {
|
||||
/// `(x, y)` itself holds no pushable solid, so it doubles as the "is the cell
|
||||
/// ahead shovable?" half of a "can I move here?" query.
|
||||
pub fn can_push(&self, x: usize, y: usize, dir: Direction) -> bool {
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
let (mut cx, mut cy) = (x, y);
|
||||
loop {
|
||||
// This cell must hold a solid pushable in `dir` to advance the chain.
|
||||
if !self.is_pushable(cx, cy, dir) {
|
||||
return false;
|
||||
}
|
||||
let next = (cx as i32 + dx, cy as i32 + dy);
|
||||
let next = (cx as i64 + dx, cy as i64 + dy);
|
||||
if !self.in_bounds(next) {
|
||||
return false; // chain runs off the board
|
||||
}
|
||||
@@ -329,8 +301,8 @@ impl Board {
|
||||
if !self.is_pushable(x, y, dir) {
|
||||
return false;
|
||||
}
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let next = (x as i32 + dx, y as i32 + dy);
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
let next = (x as i64 + dx, y as i64 + dy);
|
||||
if !self.in_bounds(next) {
|
||||
return false; // nothing to shift into off the board
|
||||
}
|
||||
@@ -354,7 +326,7 @@ impl Board {
|
||||
if !self.can_push(x, y, dir) {
|
||||
return;
|
||||
}
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
// can_push guaranteed the chain ends at an in-bounds passable cell, so
|
||||
// re-walk it (no bounds checks needed) and shift the far end first, which
|
||||
// keeps each destination cell vacated before its occupant arrives.
|
||||
@@ -362,8 +334,8 @@ impl Board {
|
||||
let (mut cx, mut cy) = (x, y);
|
||||
while !self.is_passable(cx, cy) {
|
||||
chain.push((cx, cy));
|
||||
cx = (cx as i32 + dx) as usize;
|
||||
cy = (cy as i32 + dy) as usize;
|
||||
cx = (cx as i64 + dx) as usize;
|
||||
cy = (cy as i64 + dy) as usize;
|
||||
}
|
||||
for &(px, py) in chain.iter().rev() {
|
||||
self.shift_solid(px, py, dx, dy);
|
||||
@@ -376,8 +348,8 @@ impl Board {
|
||||
/// terrain archetype (a crate) is moved within its own layer, leaving a
|
||||
/// transparent cell behind so the layer beneath (e.g. floor) shows through.
|
||||
/// The caller guarantees the destination is already clear.
|
||||
fn shift_solid(&mut self, x: usize, y: usize, dx: i32, dy: i32) {
|
||||
let (tx, ty) = ((x as i32 + dx) as usize, (y as i32 + dy) as usize);
|
||||
fn shift_solid(&mut self, x: usize, y: usize, dx: i64, dy: i64) {
|
||||
let (tx, ty) = ((x as i64 + dx) as usize, (y as i64 + dy) as usize);
|
||||
let Some(solid) = self.solid_at(x, y) else {
|
||||
return; // nothing to shift
|
||||
};
|
||||
@@ -541,7 +513,7 @@ impl Board {
|
||||
|
||||
/// Shifts a set of cells, given as `(x, y)` coordinates. Backs the script
|
||||
/// `shift()` fn. Returns any errors as [`LogLine`]s for the caller to log.
|
||||
pub fn apply_shift(&mut self, cells: &[(i32, i32)]) -> Vec<LogLine> {
|
||||
pub fn apply_shift(&mut self, cells: &[(i64, i64)]) -> Vec<LogLine> {
|
||||
// Validate all the cells are in bounds, error if not:
|
||||
if cells.iter().any(|&c| !self.in_bounds(c)) {
|
||||
return vec![LogLine::error("Called shift() with a cell out of bounds")]
|
||||
@@ -637,7 +609,7 @@ pub(crate) mod tests {
|
||||
pub(crate) fn open_board(
|
||||
w: usize,
|
||||
h: usize,
|
||||
player: (i32, i32),
|
||||
player: (i64, i64),
|
||||
objects: Vec<ObjectDef>,
|
||||
) -> Board {
|
||||
let mut object_map: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
|
||||
|
||||
+32
-41
@@ -1,8 +1,8 @@
|
||||
use crate::action::Action;
|
||||
use crate::action::{Action, SendArg};
|
||||
use crate::board::Board;
|
||||
use crate::log::LogLine;
|
||||
use crate::script::{ScriptHost, ScriptState};
|
||||
use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg};
|
||||
use crate::script::ScriptHost;
|
||||
use crate::utils::{Direction, ObjectId, PlayerPos};
|
||||
use crate::world::World;
|
||||
use std::cell::{Ref, RefMut};
|
||||
use std::time::Duration;
|
||||
@@ -13,6 +13,7 @@ pub const SAY_DURATION: f64 = 3.0;
|
||||
// Re-export ScrollLine so kiln-tui can pattern-match scroll content without
|
||||
// accessing the private `action` module directly.
|
||||
pub use crate::action::ScrollLine;
|
||||
use crate::api::state::ScriptState;
|
||||
use crate::player::Player;
|
||||
|
||||
/// An active scroll overlay opened by a scripted object via `scroll()`.
|
||||
@@ -162,7 +163,7 @@ impl GameState {
|
||||
/// the game is about to start — never during map deserialization, since a script
|
||||
/// may inspect the board.
|
||||
pub fn run_init(&mut self) {
|
||||
self.scripts.run_init(ScriptState(self.player));
|
||||
self.scripts.run_init(ScriptState::from_game_state(&self));
|
||||
self.resolve();
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ impl GameState {
|
||||
// this runs exactly once per player interaction with a scroll.
|
||||
self.handle_scroll();
|
||||
let secs = dt.as_secs_f64();
|
||||
self.scripts.run_tick(ScriptState(self.player), secs);
|
||||
self.scripts.run_tick(ScriptState::from_game_state(&self), secs);
|
||||
// Expire speech bubbles before resolving new actions so a fresh say()
|
||||
// this frame isn't immediately culled.
|
||||
self.speech_bubbles.retain_mut(|b| {
|
||||
@@ -212,10 +213,11 @@ impl GameState {
|
||||
let mut gem_delta: i64 = 0;
|
||||
let mut health_delta: i64 = 0;
|
||||
let mut key_changes: Vec<(String, bool)> = Vec::new();
|
||||
let mut sends: Vec<(ObjectId, String, Option<ScriptArg>)> = Vec::new();
|
||||
let mut sends: Vec<(ObjectId, String, SendArg)> = Vec::new();
|
||||
let mut new_bubbles: Vec<SpeechBubble> = Vec::new();
|
||||
// Collected outside the board borrow so we can assign to self.active_scroll.
|
||||
let mut new_scroll: Option<Scroll> = None;
|
||||
|
||||
{
|
||||
let mut board = self.board_mut();
|
||||
for ba in actions {
|
||||
@@ -347,11 +349,12 @@ impl GameState {
|
||||
self.log.push(LogLine::error(format!("set_key: unknown color {color:?}")));
|
||||
}
|
||||
}
|
||||
let state = ScriptState::from_game_state(self);
|
||||
for (bumped, bumper) in bumps {
|
||||
self.scripts.run_bump(ScriptState(self.player), bumped, bumper);
|
||||
self.scripts.run_bump(state.clone(), bumped, bumper);
|
||||
}
|
||||
for (target, fn_name, arg) in sends {
|
||||
self.scripts.run_send(ScriptState(self.player), target, &fn_name, arg);
|
||||
self.scripts.run_send(state.clone(), target, &fn_name, arg);
|
||||
}
|
||||
self.drain_errors();
|
||||
}
|
||||
@@ -367,7 +370,7 @@ impl GameState {
|
||||
if let Some(scroll) = self.active_scroll.take()
|
||||
&& let Some(choice) = scroll.choice
|
||||
{
|
||||
self.scripts.run_send(ScriptState(self.player), scroll.source, &choice, None);
|
||||
self.scripts.run_send(ScriptState::from_game_state(&self), scroll.source, &choice, SendArg::None);
|
||||
self.drain_errors();
|
||||
}
|
||||
}
|
||||
@@ -407,8 +410,8 @@ impl GameState {
|
||||
// Switch to the new board and place the player at the arrival portal.
|
||||
self.current_board_name = target_map.to_string();
|
||||
self.board_mut().player = PlayerPos {
|
||||
x: ax as i32,
|
||||
y: ay as i32,
|
||||
x: ax as i64,
|
||||
y: ay as i64,
|
||||
};
|
||||
// Rebuild the script host for the new board's objects.
|
||||
self.scripts = ScriptHost::new(
|
||||
@@ -432,9 +435,9 @@ impl GameState {
|
||||
let grabbed;
|
||||
let portal_target;
|
||||
{
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
let mut board = self.board_mut();
|
||||
let target = (board.player.x + dx, board.player.y + dy);
|
||||
let target = (board.player.x as i64 + dx, board.player.y as i64 + dy);
|
||||
if !board.in_bounds(target) {
|
||||
return;
|
||||
}
|
||||
@@ -454,8 +457,8 @@ impl GameState {
|
||||
if grabbed.is_none() {
|
||||
board.push(nx, ny, dir);
|
||||
}
|
||||
board.player.x = nx as i32;
|
||||
board.player.y = ny as i32;
|
||||
board.player.x = nx as i64;
|
||||
board.player.y = ny as i64;
|
||||
// Check for a portal at the new position; clone strings to release the borrow.
|
||||
portal_target = board
|
||||
.portals
|
||||
@@ -473,12 +476,13 @@ impl GameState {
|
||||
}
|
||||
// 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.
|
||||
let state = ScriptState::from_game_state(&self);
|
||||
if let Some(id) = grabbed {
|
||||
self.scripts.run_grab(ScriptState(self.player), id);
|
||||
self.scripts.run_grab(state.clone(), id);
|
||||
self.resolve();
|
||||
}
|
||||
if let Some(idx) = bumped {
|
||||
self.scripts.run_bump(ScriptState(self.player), idx, -1);
|
||||
self.scripts.run_bump(state, idx, -1);
|
||||
self.drain_errors();
|
||||
}
|
||||
}
|
||||
@@ -493,9 +497,9 @@ impl GameState {
|
||||
/// pushed aside or blocks the move — since something tried to move into it. Walls and
|
||||
/// crates carry no script, so only solid objects yield a bump.
|
||||
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
|
||||
let target = (ox as i32 + dx, oy as i32 + dy);
|
||||
let target = (ox as i64 + dx, oy as i64 + dy);
|
||||
if !board.in_bounds(target) {
|
||||
return None;
|
||||
}
|
||||
@@ -572,7 +576,7 @@ mod tests {
|
||||
let mut obj = ObjectDef::new(0, 0);
|
||||
obj.solid = false;
|
||||
obj.script_name = Some("s".to_string());
|
||||
let mut board = open_board(board_w, 1, (board_w as i32 - 1, 0), vec![obj]);
|
||||
let mut board = open_board(board_w, 1, (board_w as i64 - 1, 0), vec![obj]);
|
||||
crate_at(&mut board, 2, 0);
|
||||
let scripts = HashMap::from([("s".to_string(), src.to_string())]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
@@ -580,26 +584,13 @@ mod tests {
|
||||
game
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_push_shoves_a_crate() {
|
||||
// The object pushes the crate at (2,0) one step east on its first tick.
|
||||
let mut game = game_with_object_script(
|
||||
5,
|
||||
"fn tick(dt) { if Queue.length() == 0 { push(2, 0, East); } }",
|
||||
);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 3, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_shift_rotates_crates() {
|
||||
// Rotate the crate at (2,0) with the empty cell at (3,0) in a two-cell cycle:
|
||||
// crate moves to (3,0), empty moves back to (2,0).
|
||||
let mut game = game_with_object_script(
|
||||
5,
|
||||
"fn tick(dt) { if Queue.length() == 0 { shift([[2, 0], [3, 0]]); } }",
|
||||
"fn tick(m,s,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }",
|
||||
);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
@@ -617,10 +608,10 @@ mod tests {
|
||||
obj.script_name = Some("s".to_string());
|
||||
let mut board = open_board(4, 1, (3, 0), vec![obj]);
|
||||
crate_at(&mut board, 2, 0);
|
||||
let src = "fn init() { \
|
||||
log(if passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \
|
||||
log(if passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \
|
||||
log(if passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }";
|
||||
let src = "fn init(m,s) { \
|
||||
log(if s.board.passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \
|
||||
log(if s.board.passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \
|
||||
log(if s.board.passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }";
|
||||
let scripts = HashMap::from([("s".to_string(), src.to_string())]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
@@ -735,7 +726,7 @@ mod tests {
|
||||
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
||||
let scripts = HashMap::from([(
|
||||
"s".to_string(),
|
||||
"fn init() { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(),
|
||||
"fn init(m, s) { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(),
|
||||
)]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
@@ -751,7 +742,7 @@ mod tests {
|
||||
let board2 = open_board(2, 1, (1, 0), vec![sobj2]);
|
||||
let scripts2 = HashMap::from([(
|
||||
"t".to_string(),
|
||||
"fn init() { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(),
|
||||
"fn init(m, s) { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(),
|
||||
)]);
|
||||
let mut game2 = GameState::with_scripts(board2, scripts2);
|
||||
game2.run_init();
|
||||
@@ -767,7 +758,7 @@ mod tests {
|
||||
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
||||
let scripts = HashMap::from([(
|
||||
"s".to_string(),
|
||||
r#"fn init() { set_key("chartreuse", true); }"#.to_string(),
|
||||
r#"fn init(m,s) { set_key("chartreuse", true); }"#.to_string(),
|
||||
)]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use color::Rgba8;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use rhai::Engine;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::ErrorSink;
|
||||
|
||||
/// The visual representation of a single board cell.
|
||||
///
|
||||
@@ -32,6 +35,19 @@ impl Hash for Glyph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for Glyph {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<Glyph>("Glyph");
|
||||
engine.register_get("tile", |g: &mut Glyph| g.tile);
|
||||
engine.register_get("fg", |g: &mut Glyph| {
|
||||
format!("#{:02x}{:02x}{:02x}", g.fg.r, g.fg.g, g.fg.b)
|
||||
});
|
||||
engine.register_get("bg", |g: &mut Glyph| {
|
||||
format!("#{:02x}{:02x}{:02x}", g.bg.r, g.bg.g, g.bg.b)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// Returns the glyph used to render the player: tile 64 (`@`) in white on dark blue.
|
||||
///
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use color::Rgba8;
|
||||
use rhai::Engine;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::ErrorSink;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum KeyType {
|
||||
@@ -82,4 +85,18 @@ impl Keyring {
|
||||
(self.white, KeyType::White.glyph().fg),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for Keyring {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<Keyring>("Keyring")
|
||||
.register_get("red", |keyring: &mut Keyring| keyring.red)
|
||||
.register_get("orange", |keyring: &mut Keyring| keyring.orange)
|
||||
.register_get("yellow", |keyring: &mut Keyring| keyring.yellow)
|
||||
.register_get("green", |keyring: &mut Keyring| keyring.green)
|
||||
.register_get("blue", |keyring: &mut Keyring| keyring.blue)
|
||||
.register_get("cyan", |keyring: &mut Keyring| keyring.cyan)
|
||||
.register_get("purple", |keyring: &mut Keyring| keyring.purple)
|
||||
.register_get("white", |keyring: &mut Keyring| keyring.white);
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,10 @@ mod utils;
|
||||
pub mod world;
|
||||
pub mod player;
|
||||
pub mod keys;
|
||||
|
||||
pub use archetype::{Archetype, Builtin};
|
||||
pub use board::Board;
|
||||
pub use utils::Direction;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod api;
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::path::Path;
|
||||
use tinyrand::{Seeded, StdRand};
|
||||
|
||||
use crate::api::queue::ObjQueue;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::Board;
|
||||
use crate::builtin_scripts::archetype_from_builtin_tag;
|
||||
@@ -245,6 +245,7 @@ impl TryFrom<MapFile> for Board {
|
||||
// (see `expand_builtin_archetypes` below), not via templates.
|
||||
builtin_script: None,
|
||||
tags: t.tags.into_iter().collect(),
|
||||
queue: ObjQueue::new(),
|
||||
name,
|
||||
},
|
||||
);
|
||||
@@ -278,8 +279,8 @@ impl TryFrom<MapFile> for Board {
|
||||
height: h,
|
||||
layers,
|
||||
player: PlayerPos {
|
||||
x: px as i32,
|
||||
y: py as i32,
|
||||
x: px as i64,
|
||||
y: py as i64,
|
||||
},
|
||||
objects,
|
||||
next_object_id,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::glyph::Glyph;
|
||||
use crate::utils::ObjectId;
|
||||
use color::Rgba8;
|
||||
use std::collections::HashSet;
|
||||
use crate::api::queue::ObjQueue;
|
||||
|
||||
/// A scripted object placed on the board, loaded from a map file.
|
||||
///
|
||||
@@ -75,6 +76,8 @@ pub struct ObjectDef {
|
||||
/// Names are validated for uniqueness at map-load time; a duplicate name is
|
||||
/// cleared to `None` (the object survives but becomes anonymous).
|
||||
pub name: Option<String>,
|
||||
/// The output queue of actions for this object
|
||||
pub queue: ObjQueue,
|
||||
}
|
||||
|
||||
impl ObjectDef {
|
||||
@@ -107,6 +110,7 @@ impl ObjectDef {
|
||||
script_name: None,
|
||||
builtin_script: None,
|
||||
tags: HashSet::new(),
|
||||
queue: ObjQueue::new(),
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
|
||||
+234
-786
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
// 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
|
||||
// count and remove ourselves from the board.
|
||||
fn grab() {
|
||||
fn grab(me, state) {
|
||||
add_gems(1);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// A heart is a grabbable collectible: walking onto it fires `grab()` instead
|
||||
// of blocking. It restores 1 health and removes itself from the board.
|
||||
fn grab() {
|
||||
fn grab(me, state) {
|
||||
alter_health(1);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 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
|
||||
// count and remove ourselves from the board.
|
||||
fn grab() {
|
||||
fn grab(me, state) {
|
||||
let colors = [
|
||||
"red",
|
||||
"orange",
|
||||
@@ -16,7 +16,7 @@ fn grab() {
|
||||
];
|
||||
|
||||
for c in colors {
|
||||
if Me.has_tag(`BUILTIN_key_${c}`) {
|
||||
if me.has_tag(`BUILTIN_key_${c}`) {
|
||||
set_key(c, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
// pusher shares one compiled copy and learns its direction from the
|
||||
// `BUILTIN_pusher_<dir>` tag the map loader attached to it.
|
||||
|
||||
fn tick(dt) {
|
||||
fn tick(me, state, dt) {
|
||||
// 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
|
||||
// itself costs 0.25s), matching the old global pusher heartbeat.
|
||||
//
|
||||
// The direction is read here, at the top level of the hook, because `Me` is a
|
||||
// per-object scope constant and is not visible inside other script functions.
|
||||
if Queue.length() == 0 {
|
||||
let dir = if Me.has_tag("BUILTIN_pusher_north") { North }
|
||||
else if Me.has_tag("BUILTIN_pusher_south") { South }
|
||||
else if Me.has_tag("BUILTIN_pusher_east") { East }
|
||||
if !me.waiting {
|
||||
let dir = if me.has_tag("BUILTIN_pusher_north") { North }
|
||||
else if me.has_tag("BUILTIN_pusher_south") { South }
|
||||
else if me.has_tag("BUILTIN_pusher_east") { East }
|
||||
else { West };
|
||||
move(dir);
|
||||
delay(0.25);
|
||||
|
||||
@@ -8,24 +8,24 @@
|
||||
// can't decide that with `can_shift` alone — `can_shift(j) == false` is ambiguous
|
||||
// between "j is empty" and "j is a blocked solid" — so we seed with `can_shift`
|
||||
// and then cascade-demote using `passable` to spot the genuine holes.
|
||||
fn tick(dt) {
|
||||
fn tick(me, state, dt) {
|
||||
// Only start a new rotation when the previous one (and its delay) has drained,
|
||||
// exactly like the built-in pusher's pacing.
|
||||
if Queue.length() != 0 { return; }
|
||||
if me.waiting { return; }
|
||||
|
||||
// Direction from the builtin tag; clockwise unless explicitly counter-clockwise.
|
||||
let cw = !Me.has_tag("BUILTIN_spinner_ccw");
|
||||
let cw = !me.has_tag("BUILTIN_spinner_ccw");
|
||||
|
||||
// The 8 neighbour offsets in clockwise order, starting at North.
|
||||
let cw_ring = [
|
||||
[Me.x, Me.y-1], // N
|
||||
[Me.x+1, Me.y-1], // NE
|
||||
[Me.x+1, Me.y], // E
|
||||
[Me.x+1, Me.y+1], // SE
|
||||
[Me.x, Me.y+1], // S
|
||||
[Me.x-1, Me.y+1], // SW
|
||||
[Me.x-1, Me.y], // W
|
||||
[Me.x-1, Me.y-1], // NW
|
||||
[me.x, me.y-1], // N
|
||||
[me.x+1, me.y-1], // NE
|
||||
[me.x+1, me.y], // E
|
||||
[me.x+1, me.y+1], // SE
|
||||
[me.x, me.y+1], // S
|
||||
[me.x-1, me.y+1], // SW
|
||||
[me.x-1, me.y], // W
|
||||
[me.x-1, me.y-1], // NW
|
||||
];
|
||||
|
||||
// For CCW, it's the same list in reverse
|
||||
@@ -45,10 +45,10 @@ fn tick(dt) {
|
||||
// line spins '/'-'\'-, slash-swapped for counter-clockwise. Frame state lives in
|
||||
// the board Registry, keyed per spinner, since script scope resets each tick.
|
||||
let frames = if cw { [47, 0xc4, 92, 0xb3] } else { [92, 0xc4, 47, 0xb3] };
|
||||
let fkey = `spin_${Me.id}`;
|
||||
let fkey = `spin_${me.id}`;
|
||||
let f = Registry.get_or(fkey, 0);
|
||||
set_tile(frames[f % 4]);
|
||||
Registry.set(fkey, (f + 1) % 4);
|
||||
|
||||
delay(0.5);
|
||||
me.delay(0.5);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
||||
fn move_command_relocates_the_source_object() {
|
||||
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "m")]);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
// 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.
|
||||
let board = open_board(5, 3, (0, 0), vec![scripted_object(0, 1, "m")]);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(West); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(West); }")]));
|
||||
game.run_init();
|
||||
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() {
|
||||
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "s")]);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("s", "fn init() { set_tile(7); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("s", "fn init(m,s) { set_tile(7); }")]));
|
||||
game.run_init();
|
||||
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")]);
|
||||
crate_at(&mut board, 2, 0);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
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.
|
||||
let board = open_board(5, 1, (2, 0), vec![scripted_object(1, 0, "m")]);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")]));
|
||||
game.run_init();
|
||||
{
|
||||
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")]);
|
||||
wall_at(&mut board, 3, 0);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
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 mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("m", "fn init() { move(East); move(East); }")]),
|
||||
scripts_from(&[("m", "fn init(m,s) { move(East); move(East); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
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);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("m", "fn init() { move(East); move(South); }")]),
|
||||
scripts_from(&[("m", "fn init(m,s) { move(East); move(South); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
// First (eastward) move is blocked by the wall: object hasn't moved.
|
||||
@@ -136,7 +136,7 @@ fn queue_length_reports_pending_actions() {
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"q",
|
||||
"fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }",
|
||||
"fn init(m,s) { set_tile(5); set_tile(6); log(`len=${m.queue.length}`); }",
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
@@ -166,7 +166,7 @@ fn blocked_reports_solid_and_clear() {
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
"fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
@@ -178,38 +178,9 @@ fn blocked_reports_solid_and_clear() {
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
"fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_sees_earlier_objects_pending_move() {
|
||||
// obj0 (earlier in the array) queues a move into (2,1); obj1 checks blocked()
|
||||
// toward that same cell and must see the pending move.
|
||||
let board = open_board(
|
||||
5,
|
||||
3,
|
||||
(0, 0),
|
||||
vec![
|
||||
scripted_object(1, 1, "mover"),
|
||||
scripted_object(3, 1, "checker"),
|
||||
],
|
||||
);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
("mover", "fn tick(dt) { move(East); }"),
|
||||
(
|
||||
"checker",
|
||||
"fn tick(dt) { if blocked(West) { set_tile(1); } else { set_tile(2); } }",
|
||||
),
|
||||
]),
|
||||
);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1));
|
||||
assert_eq!(b.objects[&2].glyph.tile, 1); // checker saw the pending move
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ fn collision_priority_resolves_in_array_order_and_bumps() {
|
||||
scripts_from(&[
|
||||
(
|
||||
"e",
|
||||
"fn init() { move(East); } fn bump(id) { log(`o0 by ${id}`); }",
|
||||
"fn init(m,s) { move(East); } fn bump(m,s,id) { log(`o0 by ${id}`); }",
|
||||
),
|
||||
(
|
||||
"w",
|
||||
"fn init() { move(West); } fn bump(id) { log(`o1 by ${id}`); }",
|
||||
"fn init(m,s) { move(West); } fn bump(m,s,id) { log(`o1 by ${id}`); }",
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Builds a 3×3 board with the player at `(px, py)` and the given portals.
|
||||
fn make_board(px: i32, py: i32, portals: Vec<PortalDef>) -> Board {
|
||||
fn make_board(px: i64, py: i64, portals: Vec<PortalDef>) -> Board {
|
||||
Board {
|
||||
name: "test".into(),
|
||||
width: 3,
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
||||
fn init_runs_only_on_run_init_not_at_construction() {
|
||||
let (board, scripts) = board_with_object(
|
||||
Some("greet"),
|
||||
&[("greet", r#"fn init() { log("hello"); }"#)],
|
||||
&[("greet", r#"fn init(m,s) { log("hello"); }"#)],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
// 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() {
|
||||
let (board, scripts) = board_with_object(
|
||||
Some("t"),
|
||||
&[("t", r#"fn tick(dt) { log(dt.to_string()); }"#)],
|
||||
&[("t", r#"fn tick(m,s,dt) { log(dt.to_string()); }"#)],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
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 mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("r", "fn init() { log(Board.player_x.to_string()); }")]),
|
||||
scripts_from(&[("r", "fn init(m,s) { log(s.player.x.to_string()); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
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(
|
||||
board,
|
||||
scripts_from(&[
|
||||
("e", "fn init() { move(East); }"),
|
||||
("w", "fn init() { move(West); }"),
|
||||
("e", "fn init(m,s) { move(East); }"),
|
||||
("w", "fn init(m,s) { move(West); }"),
|
||||
]),
|
||||
);
|
||||
game.run_init();
|
||||
@@ -127,7 +127,7 @@ fn set_tag_adds_and_removes_via_my_id() {
|
||||
// present on the object afterward.
|
||||
let (board, scripts) = board_with_object(
|
||||
Some("t"),
|
||||
&[("t", r#"fn init() { set_tag(Me.id, "active", true); }"#)],
|
||||
&[("t", r#"fn init(m,s) { set_tag(m.id, "active", true); }"#)],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
@@ -136,7 +136,7 @@ fn set_tag_adds_and_removes_via_my_id() {
|
||||
// A script removes a pre-existing tag.
|
||||
let (mut board2, scripts2) = board_with_object(
|
||||
Some("t2"),
|
||||
&[("t2", r#"fn init() { set_tag(Me.id, "active", false); }"#)],
|
||||
&[("t2", r#"fn init(m,s) { set_tag(m.id, "active", false); }"#)],
|
||||
);
|
||||
// Seed the tag before construction.
|
||||
board2
|
||||
@@ -158,8 +158,8 @@ fn has_tag_reads_own_tags() {
|
||||
Some("t"),
|
||||
&[(
|
||||
"t",
|
||||
r#"fn init() { set_tag(Me.id, "active", true); }
|
||||
fn tick(dt) { log(Me.has_tag("active").to_string()); }"#,
|
||||
r#"fn init(m,s) { set_tag(m.id, "active", true); }
|
||||
fn tick(m,s,dt) { log(m.has_tag("active").to_string()); }"#,
|
||||
)],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
@@ -182,8 +182,8 @@ fn objects_with_tag_returns_matching_ids() {
|
||||
scripts_from(&[
|
||||
(
|
||||
"q",
|
||||
r#"fn init() {
|
||||
let infos = Board.tagged("enemy");
|
||||
r#"fn init(m,s) {
|
||||
let infos = s.board.tagged("enemy");
|
||||
log(infos.len().to_string());
|
||||
log(infos[0].id.to_string());
|
||||
}"#,
|
||||
@@ -201,18 +201,18 @@ fn objects_with_tag_returns_matching_ids() {
|
||||
fn my_name_returns_name_or_empty_string() {
|
||||
// An object with a name set on its ObjectDef should see it via my_name().
|
||||
let (mut board, scripts) =
|
||||
board_with_object(Some("n"), &[("n", r#"fn init() { log(Me.name); }"#)]);
|
||||
board_with_object(Some("n"), &[("n", r#"fn init(m,s) { log(m.name); }"#)]);
|
||||
board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string());
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
assert_eq!(log_texts(&game), vec!["beacon"]);
|
||||
|
||||
// An unnamed object should get an empty string.
|
||||
// An unnamed object should get ().
|
||||
let (board2, scripts2) =
|
||||
board_with_object(Some("n"), &[("n", r#"fn init() { log(Me.name); }"#)]);
|
||||
board_with_object(Some("n"), &[("n", r#"fn init(m,s) { if m.name == () { log("null"); }}"#)]);
|
||||
let mut game2 = GameState::with_scripts(board2, scripts2);
|
||||
game2.run_init();
|
||||
assert_eq!(log_texts(&game2), vec![""]);
|
||||
assert_eq!(log_texts(&game2), vec!["null"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -228,9 +228,9 @@ fn object_id_for_name_finds_by_name() {
|
||||
scripts_from(&[
|
||||
(
|
||||
"q",
|
||||
r#"fn init() {
|
||||
log(Board.named("target").id.to_string());
|
||||
let miss = Board.named("missing");
|
||||
r#"fn init(m,s) {
|
||||
log(s.board.named("target").id.to_string());
|
||||
let miss = s.board.named("missing");
|
||||
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 mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("b", "fn bump(id) { log(`bumped by ${id}`); }")]),
|
||||
scripts_from(&[("b", "fn bump(m,s,id) { log(`bumped by ${id}`); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
@@ -271,7 +271,7 @@ fn scroll_opens_on_player_bump() {
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"s",
|
||||
r#"fn bump(id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#,
|
||||
r#"fn bump(m,s,id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#,
|
||||
)]),
|
||||
);
|
||||
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 mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("s", r#"fn bump(id) { scroll(["Hello"]); }"#)]),
|
||||
scripts_from(&[("s", r#"fn bump(m,s,id) { scroll(["Hello"]); }"#)]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
@@ -317,7 +317,7 @@ fn handle_scroll_with_choice_dispatches_send_to_source() {
|
||||
scripts_from(&[(
|
||||
"s",
|
||||
r#"
|
||||
fn bump(id) { scroll(["Muffin?", ["eat", "Eat it"]]); }
|
||||
fn bump(m,s,id) { scroll(["Muffin?", ["eat", "Eat it"]]); }
|
||||
fn eat() { log("eaten"); }
|
||||
"#,
|
||||
)]),
|
||||
|
||||
+80
-13
@@ -1,8 +1,12 @@
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::glyph::Glyph;
|
||||
use color::Rgba8;
|
||||
use rhai::Dynamic;
|
||||
use crate::Board;
|
||||
use crate::log::LogLine;
|
||||
|
||||
/// Which directions a solid may be pushed in.
|
||||
///
|
||||
@@ -215,8 +219,8 @@ impl Solid {
|
||||
pub fn place(self, board: &mut Board, x: usize, y: usize) {
|
||||
match self.kind {
|
||||
SolidKind::Player => {
|
||||
board.player.x = x as i32;
|
||||
board.player.y = y as i32;
|
||||
board.player.x = x as i64;
|
||||
board.player.y = y as i64;
|
||||
}
|
||||
SolidKind::Object(id) => {
|
||||
if let Some(obj) = board.objects.get_mut(&id) {
|
||||
@@ -293,9 +297,9 @@ impl PortalDef {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PlayerPos {
|
||||
/// Column position (0-indexed, increasing rightward).
|
||||
pub x: i32,
|
||||
pub x: i64,
|
||||
/// Row position (0-indexed, increasing downward).
|
||||
pub y: i32,
|
||||
pub y: i64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -325,14 +329,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An optional argument passed to a script function via [`crate::action::Action::Send`].
|
||||
pub(crate) enum ScriptArg {
|
||||
/// A string argument.
|
||||
Str(String),
|
||||
/// A numeric argument (stored as `f64` to cover both integer and float callers).
|
||||
Num(f64),
|
||||
}
|
||||
|
||||
/// A cardinal movement direction.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Direction {
|
||||
@@ -342,6 +338,37 @@ pub enum Direction {
|
||||
West,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub fn char(self) -> char {
|
||||
match self {
|
||||
Direction::North => 'n',
|
||||
Direction::South => 's',
|
||||
Direction::East => 'e',
|
||||
Direction::West => 'w',
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> [Direction; 4] {
|
||||
[Direction::North, Direction::South, Direction::East, Direction::West]
|
||||
}
|
||||
|
||||
pub fn dx(self) -> i64 {
|
||||
match self {
|
||||
Direction::North | Direction::South => 0,
|
||||
Direction::East => 1,
|
||||
Direction::West => -1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dy(self) -> i64 {
|
||||
match self {
|
||||
Direction::East | Direction::West => 0,
|
||||
Direction::South => 1,
|
||||
Direction::North => -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be stored in a board's script registry across board transitions.
|
||||
///
|
||||
/// Restricted to primitive types that convert cleanly to and from `rhai::Dynamic`
|
||||
@@ -389,7 +416,7 @@ impl Into<Dynamic> for RegistryValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Direction> for (i32, i32) {
|
||||
impl From<Direction> for (i64, i64) {
|
||||
/// The `(dx, dy)` cell delta for a direction (screen coordinates: +y down).
|
||||
fn from(d: Direction) -> Self {
|
||||
match d {
|
||||
@@ -400,3 +427,43 @@ impl From<Direction> for (i32, i32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone,Debug, PartialEq)]
|
||||
pub enum Hook {
|
||||
Init, Tick, Bump, Grab
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Hook::Init => "init",
|
||||
Hook::Bump => "bump",
|
||||
Hook::Grab => "grab",
|
||||
Hook::Tick => "tick"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Hook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel for engine/compile/runtime errors.
|
||||
#[derive(Clone)]
|
||||
pub struct ErrorSink(Rc<RefCell<Vec<LogLine>>>);
|
||||
|
||||
impl ErrorSink {
|
||||
pub fn new() -> Self {
|
||||
Self(Rc::new(RefCell::new(Vec::new())))
|
||||
}
|
||||
|
||||
pub fn error(&self, msg: String) {
|
||||
self.0.borrow_mut().push(LogLine::error(msg));
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Vec<LogLine> {
|
||||
std::mem::take(&mut self.0.borrow_mut())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user