Compare commits

..

5 commits

Author SHA1 Message Date
Alex Bethel daafe3b023 Add visibility simulation 2022-01-15 20:15:30 -06:00
Alex Bethel 9a2b24e006 Make text cursor follow player 2022-01-15 17:53:51 -06:00
Alex Bethel dd584d2d19 Avoid use of thread_rng outside of main 2022-01-15 14:57:24 -06:00
Alex Bethel aa7361df6b Work on restructuring world generator API
We need to be able to spawn entities from the generator function, both
as level props and as enemies; the previous API makes this impossible,
so I'm working on restructuring it. Not too happy with it yet, still
needs more work.
2022-01-15 14:49:15 -06:00
Alex Bethel 652db1241b Remove extra import 2022-01-15 14:20:33 -06:00
5 changed files with 174 additions and 33 deletions

View file

@ -1,22 +1,35 @@
use std::fmt::Display;
use pancurses::Window;
use rand::Rng;
use specs::prelude::*;
use crate::rooms;
use crate::{
components::{CharRender, Position},
rooms,
};
/// The size of a dungeon level, in tiles.
pub const LEVEL_SIZE: (usize, usize) = (80, 24);
/// A single level of the dungeon.
#[derive(Clone)]
pub struct DungeonLevel {
/// The tiles at every position in the level.
tiles: [[DungeonTile; LEVEL_SIZE.0]; LEVEL_SIZE.1],
/// The locations of the level's exits.
exits: LevelExits,
}
/// The entrances and exits from a level.
#[derive(Clone)]
pub struct LevelExits {
/// The location of each of the up-staircases.
upstairs: Vec<(i32, i32)>,
pub upstairs: Vec<(i32, i32)>,
/// The location of each of the down-staircases.
downstairs: Vec<(i32, i32)>,
pub downstairs: Vec<(i32, i32)>,
}
/// The smallest measurable independent location in the dungeon,
@ -49,32 +62,57 @@ impl DungeonTile {
}
impl DungeonLevel {
/// Creates a new level in a branch that has the given
/// configuration.
pub fn new() -> Self {
rooms::generate_level(100, &mut rand::thread_rng(), 1, 1)
}
/// Creates a new level with the given set of tiles, upstairs, and
/// downstairs.
pub fn from_raw_parts(
pub fn new(
tiles: [[DungeonTile; LEVEL_SIZE.0]; LEVEL_SIZE.1],
upstairs: Vec<(i32, i32)>,
downstairs: Vec<(i32, i32)>,
) -> Self {
Self {
tiles,
exits: LevelExits {
upstairs,
downstairs,
},
}
}
/// Draws a level on the display window.
pub fn draw(&self, win: &Window) {
/// Creates a new level and registers it with the given world.
pub fn generate_level(world: &mut World, rng: &mut impl Rng) -> LevelExits {
let level = rooms::generate_level(100, rng, 1, 1);
world.insert(level.clone()); // inefficient but whatever
// Spawn some zombies in the world.
for _ in 0..20 {
let (x, y) = (
rng.gen_range(0..LEVEL_SIZE.0 as _),
rng.gen_range(0..LEVEL_SIZE.1 as _),
);
if level.tile(x, y).is_navigable() {
world
.create_entity()
.with(Position { x, y })
.with(CharRender { glyph: 'Z' })
.build();
}
}
level.exits
}
/// 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 {
' '
});
}
}
}
@ -130,16 +168,6 @@ impl DungeonLevel {
pub fn tile(&self, x: i32, y: i32) -> &DungeonTile {
&self.tiles[y as usize][x as usize]
}
/// Gets the list of up-stairs.
pub fn upstairs(&self) -> &[(i32, i32)] {
&self.upstairs
}
/// Gets the list of down-stairs.
pub fn downstairs(&self) -> &[(i32, i32)] {
&self.downstairs
}
}
impl Display for DungeonLevel {

View file

@ -5,6 +5,7 @@ use level::DungeonLevel;
use pancurses::{endwin, initscr, noecho, Window};
use player::player_turn;
use rand::thread_rng;
use specs::prelude::*;
use systems::{MobSystem, TimeSystem};
@ -14,14 +15,15 @@ mod player;
mod rooms;
mod systems;
mod util;
mod visibility;
fn main() {
let mut world = World::new();
register_all(&mut world);
let level = DungeonLevel::new();
let spawn_pos = level.upstairs()[0];
let level = DungeonLevel::generate_level(&mut world, &mut thread_rng());
let spawn_pos = level.upstairs[0];
world.insert(level);

View file

@ -5,8 +5,9 @@ use specs::prelude::*;
use crate::{
components::{CharRender, MobAction, Mobile, Player, Position},
level::{DungeonLevel, DungeonTile},
level::DungeonLevel,
quit,
visibility::{visible, CellVisibility, Lighting},
};
/// Runs a player turn on the ECS, using the given `screen` for input
@ -85,11 +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) {
// screen.clear();
// Calculate the player's position.
let plrs = ecs.read_storage::<Player>();
let pos = ecs.read_storage::<Position>();
let (_plr, player_pos) = (&plrs, &pos)
.join()
.next()
.expect("Player must have a position");
// Draw the base level.
let level = ecs.fetch::<DungeonLevel>();
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::<CharRender>();
@ -98,8 +120,8 @@ fn render_screen(ecs: &mut World, screen: &mut Window) {
screen.mvaddch(pos.y as _, pos.x as _, render.glyph);
}
// Leave the cursor at the lower-left.
screen.mv(0, 0);
// Leave the cursor on the player's position.
screen.mv(player_pos.y, player_pos.x);
screen.refresh();
}

View file

@ -93,7 +93,7 @@ pub fn generate_level(
*slot = value;
}
DungeonLevel::from_raw_parts(data, upstairs, downstairs)
DungeonLevel::new(data, upstairs, downstairs)
}
/// The bounding box of a room.

89
src/visibility.rs Normal file
View file

@ -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<i32>,
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<dyn Iterator<Item = (i32, i32)>> {
// 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),
)
}
}