Compare commits

..

15 Commits

9 changed files with 630 additions and 319 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
*~

45
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'hellorust'",
"cargo": {
"args": [
"build",
"--bin=hellorust",
"--package=hellorust"
],
"filter": {
"name": "hellorust",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'hellorust'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=hellorust",
"--package=hellorust"
],
"filter": {
"name": "hellorust",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

506
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,6 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rltk = { version = "0.7.0" } rltk = { version = "0.8.0" }
specs = "0.16.1" specs = "0.16.1"
specs-derive = "0.4.1" specs-derive = "0.4.1"

35
src/components.rs Normal file
View File

@@ -0,0 +1,35 @@
use rltk::{Point, RGB};
use specs::prelude::*;
#[derive(Component)]
pub struct Position {
pub x: i32,
pub y: i32,
}
#[derive(Component)]
pub struct Renderable {
pub glyph: u16,
pub fg: RGB,
pub bg: RGB,
}
#[derive(Component, Debug)]
pub struct Player {}
#[derive(Component)]
pub struct Viewshed {
pub visible_tiles: Vec<Point>,
pub range: i32,
pub dirty: bool,
}
#[derive(Component, Debug)]
pub struct Monster {}
#[derive(Component, Debug)]
pub struct Name {
pub name : String
}

View File

@@ -1,56 +1,54 @@
use rltk::{Console, GameState, Rltk, VirtualKeyCode, RGB}; use rltk::{GameState, Point, Rltk, VirtualKeyCode, RGB};
use specs::prelude::*; use specs::prelude::*;
use std::cmp::{max, min}; use std::cmp::{max, min};
mod rect; mod rect;
pub use rect::*; pub use rect::*;
mod map; mod map;
pub use map::*; pub use map::*;
mod components;
pub use components::*;
mod visibility_system;
pub use visibility_system::VisibilitySystem;
mod monster_ai_system;
pub use monster_ai_system::MonsterAI;
#[macro_use] #[macro_use]
extern crate specs_derive; extern crate specs_derive;
#[derive(Component)] #[derive(PartialEq, Copy, Clone)]
pub struct Position { pub enum RunState {
x: i32, Paused,
y: i32, Running,
}
#[derive(Component)]
pub struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
} }
pub struct State { pub struct State {
ecs: World, ecs: World,
runstate: RunState,
} }
#[derive(Component)] pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
pub struct LeftMover {}
#[derive(Component, Debug)]
pub struct Player {}
fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
let mut positions = ecs.write_storage::<Position>(); let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>(); let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();
for (_player, pos) in (&mut players, &mut positions).join() { for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
let x = min(79, max(0, pos.x + delta_x)); let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
let y = min(49, max(0, pos.y + delta_y)); if map.tiles[destination_idx] != TileType::Wall {
if map.tile_at(x,y) != TileType::Wall { pos.x = min(79, max(0, pos.x + delta_x));
pos.x = x; pos.y = min(49, max(0, pos.y + delta_y));
pos.y = y; let mut ppos = ecs.write_resource::<Point>();
ppos.x = pos.x;
ppos.y = pos.y;
viewshed.dirty = true;
} }
} }
} }
pub fn player_input(gs: &mut State, ctx: &mut Rltk) { pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
// Player movement // Player movement
match ctx.key { match ctx.key {
None => {} // Nothing happened None => return RunState::Paused, // Nothing happened
Some(key) => match key { Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs), VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut gs.ecs), VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut gs.ecs),
@@ -64,30 +62,18 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) {
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs), VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs), VirtualKeyCode::Numpad2 => try_move_player(0, 1, &mut gs.ecs),
VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs), VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
_ => {} _ => return RunState::Paused,
}, },
} }
} RunState::Running
struct LeftWalker {}
impl<'a> System<'a> for LeftWalker {
type SystemData = (ReadStorage<'a, LeftMover>, WriteStorage<'a, Position>);
fn run(&mut self, (lefty, mut pos): Self::SystemData) {
for (_lefty, pos) in (&lefty, &mut pos).join() {
pos.x -= 1;
if pos.x < 0 {
pos.x = 79;
}
}
}
} }
impl State { impl State {
fn run_systems(&mut self) { fn run_systems(&mut self) {
let mut lw = LeftWalker {}; let mut vis = VisibilitySystem {};
lw.run_now(&self.ecs); vis.run_now(&self.ecs);
let mut mob = MonsterAI {};
mob.run_now(&self.ecs);
self.ecs.maintain(); self.ecs.maintain();
} }
} }
@@ -95,32 +81,78 @@ impl State {
impl GameState for State { impl GameState for State {
fn tick(&mut self, ctx: &mut Rltk) { fn tick(&mut self, ctx: &mut Rltk) {
ctx.cls(); ctx.cls();
if self.runstate == RunState::Running {
self.run_systems(); self.run_systems();
player_input(self, ctx); self.runstate = RunState::Paused;
let map = self.ecs.fetch::<Map>(); } else {
map.draw_map(ctx); self.runstate = player_input(self, ctx);
}
draw_map(&self.ecs, ctx);
let positions = self.ecs.read_storage::<Position>(); let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>(); let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();
for (pos, render) in (&positions, &renderables).join() { for (pos, render) in (&positions, &renderables).join() {
let idx = map.xy_idx(pos.x, pos.y);
if map.visible_tiles[idx] {
ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph); ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
} }
} }
}
} }
fn main() { pub fn draw_map(ecs: &World, ctx: &mut Rltk) {
let map = ecs.fetch::<Map>();
let mut y = 0;
let mut x = 0;
for (idx, tile) in map.tiles.iter().enumerate() {
// Render a tile depending upon the tile type, if revealed
if map.revealed_tiles[idx] {
let glyph;
let mut fg;
match tile {
TileType::Floor => {
glyph = rltk::to_cp437('.');
fg = RGB::from_f32(0.5, 0.5, 0.5);
}
TileType::Wall => {
glyph = rltk::to_cp437('#');
fg = RGB::from_f32(0.0, 1.0, 0.0);
}
}
if !map.visible_tiles[idx] {
fg = fg.to_greyscale()
}
ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
}
// Move the coordinates
x += 1;
if x >= map.width {
x = 0;
y += 1;
}
}
}
fn main() -> rltk::BError {
use rltk::RltkBuilder; use rltk::RltkBuilder;
let context = RltkBuilder::simple80x50() let context = RltkBuilder::simple80x50()
.with_title("Roguelike Tutorial") .with_title("Roguelike Tutorial")
.build(); .build()?;
let mut gs = State { ecs: World::new() }; let mut gs = State {
ecs: World::new(),
runstate: RunState::Running,
};
let map = Map::new_map_rooms_and_corridors(); let map = Map::new_map_rooms_and_corridors();
let (player_x, player_y) = map.rooms[0].center(); let (player_x, player_y) = map.rooms[0].center();
gs.ecs.register::<Position>(); gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>(); gs.ecs.register::<Renderable>();
gs.ecs.register::<LeftMover>();
gs.ecs.register::<Player>(); gs.ecs.register::<Player>();
gs.ecs.register::<Viewshed>();
gs.ecs.register::<Monster>();
gs.ecs.register::<Name>();
gs.ecs gs.ecs
.create_entity() .create_entity()
@@ -134,21 +166,46 @@ fn main() {
bg: RGB::named(rltk::BLACK), bg: RGB::named(rltk::BLACK),
}) })
.with(Player {}) .with(Player {})
.with(Viewshed {
visible_tiles: Vec::new(),
range: 8,
dirty: true,
})
.with(Name {
name: "Player".to_string(),
})
.build(); .build();
for i in 0..10 { let mut rng = rltk::RandomNumberGenerator::new();
for (idx, room) in map.rooms.iter().skip(1).enumerate() {
let (x, y) = room.center();
let (glyph, name): (u16, String) = match rng.roll_dice(1, 2) {
1 => (rltk::to_cp437('g'), "Goblin".to_string()),
_ => (rltk::to_cp437('o'), "Orc".to_string()),
};
gs.ecs gs.ecs
.create_entity() .create_entity()
.with(Position { x: i * 7, y: 20 }) .with(Position { x, y })
.with(Renderable { .with(Renderable {
glyph: rltk::to_cp437('☺'), glyph,
fg: RGB::named(rltk::RED), fg: RGB::named(rltk::RED),
bg: RGB::named(rltk::BLACK), bg: RGB::named(rltk::BLACK),
}) })
.with(LeftMover {}) .with(Viewshed {
visible_tiles: Vec::new(),
range: 8,
dirty: true,
})
.with(Monster {})
.with(Name {
name: format!("{} #{}", &name, idx),
})
.build(); .build();
} }
gs.ecs.insert(map);
rltk::main_loop(context, gs); gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y));
rltk::main_loop(context, gs)
} }

View File

@@ -1,5 +1,5 @@
use super::rect::*; use super::rect::*;
use rltk::{Console, RandomNumberGenerator, Rltk, RGB}; use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator};
use std::cmp::{max, min}; use std::cmp::{max, min};
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
@@ -7,12 +7,14 @@ pub enum TileType {
Wall, Wall,
Floor, Floor,
} }
#[derive(Default)]
pub struct Map { pub struct Map {
pub tiles: Vec<TileType>, pub tiles: Vec<TileType>,
pub rooms: Vec<Rect>, pub rooms: Vec<Rect>,
pub width: i32, pub width: i32,
pub height: i32, pub height: i32,
pub revealed_tiles: Vec<bool>,
pub visible_tiles: Vec<bool>,
} }
impl Map { impl Map {
@@ -20,14 +22,14 @@ impl Map {
((y * self.width) + x) as usize ((y * self.width) + x) as usize
} }
pub fn tile_at(&self, x: i32, y:i32) -> TileType { pub fn tile_at(&self, x: i32, y: i32) -> TileType {
self.tiles[self.xy_idx(x, y)] self.tiles[self.xy_idx(x, y)]
} }
fn apply_room_to_map(&mut self, room: &Rect) { fn apply_room_to_map(&mut self, room: &Rect) {
for x in room.x1..room.x2 { for x in room.x1..room.x2 {
for y in room.y1..room.y2 { for y in room.y1..room.y2 {
let idx = self.xy_idx(x,y); let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor self.tiles[idx] = TileType::Floor
} }
} }
@@ -35,14 +37,14 @@ impl Map {
fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) { fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) {
for x in min(x1, x2)..=max(x1, x2) { for x in min(x1, x2)..=max(x1, x2) {
let idx=self.xy_idx(x, y); let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor self.tiles[idx] = TileType::Floor
} }
} }
fn apply_vertical_tunnel(&mut self, x: i32, y1: i32, y2: i32) { fn apply_vertical_tunnel(&mut self, x: i32, y1: i32, y2: i32) {
for y in min(y1, y2)..=max(y1, y2) { for y in min(y1, y2)..=max(y1, y2) {
let idx=self.xy_idx(x, y); let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor self.tiles[idx] = TileType::Floor
} }
} }
@@ -53,6 +55,8 @@ impl Map {
rooms: Vec::new(), rooms: Vec::new(),
width: 80, width: 80,
height: 50, height: 50,
revealed_tiles: vec![false; 80 * 50],
visible_tiles: vec![false; 80 * 50],
}; };
const MAX_ROOMS: i32 = 30; const MAX_ROOMS: i32 = 30;
@@ -80,18 +84,10 @@ impl Map {
let r2_center = map.rooms[map.rooms.len() - 1].center(); let r2_center = map.rooms[map.rooms.len() - 1].center();
if rng.range(0, 2) == 1 { if rng.range(0, 2) == 1 {
map.apply_horizontal_tunnel(r1_center.0, r2_center.0, r1_center.1); map.apply_horizontal_tunnel(r1_center.0, r2_center.0, r1_center.1);
map.apply_vertical_tunnel( map.apply_vertical_tunnel(r2_center.0, r1_center.1, r2_center.1);
max(r1_center.0, r2_center.0),
r1_center.1,
r2_center.1,
);
} else { } else {
map.apply_vertical_tunnel(r1_center.0, r1_center.1, r2_center.1); map.apply_vertical_tunnel(r1_center.0, r1_center.1, r2_center.1);
map.apply_horizontal_tunnel( map.apply_horizontal_tunnel(r1_center.0, r2_center.0, r2_center.1);
r1_center.0,
r2_center.0,
max(r1_center.1, r2_center.1),
);
} }
} }
map.rooms.push(new_room); map.rooms.push(new_room);
@@ -101,38 +97,57 @@ impl Map {
map map
} }
pub fn draw_map(&self, ctx: &mut Rltk) { fn is_exit_valid(&self, x: i32, y: i32) -> bool {
let mut y = 0; if x < 1 || x > self.width - 1 || y < 1 || y > self.height - 1 {
let mut x = 0; return false;
for tile in self.tiles.iter() {
// Render a tile depending upon the tile type
match tile {
TileType::Floor => {
ctx.set(
x,
y,
RGB::from_f32(0.5, 0.5, 0.5),
RGB::from_f32(0., 0., 0.),
rltk::to_cp437('.'),
);
}
TileType::Wall => {
ctx.set(
x,
y,
RGB::from_f32(0.0, 1.0, 0.0),
RGB::from_f32(0., 0., 0.),
rltk::to_cp437('#'),
);
}
}
// Move the coordinates
x += 1;
if x > 79 {
x = 0;
y += 1;
}
} }
let idx = self.xy_idx(x, y);
self.tiles[idx as usize] != TileType::Wall
}
}
impl BaseMap for Map {
fn is_opaque(&self, idx: usize) -> bool {
self.tiles[idx as usize] == TileType::Wall
}
fn get_available_exits(&self, idx: usize) -> rltk::SmallVec<[(usize, f32); 10]> {
let mut exits = rltk::SmallVec::new();
let x = idx as i32 % self.width;
let y = idx as i32 / self.width;
let w = self.width as usize;
// Cardinal directions
if self.is_exit_valid(x - 1, y) {
exits.push((idx - 1, 1.0))
};
if self.is_exit_valid(x + 1, y) {
exits.push((idx + 1, 1.0))
};
if self.is_exit_valid(x, y - 1) {
exits.push((idx - w, 1.0))
};
if self.is_exit_valid(x, y + 1) {
exits.push((idx + w, 1.0))
};
// Diagonals
if self.is_exit_valid(x - 1, y - 1) {
exits.push(((idx - w) - 1, 1.45));
}
if self.is_exit_valid(x + 1, y - 1) {
exits.push(((idx - w) + 1, 1.45));
}
if self.is_exit_valid(x - 1, y + 1) {
exits.push(((idx + w) - 1, 1.45));
}
if self.is_exit_valid(x + 1, y + 1) {
exits.push(((idx + w) + 1, 1.45));
}
exits
}
}
impl Algorithm2D for Map {
fn dimensions(&self) -> Point {
Point::new(self.width, self.height)
} }
} }

24
src/monster_ai_system.rs Normal file
View File

@@ -0,0 +1,24 @@
use super::{Monster, Name, Viewshed};
use rltk::{console, Point};
use specs::prelude::*;
pub struct MonsterAI {}
impl<'a> System<'a> for MonsterAI {
type SystemData = (
ReadExpect<'a, Point>,
ReadStorage<'a, Viewshed>,
ReadStorage<'a, Monster>,
ReadStorage<'a, Name>,
);
fn run(&mut self, data: Self::SystemData) {
let (player_pos, viewshed, monster, name) = data;
for (viewshed, _monster, name) in (&viewshed, &monster, &name).join() {
if viewshed.visible_tiles.contains(&*player_pos) {
console::log(format!("Monster {} shouts insults", name.name));
}
}
}
}

46
src/visibility_system.rs Normal file
View File

@@ -0,0 +1,46 @@
extern crate specs;
use super::{Map, Player, Position, Viewshed};
use specs::prelude::*;
extern crate rltk;
use rltk::{field_of_view, Point};
pub struct VisibilitySystem {}
impl<'a> System<'a> for VisibilitySystem {
type SystemData = (
WriteExpect<'a, Map>,
Entities<'a>,
WriteStorage<'a, Viewshed>,
WriteStorage<'a, Position>,
ReadStorage<'a, Player>,
);
fn run(&mut self, data: Self::SystemData) {
let (mut map, entities, mut viewshed, pos, player) = data;
for (ent, viewshed, pos) in (&entities, &mut viewshed, &pos).join() {
if viewshed.dirty {
viewshed.visible_tiles.clear();
viewshed.visible_tiles =
field_of_view(Point::new(pos.x, pos.y), viewshed.range, &*map);
viewshed
.visible_tiles
.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1);
viewshed.dirty = false;
// If this is the player, reveal what they can see
let p: Option<&Player> = player.get(ent);
if let Some(_) = p {
for t in map.visible_tiles.iter_mut() {
*t = false
}
for vis in viewshed.visible_tiles.iter() {
let idx = map.xy_idx(vis.x, vis.y);
map.revealed_tiles[idx] = true;
map.visible_tiles[idx] = true;
}
}
}
}
}
}