portals 1
This commit is contained in:
@@ -52,6 +52,10 @@ pub(crate) enum Action {
|
||||
/// Open a scrollable text overlay. Pauses game ticks while shown; a choice
|
||||
/// selection dispatches [`Send`](Action::Send) to the source object.
|
||||
Scroll(Vec<ScrollLine>),
|
||||
/// 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 },
|
||||
}
|
||||
|
||||
// ── Direction helper ──────────────────────────────────────────────────────────
|
||||
@@ -125,6 +129,11 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||
Action::Scroll(_) => {
|
||||
m.insert("type".into(), ds("Scroll"));
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use crate::archetype::Archetype;
|
||||
use crate::archetype::Archetype::Empty;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::log::LogLine;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::Direction;
|
||||
use crate::utils::{ObjectId, Player, PortalDef, Solid};
|
||||
use crate::utils::{ObjectId, Player, PortalDef, RegistryValue, Solid};
|
||||
|
||||
/// The complete state of one game board (a single room or screen).
|
||||
///
|
||||
@@ -67,6 +67,11 @@ pub struct Board {
|
||||
/// [`LogLine`]s. Empty for a clean load; see [`Board::is_valid`]. Not part of
|
||||
/// the map file (purely a load diagnostic).
|
||||
pub(crate) load_errors: Vec<LogLine>,
|
||||
/// Per-board key→value store written and read by Rhai scripts via the
|
||||
/// `Registry` scope constant. Persists automatically across board transitions
|
||||
/// because all boards live as `Rc<RefCell<Board>>` in `World::boards` and are
|
||||
/// never evicted. Not saved to disk in v1.
|
||||
pub registry: HashMap<String, RegistryValue>,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
@@ -308,7 +313,7 @@ impl Board {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use color::Rgba8;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::glyph::Glyph;
|
||||
@@ -345,6 +350,7 @@ pub(crate) mod tests {
|
||||
portals: Vec::new(),
|
||||
board_script_name: None,
|
||||
load_errors: Vec::new(),
|
||||
registry: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,26 @@ impl GameState {
|
||||
Action::Scroll(lines) => {
|
||||
new_scroll = Some(Scroll { source: ba.source, lines });
|
||||
}
|
||||
Action::Teleport { x, y } => {
|
||||
if !board.in_bounds((x, y)) {
|
||||
logs.push(LogLine::error(format!(
|
||||
"teleport({x},{y}): out of bounds"
|
||||
)));
|
||||
} else {
|
||||
let (ux, uy) = (x as usize, y as usize);
|
||||
let source_solid = board.objects.get(&ba.source)
|
||||
.map(|o| o.solid)
|
||||
.unwrap_or(false);
|
||||
if source_solid && board.solid_at(ux, uy).is_some() {
|
||||
logs.push(LogLine::error(format!(
|
||||
"teleport({x},{y}): destination is solid"
|
||||
)));
|
||||
} else if let Some(obj) = board.objects.get_mut(&ba.source) {
|
||||
obj.x = ux;
|
||||
obj.y = uy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +712,7 @@ impl TryFrom<MapFile> for Board {
|
||||
portals,
|
||||
board_script_name: mf.map.board_script_name,
|
||||
load_errors,
|
||||
registry: std::collections::HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+59
-2
@@ -19,6 +19,7 @@
|
||||
//! | `Board` | `Board` | Read-only world handle. |
|
||||
//! | `Queue` | `Queue` | This object's pending-action queue. |
|
||||
//! | `Me` | `Me` | Self-reference (id, name, x, y, tags, glyph). |
|
||||
//! | `Registry` | `Registry` | Board-scoped key→value store persisting across board transitions. |
|
||||
//!
|
||||
//! Direction and color constants are registered as a global Rhai module and
|
||||
//! are visible to all functions at any call depth, including Rhai-to-Rhai calls:
|
||||
@@ -46,7 +47,7 @@
|
||||
//!
|
||||
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
|
||||
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
|
||||
//! `send(target_id, fn_name [, arg])`
|
||||
//! `send(target_id, fn_name [, arg])`, `teleport(x, y)`
|
||||
//!
|
||||
//! ## Queue API
|
||||
//!
|
||||
@@ -57,7 +58,7 @@ use crate::game::SAY_DURATION;
|
||||
use crate::board::Board;
|
||||
use crate::log::LogLine;
|
||||
use crate::map_file::{color_to_hex, parse_color};
|
||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||
use crate::utils::{Direction, ObjectId, RegistryValue, ScriptArg};
|
||||
use rhai::{Array, CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, Module, NativeCallContext, Scope, AST};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -133,6 +134,15 @@ struct Me {
|
||||
board: BoardRef,
|
||||
}
|
||||
|
||||
/// The board's script registry, pushed into scope as the constant `Registry`.
|
||||
/// `get`/`set`/`get_or` methods let scripts read and write per-board key→value pairs
|
||||
/// that persist across board transitions. Mutation goes through the inner
|
||||
/// `Rc<RefCell<Board>>`, so the struct itself can be shared as a scope constant.
|
||||
#[derive(Clone)]
|
||||
struct Registry {
|
||||
board: BoardRef,
|
||||
}
|
||||
|
||||
// ── ObjectInfo helper ─────────────────────────────────────────────────────────
|
||||
|
||||
fn object_info_from(id: ObjectId, board: &Board) -> Option<ObjectInfo> {
|
||||
@@ -187,6 +197,7 @@ impl ScriptHost {
|
||||
register_me_type(&mut engine);
|
||||
register_object_info_type(&mut engine);
|
||||
register_glyph_type(&mut engine);
|
||||
register_registry_type(&mut engine);
|
||||
register_global_constants(&mut engine);
|
||||
|
||||
let board = board_cell.borrow();
|
||||
@@ -449,6 +460,43 @@ fn register_me_type(engine: &mut Engine) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Register Registry type ────────────────────────────────────────────────────
|
||||
|
||||
fn register_registry_type(engine: &mut Engine) {
|
||||
engine.register_type_with_name::<Registry>("Registry");
|
||||
|
||||
// Registry.get(key) -> Dynamic — returns () if the key is absent.
|
||||
engine.register_fn("get", |r: &mut Registry, key: ImmutableString| -> Dynamic {
|
||||
r.board.borrow()
|
||||
.registry
|
||||
.get(key.as_str())
|
||||
.map(|v| v.clone().into())
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// Registry.set(key, value) — stores a primitive value; () removes the key;
|
||||
// unsupported types (closures, custom objects) are silently ignored.
|
||||
engine.register_fn("set", |r: &mut Registry, key: ImmutableString, value: Dynamic| {
|
||||
match RegistryValue::try_from(value.clone()) {
|
||||
Ok(rv) => { r.board.borrow_mut().registry.insert(key.to_string(), rv); }
|
||||
Err(()) if value.is_unit() => { r.board.borrow_mut().registry.remove(key.as_str()); }
|
||||
Err(()) => {} // unsupported type — silently ignore
|
||||
}
|
||||
});
|
||||
|
||||
// Registry.get_or(key, default) — returns the stored value when present and the
|
||||
// same Rhai type as `default`; falls back to `default` otherwise.
|
||||
engine.register_fn("get_or", |r: &mut Registry, key: ImmutableString, default: Dynamic| -> Dynamic {
|
||||
if let Some(stored) = r.board.borrow().registry.get(key.as_str()).cloned() {
|
||||
let candidate: Dynamic = stored.into();
|
||||
// Only return the stored value if it matches the type of the default.
|
||||
if candidate.type_id() == default.type_id() { candidate } else { default }
|
||||
} else {
|
||||
default
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Register ObjectInfo type ──────────────────────────────────────────────────
|
||||
|
||||
fn register_object_info_type(engine: &mut Engine) {
|
||||
@@ -725,6 +773,14 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// teleport(x, y): move the calling object to an arbitrary cell. Zero time cost.
|
||||
// Blocked (with an error logged at resolve time) if the object is solid and the
|
||||
// destination already has a solid occupant.
|
||||
let q = queues.clone();
|
||||
engine.register_fn("teleport", move |ctx: NativeCallContext, x: i64, y: i64| {
|
||||
emit(&q, source_of(&ctx), Action::Teleport { x: x as i32, y: y as i32 });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Queue API ─────────────────────────────────────────────────────────────────
|
||||
@@ -790,6 +846,7 @@ fn new_object_scope(board: &BoardRef, queue: &ObjQueue, object_id: ObjectId) ->
|
||||
scope.push_constant("Board", board.clone());
|
||||
scope.push_constant("Queue", queue.clone());
|
||||
scope.push_constant("Me", Me { object_id, board: board.clone() });
|
||||
scope.push_constant("Registry", Registry { board: board.clone() });
|
||||
scope
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ fn make_board(px: i32, py: i32, portals: Vec<PortalDef>) -> Board {
|
||||
portals,
|
||||
board_script_name: None,
|
||||
load_errors: Vec::new(),
|
||||
registry: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> (
|
||||
portals: Vec::new(),
|
||||
board_script_name: None,
|
||||
load_errors: Vec::new(),
|
||||
registry: HashMap::new(),
|
||||
};
|
||||
(board, scripts_from(scripts))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use color::Rgba8;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::glyph::Glyph;
|
||||
use rhai::Dynamic;
|
||||
|
||||
/// Which directions a solid may be pushed in.
|
||||
///
|
||||
@@ -169,6 +170,45 @@ pub enum Direction {
|
||||
West,
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// and are ready for TOML serialization if disk persistence is added later.
|
||||
/// Arbitrary `Dynamic` values (closures, custom types) are rejected at the boundary.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RegistryValue {
|
||||
Bool(bool),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
impl TryFrom<Dynamic> for RegistryValue {
|
||||
type Error = ();
|
||||
/// Returns `Err(())` for `()` (unit) and all unsupported types.
|
||||
fn try_from(value: Dynamic) -> Result<Self, ()> {
|
||||
if value.is_bool() { return Ok(RegistryValue::Bool(value.as_bool().unwrap())); }
|
||||
if value.is_int() { return Ok(RegistryValue::Int(value.as_int().unwrap())); }
|
||||
if value.is_float() { return Ok(RegistryValue::Float(value.as_float().unwrap())); }
|
||||
if value.is_string() { return Ok(RegistryValue::Str(value.into_string().unwrap())); }
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
// `From<RegistryValue> for Dynamic` would violate orphan rules (Dynamic is foreign),
|
||||
// so we implement Into directly — the documented exception.
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<Dynamic> for RegistryValue {
|
||||
fn into(self) -> Dynamic {
|
||||
match self {
|
||||
RegistryValue::Bool(b) => Dynamic::from(b),
|
||||
RegistryValue::Int(n) => Dynamic::from(n),
|
||||
RegistryValue::Float(f) => Dynamic::from(f),
|
||||
RegistryValue::Str(s) => Dynamic::from(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Direction> for (i32, i32) {
|
||||
/// The `(dx, dy)` cell delta for a direction (screen coordinates: +y down).
|
||||
fn from(d: Direction) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user