more test cleanup
This commit is contained in:
@@ -162,17 +162,6 @@ pub struct BoardAction {
|
|||||||
pub(crate) action: Action,
|
pub(crate) action: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum Consequence {
|
|
||||||
Enter(ObjectId),
|
|
||||||
Bump(ObjectId, Direction),
|
|
||||||
Send(ObjectId, String, SendArg)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enters_for_cell(board: &Board, x: usize, y: usize) -> Vec<Consequence> {
|
|
||||||
board.sensor_ids_at(x, y).into_iter().map(|id| Consequence::Enter(id)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<(), String> {
|
pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<(), String> {
|
||||||
if !board.in_bounds((x, y)) {
|
if !board.in_bounds((x, y)) {
|
||||||
Err(format!("teleport({target},{x},{y}): out of bounds"))
|
Err(format!("teleport({target},{x},{y}): out of bounds"))
|
||||||
|
|||||||
@@ -65,17 +65,6 @@ impl ObjectInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_def(obj: &ObjectDef, board: BoardRef, x: usize, y: usize) -> ObjectInfo {
|
|
||||||
Self {
|
|
||||||
id: obj.scripting.id,
|
|
||||||
x: x as i64,
|
|
||||||
y: y as i64,
|
|
||||||
board: board.clone(),
|
|
||||||
script_key: obj.scripting.script_name.clone(),
|
|
||||||
queue: obj.scripting.queue.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drain(&mut self, target: &mut Vec<BoardAction>, dt: f64) {
|
pub fn drain(&mut self, target: &mut Vec<BoardAction>, dt: f64) {
|
||||||
if let Some(scr) = self.board.borrow_mut().scripting_mut(self.id) {
|
if let Some(scr) = self.board.borrow_mut().scripting_mut(self.id) {
|
||||||
scr.queue.drain(self.id, target, dt)
|
scr.queue.drain(self.id, target, dt)
|
||||||
@@ -140,9 +129,8 @@ impl Registerable for ObjectInfo {
|
|||||||
|
|
||||||
engine.register_fn("blocked", move |o: &mut ObjectInfo, dir: Direction| -> bool {
|
engine.register_fn("blocked", move |o: &mut ObjectInfo, dir: Direction| -> bool {
|
||||||
let board = o.board.borrow();
|
let board = o.board.borrow();
|
||||||
let tx = o.x + dir.dx();
|
let (tx, ty) = dir.from_point(o.x, o.y);
|
||||||
let ty = o.y + dir.dy();
|
!board.in_bounds((tx, ty)) || !board.is_passable(tx as usize, ty as usize)
|
||||||
board.in_bounds((o.x, o.y)) && !board.is_passable(tx as usize, ty as usize)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ impl Registerable for PlayerWithPos {
|
|||||||
.register_get("health", |player: &mut PlayerWithPos| player.0.borrow().health)
|
.register_get("health", |player: &mut PlayerWithPos| player.0.borrow().health)
|
||||||
.register_get("max_health", |player: &mut PlayerWithPos| player.0.borrow().max_health)
|
.register_get("max_health", |player: &mut PlayerWithPos| player.0.borrow().max_health)
|
||||||
.register_get("keys", |player: &mut PlayerWithPos| player.0.borrow().keys)
|
.register_get("keys", |player: &mut PlayerWithPos| player.0.borrow().keys)
|
||||||
.register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player_pos().0)
|
.register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player_pos().0 as i64)
|
||||||
.register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player_pos().1);
|
.register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player_pos().1 as i64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,38 +266,6 @@ impl Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The object that a move **into** `(x, y)` heading `dir` bumps, if any.
|
|
||||||
///
|
|
||||||
/// Walks the chain of pushable crates from the target cell in `dir` until it
|
|
||||||
/// reaches something that stops it, and reports the object it presses against:
|
|
||||||
/// - a `Block` object in the target cell (or at the end of a pushable chain) is the
|
|
||||||
/// bumped object,
|
|
||||||
/// - open space or the player means nothing is bumped,
|
|
||||||
///
|
|
||||||
/// This is what lets *any* solid — the player, another object, or a pushed
|
|
||||||
/// crate — trigger a `bump`: the bumper need not be an object, since we only
|
|
||||||
/// return the *bumped* object's id (the direction it came from is supplied by
|
|
||||||
/// the caller from its move direction).
|
|
||||||
pub fn bump_target(&self, x: usize, y: usize, dir: Direction) -> Option<ObjectId> {
|
|
||||||
let (dx, dy): (i64, i64) = dir.into();
|
|
||||||
let (mut cx, mut cy) = (x, y);
|
|
||||||
loop {
|
|
||||||
if !self.in_bounds((cx as i64, cy as i64)) {
|
|
||||||
return None; // push chain runs off the board
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Tile::Object(obj)) = self.get(cx, cy) {
|
|
||||||
if obj.enter_response.transmits_push(dir) {
|
|
||||||
// An object we can push through, go to the next cell
|
|
||||||
cx = (cx as i64 + dx) as usize;
|
|
||||||
cy = (cy as i64 + dy) as usize;
|
|
||||||
} else if obj.enter_response.bumpable(dir) {
|
|
||||||
return Some(obj.scripting.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the chain of pushable solids starting at `(x, y)` can be shoved one
|
/// Whether the chain of pushable solids starting at `(x, y)` can be shoved one
|
||||||
/// step in `dir` — i.e. the chain ends at a passable cell rather than the board
|
/// step in `dir` — i.e. the chain ends at a passable cell rather than the board
|
||||||
/// edge or a non-pushable solid.
|
/// edge or a non-pushable solid.
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ impl BoardSpec {
|
|||||||
///
|
///
|
||||||
/// Returns `Err` only on a grid-dimension mismatch (the single hard error);
|
/// Returns `Err` only on a grid-dimension mismatch (the single hard error);
|
||||||
/// every other problem is recorded on `errors`.
|
/// every other problem is recorded on `errors`.
|
||||||
pub(crate) fn build_grid(&self) -> Result<Vec<Option<TileSpec>>, String> {
|
pub fn build_grid(&self) -> Result<Vec<Option<TileSpec>>, String> {
|
||||||
let grid = self.grid_chars()?;
|
let grid = self.grid_chars()?;
|
||||||
|
|
||||||
// Walk the grid, filling cells and collecting placements.
|
// Walk the grid, filling cells and collecting placements.
|
||||||
@@ -116,7 +116,7 @@ impl BoardSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Try to check for validity of the board, return a list of errors we find (if any)
|
/// Try to check for validity of the board, return a list of errors we find (if any)
|
||||||
pub(crate) fn validate(&self, grid: &Vec<Option<TileSpec>>, valid_script_names: &HashSet<&String>) -> Result<(), Vec<String>> {
|
pub fn validate(&self, grid: &Vec<Option<TileSpec>>, valid_script_names: &HashSet<&String>) -> Result<(), Vec<String>> {
|
||||||
let mut errors = vec![];
|
let mut errors = vec![];
|
||||||
|
|
||||||
// Check for player being positioned exactly once
|
// Check for player being positioned exactly once
|
||||||
@@ -188,13 +188,13 @@ impl BoardSpec {
|
|||||||
if errors.is_empty() { Ok(()) } else { Err(errors) }
|
if errors.is_empty() { Ok(()) } else { Err(errors) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn into_board(self, script_names: &HashSet<&String>) -> Result<Board, String> {
|
pub fn into_board(self, script_names: &HashSet<&String>) -> Result<Board, String> {
|
||||||
let grid = self.build_grid()?;
|
let grid = self.build_grid()?;
|
||||||
if let Err(errors) = self.validate(&grid, script_names) {
|
if let Err(errors) = self.validate(&grid, script_names) {
|
||||||
return Err(errors.join("\n"));
|
return Err(errors.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut next_object_id = 0;
|
let mut next_object_id = 1;
|
||||||
let mut tile_grid = Vec::with_capacity(grid.len());
|
let mut tile_grid = Vec::with_capacity(grid.len());
|
||||||
|
|
||||||
for spec in grid.into_iter() {
|
for spec in grid.into_iter() {
|
||||||
@@ -216,7 +216,7 @@ impl BoardSpec {
|
|||||||
floor: self.floor.into_floor(self.width, self.height),
|
floor: self.floor.into_floor(self.width, self.height),
|
||||||
sensors,
|
sensors,
|
||||||
portals: self.portals,
|
portals: self.portals,
|
||||||
next_object_id: 0,
|
next_object_id,
|
||||||
dark: self.dark,
|
dark: self.dark,
|
||||||
registry: Default::default(),
|
registry: Default::default(),
|
||||||
};
|
};
|
||||||
|
|||||||
+8
-41
@@ -1,4 +1,4 @@
|
|||||||
use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction, Consequence, SendArg};
|
use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction, SendArg};
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::script::ScriptHost;
|
use crate::script::ScriptHost;
|
||||||
@@ -9,32 +9,6 @@ use std::collections::{BTreeSet, HashSet, VecDeque};
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// A single `send` to an object, with an arg.
|
|
||||||
///
|
|
||||||
/// Most things (bump, etc) are only triggered by player actions now.
|
|
||||||
/// However, sends still might trigger other sends! So we need to keep a
|
|
||||||
/// list of sends that we trigger in the process of resolving a list of
|
|
||||||
/// actions. When we resolve these sends, we'll keep a list of things we've
|
|
||||||
/// resolved, so we refuse to do the same send twice in a tick: this prevents
|
|
||||||
/// us from accidentally doing an infinite recursion.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct SendAction(ObjectId, String, SendArg);
|
|
||||||
|
|
||||||
impl Hash for SendAction {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.0.hash(state);
|
|
||||||
self.1.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for SendAction {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.1 == other.1 && self.0 == other.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for SendAction {}
|
|
||||||
|
|
||||||
/// How long a `say()` speech bubble stays on screen, in seconds.
|
/// How long a `say()` speech bubble stays on screen, in seconds.
|
||||||
pub const SAY_DURATION: f64 = 3.0;
|
pub const SAY_DURATION: f64 = 3.0;
|
||||||
|
|
||||||
@@ -43,7 +17,7 @@ pub const SAY_DURATION: f64 = 3.0;
|
|||||||
pub use crate::action::ScrollLine;
|
pub use crate::action::ScrollLine;
|
||||||
use crate::player::{Player, PlayerRef};
|
use crate::player::{Player, PlayerRef};
|
||||||
use crate::portal::Portal;
|
use crate::portal::Portal;
|
||||||
use crate::tile::{EnterResponse, LocatedObject, Tile};
|
use crate::tile::{EnterResponse, Tile};
|
||||||
|
|
||||||
/// An active scroll overlay opened by a scripted object via `scroll()`.
|
/// An active scroll overlay opened by a scripted object via `scroll()`.
|
||||||
///
|
///
|
||||||
@@ -358,7 +332,7 @@ impl GameState {
|
|||||||
{
|
{
|
||||||
// Dispatch the choice back to the source object and apply whatever it
|
// Dispatch the choice back to the source object and apply whatever it
|
||||||
// queues (plus any bump/send cascade), the same as a tick.
|
// queues (plus any bump/send cascade), the same as a tick.
|
||||||
let actions = self.scripts.run_send(scroll.source, &choice, SendArg::None);
|
self.scripts.run_send(scroll.source, &choice, SendArg::None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,8 +365,12 @@ impl GameState {
|
|||||||
// Clear per-board transient state.
|
// Clear per-board transient state.
|
||||||
self.speech_bubbles.clear();
|
self.speech_bubbles.clear();
|
||||||
self.active_scroll = None;
|
self.active_scroll = None;
|
||||||
// Switch to the new board and place the player at the arrival portal.
|
// Switch to the new board
|
||||||
self.current_board_name = target_map.to_string();
|
self.current_board_name = target_map.to_string();
|
||||||
|
// Clear any player instance that's already on the new board
|
||||||
|
let new_board_player_pos = self.board().player_pos();
|
||||||
|
self.board_mut().get_mut(new_board_player_pos.0, new_board_player_pos.1).take();
|
||||||
|
// place the player at the arrival portal.
|
||||||
self.board_mut().get_mut(ax, ay).replace(Tile::Player);
|
self.board_mut().get_mut(ax, ay).replace(Tile::Player);
|
||||||
self.board_mut().clear_all_queues();
|
self.board_mut().clear_all_queues();
|
||||||
// Rebuild the script host for the new board's objects.
|
// Rebuild the script host for the new board's objects.
|
||||||
@@ -522,17 +500,6 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a [`step_object`] call produced, for the caller to resolve after the board
|
|
||||||
/// borrow drops.
|
|
||||||
struct StepOutcome {
|
|
||||||
/// The solid object the move pressed into (directly or at a crate-chain's end),
|
|
||||||
/// which receives a `bump`.
|
|
||||||
bumped: Option<ObjectId>,
|
|
||||||
/// Non-solid objects a solid landed on this move — the mover itself (only if it
|
|
||||||
/// is solid) and every crate it shoved — each of which receives an `enter`.
|
|
||||||
entered: Vec<ObjectId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves object `id` one cell in `dir` on `board`, reporting the `bump`/`enter`
|
/// Moves object `id` one cell in `dir` on `board`, reporting the `bump`/`enter`
|
||||||
/// reactions for the caller to resolve after the board borrow drops.
|
/// reactions for the caller to resolve after the board borrow drops.
|
||||||
///
|
///
|
||||||
|
|||||||
+31
-26
@@ -135,34 +135,39 @@ impl ScriptHost {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(source) = script_key.source(&script_sources) {
|
match script_key.source(&script_sources) {
|
||||||
match engine.compile(source) {
|
Ok(None) => { continue } // This has no source...
|
||||||
Ok(ast) => {
|
Err(e) => { // Couldn't find it
|
||||||
let defines = |n: &str, params: usize| {
|
failed.insert(script_key.clone());
|
||||||
ast.iter_functions()
|
log_sink.error(e);
|
||||||
.any(|f| f.name == n && f.params.len() == params)
|
continue;
|
||||||
};
|
}
|
||||||
scripts.insert(
|
Ok(Some(source)) => {
|
||||||
script_key.clone(),
|
match engine.compile(source) {
|
||||||
CompiledScript {
|
Ok(ast) => {
|
||||||
has_init: defines("init", 1),
|
let defines = |n: &str, params: usize| {
|
||||||
has_tick: defines("tick", 2),
|
ast.iter_functions()
|
||||||
has_bump: defines("bump", 2),
|
.any(|f| f.name == n && f.params.len() == params)
|
||||||
has_grab: defines("grab", 1),
|
};
|
||||||
has_enter: defines("enter", 2),
|
scripts.insert(
|
||||||
ast,
|
script_key.clone(),
|
||||||
},
|
CompiledScript {
|
||||||
);
|
has_init: defines("init", 1),
|
||||||
}
|
has_tick: defines("tick", 2),
|
||||||
Err(err) => { // It didn't compile...
|
has_bump: defines("bump", 2),
|
||||||
failed.insert(script_key.clone());
|
has_grab: defines("grab", 1),
|
||||||
log_sink.error(format!("script '{}' failed to compile: {err}", script_key.name()));
|
has_enter: defines("enter", 2),
|
||||||
continue;
|
ast,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => { // It didn't compile...
|
||||||
|
failed.insert(script_key.clone());
|
||||||
|
log_sink.error(format!("script '{}' failed to compile: {err}", script_key.name()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// This has no source...
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,3 @@ fn blocked_reports_solid_and_clear() {
|
|||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(glyph(&game, id).tile, 7);
|
assert_eq!(glyph(&game, id).tile, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mod actions;
|
mod actions;
|
||||||
// TODO(migration): map_file is not yet ported — it is blocked on the map files
|
// TODO(migration): map_file is not yet ported — it is blocked on the map files
|
||||||
// themselves still being pre-BoardSpec (see todo.md #5).
|
// themselves still being pre-BoardSpec (see todo.md #1).
|
||||||
mod game_portals;
|
mod game_portals;
|
||||||
// mod map_file;
|
// mod map_file;
|
||||||
mod movement;
|
mod movement;
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ fn missing_hooks_and_no_script_are_noops() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compile_and_unknown_script_errors_are_logged() {
|
fn compile_and_unknown_script_errors_are_logged() {
|
||||||
// A reference to a script name that isn't in the pool. See todo.md #5: this is
|
// A reference to a script name that isn't in the world pool. `ScriptKey::None`
|
||||||
// currently a silent `continue` in ScriptHost::new, so nothing is logged.
|
// (an object with no script at all) must stay silent; only a named script that
|
||||||
|
// can't be resolved is an error.
|
||||||
let (board, scripts, _) = board_with_object(Some("ghost"), &[]);
|
let (board, scripts, _) = board_with_object(Some("ghost"), &[]);
|
||||||
let game = GameState::with_scripts(board, scripts);
|
let game = GameState::with_scripts(board, scripts);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -149,7 +150,7 @@ fn start_map_greeter_runs_init() {
|
|||||||
// Keep the failure message short: a TomlError's Display embeds the whole file.
|
// Keep the failure message short: a TomlError's Display embeds the whole file.
|
||||||
let mut world = crate::world::load(path).unwrap_or_else(|e| {
|
let mut world = crate::world::load(path).unwrap_or_else(|e| {
|
||||||
let first = e.to_string().lines().next().unwrap_or_default().to_string();
|
let first = e.to_string().lines().next().unwrap_or_default().to_string();
|
||||||
panic!("load start.toml failed (see todo.md #5, maps are still pre-BoardSpec): {first}")
|
panic!("load start.toml failed (see todo.md #1, maps are still pre-BoardSpec): {first}")
|
||||||
});
|
});
|
||||||
// Pin to the "start" board regardless of the world's current default entry point.
|
// Pin to the "start" board regardless of the world's current default entry point.
|
||||||
world.start = "start".to_string();
|
world.start = "start".to_string();
|
||||||
@@ -523,7 +524,7 @@ fn a_send_cycle_defers_instead_of_recursing() {
|
|||||||
// ── enter hook ──────────────────────────────────────────────────────────────
|
// ── enter hook ──────────────────────────────────────────────────────────────
|
||||||
// `enter(me, dir)` fires on a **sensor** when the player steps onto its cell, with
|
// `enter(me, dir)` fires on a **sensor** when the player steps onto its cell, with
|
||||||
// `dir` the side the player came from. This is the only remaining enter trigger:
|
// `dir` the side the player came from. This is the only remaining enter trigger:
|
||||||
// object movement, pushes, teleports and shifts dispatch no hooks (see todo.md).
|
// object movement, pushes, teleports and shifts dispatch no hooks at all.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_walking_onto_a_sensor_fires_enter_from_the_travel_side() {
|
fn player_walking_onto_a_sensor_fires_enter_from_the_travel_side() {
|
||||||
|
|||||||
+10
-4
@@ -135,11 +135,17 @@ impl ScriptKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source<'a>(&self, sources: &'a HashMap<String, String>) -> Option<&'a str> {
|
/// Attempt to find and return the source for the given script key:
|
||||||
|
/// - For `None`, return Ok(None) since there's not a script to find
|
||||||
|
/// - For `World`, return either Ok(Some(&str)) or Err if it's not in the given hashmap
|
||||||
|
/// - For `Builtin`, return Ok(Some(&str)) assuming the builtin is a valid name (Err in the unlikely case...)
|
||||||
|
pub fn source<'a>(&self, sources: &'a HashMap<String, String>) -> Result<Option<&'a str>, String> {
|
||||||
match self {
|
match self {
|
||||||
ScriptKey::None => None,
|
ScriptKey::None => Ok(None),
|
||||||
ScriptKey::World(name) => sources.get(name).map(String::as_str),
|
ScriptKey::World(name) => sources.get(name).map(String::as_str)
|
||||||
key @ ScriptKey::Builtin(_) => BUILTIN_SOURCES.get(key).copied(),
|
.map_or(Err(format!("unknown script '{name}'")), |s| Ok(Some(s))),
|
||||||
|
key @ ScriptKey::Builtin(name) => BUILTIN_SOURCES.get(key)
|
||||||
|
.map_or(Err(format!("No builtin script '{name}'")), |s| Ok(Some(*s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-21
@@ -181,29 +181,46 @@ mod tests {
|
|||||||
use super::{BoardWidget, DARKNESS_BG};
|
use super::{BoardWidget, DARKNESS_BG};
|
||||||
use crate::utils::rgba8_to_color;
|
use crate::utils::rgba8_to_color;
|
||||||
use kiln_core::Board;
|
use kiln_core::Board;
|
||||||
|
use kiln_core::board_spec::BoardSpec;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
use kiln_core::board_spec::BoardSpec;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Deserializes one `[boards.NAME]` table into a [`Board`].
|
||||||
|
///
|
||||||
|
/// The script pool passed to `into_board` is empty, so boards built here must be
|
||||||
|
/// script-free — it validates every object's `script` against that pool.
|
||||||
|
fn board_from(toml: &str) -> Board {
|
||||||
|
let spec: BoardSpec = toml::from_str(toml).expect("board spec parses");
|
||||||
|
spec.into_board(&HashSet::new()).expect("board spec converts")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An `{ r, g, b, a }` color table. `Glyph` derives serde straight onto
|
||||||
|
/// `color::Rgba8`, so map TOML spells colors out structurally rather than as
|
||||||
|
/// `"#rrggbb"` strings.
|
||||||
|
fn rgb(r: u8, g: u8, b: u8) -> String {
|
||||||
|
format!("{{ r = {r}, g = {g}, b = {b}, a = 255 }}")
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a tiny dark board: a 5×1 corridor with the player at the left end
|
/// Builds a tiny dark board: a 5×1 corridor with the player at the left end
|
||||||
/// and a wall at x=2 occluding the two cells behind it.
|
/// and a wall at x=2 occluding the two cells behind it.
|
||||||
fn dark_corridor() -> Board {
|
fn dark_corridor() -> Board {
|
||||||
let toml = r##"
|
board_from(
|
||||||
[map]
|
r##"
|
||||||
name = "corridor"
|
name = "corridor"
|
||||||
width = 5
|
width = 5
|
||||||
height = 1
|
height = 1
|
||||||
dark = true
|
dark = true
|
||||||
[grid]
|
grid = "@ # "
|
||||||
content = "@ # "
|
[palette."@"]
|
||||||
[grid.palette]
|
type = "player"
|
||||||
"@" = { kind = "player" }
|
[palette."#"]
|
||||||
"#" = { kind = "wall", tile = "#", fg = "#808080", bg = "#404040" }
|
type = "builtin"
|
||||||
"##;
|
kind = "wall"
|
||||||
let mf: BoardSpec = toml::from_str(toml).unwrap();
|
"##,
|
||||||
Board::try_from(mf).unwrap()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -243,21 +260,36 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn colored_object_light_tints_nearby_and_darkens_far() {
|
fn colored_object_light_tints_nearby_and_darkens_far() {
|
||||||
// A dark 6×1 corridor, no player torch: a single red light object at x=2
|
// A dark 6×1 corridor, no player torch: a single red light source at x=2
|
||||||
// (radius 2) tints its own cell red; a cell beyond its reach is darkness.
|
// (radius 2) tints its own cell red; a cell beyond its reach is darkness.
|
||||||
let toml = r##"
|
//
|
||||||
[map]
|
// The light is a Sensor, not a grid object: every grid object is solid now,
|
||||||
|
// and a lamp you can walk through belongs off-grid. The player sits at x=0
|
||||||
|
// (Board::lighting reads player_pos for the sightline) but carries no torch,
|
||||||
|
// so the sensor is the only light.
|
||||||
|
let board = board_from(&format!(
|
||||||
|
r##"
|
||||||
name = "lit"
|
name = "lit"
|
||||||
width = 6
|
width = 6
|
||||||
height = 1
|
height = 1
|
||||||
dark = true
|
dark = true
|
||||||
[grid]
|
grid = "@ "
|
||||||
sparse = [ { x = 2, y = 0, ch = "L" } ]
|
|
||||||
[grid.palette]
|
[palette."@"]
|
||||||
"L" = { kind = "object", tile = 1, fg = "#ff0000", bg = "#000000", solid = false, light = 2 }
|
type = "player"
|
||||||
"##;
|
|
||||||
let board = Board::try_from(toml::from_str::<BoardSpec>(toml).unwrap()).unwrap();
|
[[sensors]]
|
||||||
let fov = board.lighting(0); // no player torch — only the object lights
|
x = 2
|
||||||
|
y = 0
|
||||||
|
draw_layer = "Above"
|
||||||
|
opaque = false
|
||||||
|
glow = 2
|
||||||
|
glyph = {{ tile = 1, fg = {fg}, bg = {bg} }}
|
||||||
|
"##,
|
||||||
|
fg = rgb(255, 0, 0),
|
||||||
|
bg = rgb(0, 0, 0),
|
||||||
|
));
|
||||||
|
let fov = board.lighting(0); // no player torch — only the sensor lights
|
||||||
|
|
||||||
let area = Rect::new(0, 0, 6, 1);
|
let area = Rect::new(0, 0, 6, 1);
|
||||||
let mut buf = Buffer::empty(area);
|
let mut buf = Buffer::empty(area);
|
||||||
|
|||||||
Reference in New Issue
Block a user