Compare commits
No commits in common. "daafe3b023199e401c231cd5a478405988834c25" and "a5f1cca981d517f1a1a33989e6d0c341d73d5166" have entirely different histories.
daafe3b023
...
a5f1cca981
78
src/level.rs
78
src/level.rs
|
@ -1,35 +1,22 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use pancurses::Window;
|
use pancurses::Window;
|
||||||
use rand::Rng;
|
|
||||||
use specs::prelude::*;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::rooms;
|
||||||
components::{CharRender, Position},
|
|
||||||
rooms,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The size of a dungeon level, in tiles.
|
/// The size of a dungeon level, in tiles.
|
||||||
pub const LEVEL_SIZE: (usize, usize) = (80, 24);
|
pub const LEVEL_SIZE: (usize, usize) = (80, 24);
|
||||||
|
|
||||||
/// A single level of the dungeon.
|
/// A single level of the dungeon.
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DungeonLevel {
|
pub struct DungeonLevel {
|
||||||
/// The tiles at every position in the level.
|
/// The tiles at every position in the level.
|
||||||
tiles: [[DungeonTile; LEVEL_SIZE.0]; LEVEL_SIZE.1],
|
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.
|
/// The location of each of the up-staircases.
|
||||||
pub upstairs: Vec<(i32, i32)>,
|
upstairs: Vec<(i32, i32)>,
|
||||||
|
|
||||||
/// The location of each of the down-staircases.
|
/// The location of each of the down-staircases.
|
||||||
pub downstairs: Vec<(i32, i32)>,
|
downstairs: Vec<(i32, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The smallest measurable independent location in the dungeon,
|
/// The smallest measurable independent location in the dungeon,
|
||||||
|
@ -62,57 +49,32 @@ impl DungeonTile {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DungeonLevel {
|
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
|
/// Creates a new level with the given set of tiles, upstairs, and
|
||||||
/// downstairs.
|
/// downstairs.
|
||||||
pub fn new(
|
pub fn from_raw_parts(
|
||||||
tiles: [[DungeonTile; LEVEL_SIZE.0]; LEVEL_SIZE.1],
|
tiles: [[DungeonTile; LEVEL_SIZE.0]; LEVEL_SIZE.1],
|
||||||
upstairs: Vec<(i32, i32)>,
|
upstairs: Vec<(i32, i32)>,
|
||||||
downstairs: Vec<(i32, i32)>,
|
downstairs: Vec<(i32, i32)>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tiles,
|
tiles,
|
||||||
exits: LevelExits {
|
upstairs,
|
||||||
upstairs,
|
downstairs,
|
||||||
downstairs,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new level and registers it with the given world.
|
/// Draws a level on the display window.
|
||||||
pub fn generate_level(world: &mut World, rng: &mut impl Rng) -> LevelExits {
|
pub fn draw(&self, win: &Window) {
|
||||||
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 {
|
for y in 0..LEVEL_SIZE.1 {
|
||||||
win.mv(y as _, 0);
|
win.mv(y as _, 0);
|
||||||
for x in 0..LEVEL_SIZE.0 {
|
for x in 0..LEVEL_SIZE.0 {
|
||||||
win.addch(if filter((x as _, y as _)) {
|
win.addch(self.render_tile(x, y));
|
||||||
self.render_tile(x, y)
|
|
||||||
} else {
|
|
||||||
' '
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,6 +130,16 @@ impl DungeonLevel {
|
||||||
pub fn tile(&self, x: i32, y: i32) -> &DungeonTile {
|
pub fn tile(&self, x: i32, y: i32) -> &DungeonTile {
|
||||||
&self.tiles[y as usize][x as usize]
|
&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 {
|
impl Display for DungeonLevel {
|
||||||
|
|
|
@ -5,7 +5,6 @@ use level::DungeonLevel;
|
||||||
|
|
||||||
use pancurses::{endwin, initscr, noecho, Window};
|
use pancurses::{endwin, initscr, noecho, Window};
|
||||||
use player::player_turn;
|
use player::player_turn;
|
||||||
use rand::thread_rng;
|
|
||||||
use specs::prelude::*;
|
use specs::prelude::*;
|
||||||
use systems::{MobSystem, TimeSystem};
|
use systems::{MobSystem, TimeSystem};
|
||||||
|
|
||||||
|
@ -15,15 +14,14 @@ mod player;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
mod systems;
|
mod systems;
|
||||||
mod util;
|
mod util;
|
||||||
mod visibility;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut world = World::new();
|
let mut world = World::new();
|
||||||
|
|
||||||
register_all(&mut world);
|
register_all(&mut world);
|
||||||
|
|
||||||
let level = DungeonLevel::generate_level(&mut world, &mut thread_rng());
|
let level = DungeonLevel::new();
|
||||||
let spawn_pos = level.upstairs[0];
|
let spawn_pos = level.upstairs()[0];
|
||||||
|
|
||||||
world.insert(level);
|
world.insert(level);
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,8 @@ use specs::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{CharRender, MobAction, Mobile, Player, Position},
|
components::{CharRender, MobAction, Mobile, Player, Position},
|
||||||
level::DungeonLevel,
|
level::{DungeonLevel, DungeonTile},
|
||||||
quit,
|
quit,
|
||||||
visibility::{visible, CellVisibility, Lighting},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Runs a player turn on the ECS, using the given `screen` for input
|
/// Runs a player turn on the ECS, using the given `screen` for input
|
||||||
|
@ -86,32 +85,11 @@ fn possible(ecs: &World, action: &MobAction) -> bool {
|
||||||
|
|
||||||
/// Renders the state of the world onto the screen.
|
/// Renders the state of the world onto the screen.
|
||||||
fn render_screen(ecs: &mut World, screen: &mut Window) {
|
fn render_screen(ecs: &mut World, screen: &mut Window) {
|
||||||
// Calculate the player's position.
|
// screen.clear();
|
||||||
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.
|
// Draw the base level.
|
||||||
let level = ecs.fetch::<DungeonLevel>();
|
let level = ecs.fetch::<DungeonLevel>();
|
||||||
level.draw(screen, |cell| {
|
level.draw(screen);
|
||||||
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.
|
// Draw all renderable entities.
|
||||||
let renderables = ecs.read_storage::<CharRender>();
|
let renderables = ecs.read_storage::<CharRender>();
|
||||||
|
@ -120,8 +98,8 @@ fn render_screen(ecs: &mut World, screen: &mut Window) {
|
||||||
screen.mvaddch(pos.y as _, pos.x as _, render.glyph);
|
screen.mvaddch(pos.y as _, pos.x as _, render.glyph);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave the cursor on the player's position.
|
// Leave the cursor at the lower-left.
|
||||||
screen.mv(player_pos.y, player_pos.x);
|
screen.mv(0, 0);
|
||||||
|
|
||||||
screen.refresh();
|
screen.refresh();
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ pub fn generate_level(
|
||||||
*slot = value;
|
*slot = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
DungeonLevel::new(data, upstairs, downstairs)
|
DungeonLevel::from_raw_parts(data, upstairs, downstairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The bounding box of a room.
|
/// The bounding box of a room.
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
//! 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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue