From daafe3b023199e401c231cd5a478405988834c25 Mon Sep 17 00:00:00 2001 From: Alex Bethel Date: Sat, 15 Jan 2022 20:15:17 -0600 Subject: [PATCH] Add visibility simulation --- src/level.rs | 12 +++++-- src/main.rs | 1 + src/player.rs | 31 ++++++++++++++--- src/visibility.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/visibility.rs diff --git a/src/level.rs b/src/level.rs index 604d59a..e72a3e9 100644 --- a/src/level.rs +++ b/src/level.rs @@ -101,12 +101,18 @@ impl DungeonLevel { level.exits } - /// Draws a level on the display window. - pub fn draw(&self, win: &Window) { + /// Draws a level on the display window. Draws only the cells for + /// which `filter` returns true; use `|_| true` to draw the whole + /// level. + pub fn draw(&self, win: &Window, filter: impl Fn((i32, i32)) -> bool) { for y in 0..LEVEL_SIZE.1 { win.mv(y as _, 0); for x in 0..LEVEL_SIZE.0 { - win.addch(self.render_tile(x, y)); + win.addch(if filter((x as _, y as _)) { + self.render_tile(x, y) + } else { + ' ' + }); } } } diff --git a/src/main.rs b/src/main.rs index 163077c..991808f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod player; mod rooms; mod systems; mod util; +mod visibility; fn main() { let mut world = World::new(); diff --git a/src/player.rs b/src/player.rs index 786115a..053ad1b 100644 --- a/src/player.rs +++ b/src/player.rs @@ -7,6 +7,7 @@ use crate::{ components::{CharRender, MobAction, Mobile, Player, Position}, level::DungeonLevel, quit, + visibility::{visible, CellVisibility, Lighting}, }; /// Runs a player turn on the ECS, using the given `screen` for input @@ -85,9 +86,32 @@ fn possible(ecs: &World, action: &MobAction) -> bool { /// Renders the state of the world onto the screen. fn render_screen(ecs: &mut World, screen: &mut Window) { + // Calculate the player's position. + let plrs = ecs.read_storage::(); + let pos = ecs.read_storage::(); + let (_plr, player_pos) = (&plrs, &pos) + .join() + .next() + .expect("Player must have a position"); + // Draw the base level. let level = ecs.fetch::(); - level.draw(screen); + level.draw(screen, |cell| { + visible( + (player_pos.x, player_pos.y), + cell, + Some(10), + |(x, y)| { + if level.tile(x, y).is_navigable() { + CellVisibility::Transparent + } else { + CellVisibility::Blocking + } + }, + // Level is fully lit for now. + |(_x, _y)| Lighting::Lit, + ) + }); // Draw all renderable entities. let renderables = ecs.read_storage::(); @@ -97,10 +121,7 @@ fn render_screen(ecs: &mut World, screen: &mut Window) { } // Leave the cursor on the player's position. - let plrs = ecs.read_storage::(); - let pos = ecs.read_storage::(); - let (_plr, pos) = (&plrs, &pos).join().next().unwrap(); - screen.mv(pos.y, pos.x); + screen.mv(player_pos.y, player_pos.x); screen.refresh(); } diff --git a/src/visibility.rs b/src/visibility.rs new file mode 100644 index 0000000..dd79990 --- /dev/null +++ b/src/visibility.rs @@ -0,0 +1,89 @@ +//! Code for determining which cells the player and monsters can see. + +/// The light transmission properties of a cell in the world. +#[derive(Debug, PartialEq)] +pub enum CellVisibility { + /// This cell allows light to pass through: monsters can see + /// through this cell as if it is air. + Transparent, + + /// This cell blocks all light. + Blocking, +} + +/// How well-lit a cell is. +#[derive(Debug, PartialEq)] +pub enum Lighting { + /// Monsters can only see in this cell if the cell is immediately + /// adjacent to the monster. + Dark, + + /// Monsters can see in this cell from far away. + Lit, +} + +/// Calculates whether a monster standing at `origin` can see the +/// contents of cell `cell`. We assume the monster can see `radius` +/// cells away at best (None for unlimited range), that `cell_map` +/// represents whether a cell transmits light, and that `light_map` +/// represents how well-lit a cell is. +pub fn visible( + origin: (i32, i32), + cell: (i32, i32), + radius: Option, + cell_map: impl Fn((i32, i32)) -> CellVisibility, + light_map: impl Fn((i32, i32)) -> Lighting, +) -> bool { + let dx = cell.0 - origin.0; + let dy = cell.1 - origin.1; + + radius + .map(|radius| dx * dx + dy * dy < radius * radius) + .unwrap_or(true) + && (light_map(cell) == Lighting::Lit) + && (line(origin, cell).all(|tile| cell_map(tile) == CellVisibility::Transparent)) +} + +/// Constructs an iterator over the cells in a straight line from +/// `start` to `end`. The line will include `start`, but not `end`. +fn line(start: (i32, i32), end: (i32, i32)) -> Box> { + // We could use a dedicated iterator type here eventually and + // avoid the `Box` allocations, but I'm gonna assume it's not a + // significant problem until proven otherwise. + + let dx = end.0 - start.0; + let dy = end.1 - start.1; + + // Transform the world so we're working from left to right, with + // slope magnitude less than 1. + if dx.abs() < dy.abs() { + Box::new(line((start.1, start.0), (end.1, end.0)).map(|(x, y)| (y, x))) + } else if dx < 0 { + Box::new(line((-start.0, start.1), (-end.0, end.1)).map(|(x, y)| (-x, y))) + } else { + // Move the destination over by 0.5 cells on each axis, to + // navigate to the corner rather than the center of the target + // cell. It's weird but it makes things work way better. + let dx = dx as f64 - 0.5; + let dy = if dy > 0 { + dy as f64 - 0.5 + } else if dy < 0 { + dy as f64 + 0.5 + } else { + dy as f64 + }; + + // Now use float math to step along the line, one cell at a + // time. + let slope = dy as f64 / dx as f64; + Box::new( + std::iter::successors(Some((start.0, start.1 as f64)), move |&(x, y)| { + Some((x + 1, y + slope)) + }) + // Add 0.5 here to round to nearest rather than rounding + // towards zero (eliminates some bias). + .map(|(x, y)| (x, (y + 0.5) as i32)) + .take_while(move |(x, _y)| x < &end.0), + ) + } +}