Compare commits

..

19 Commits

Author SHA1 Message Date
d734593e73 chore: cargo upgrade 2020-06-29 20:22:48 -04:00
5d4cc88d34 add debug configuration 2020-04-25 11:08:14 -04:00
bf8204d1a3 update for new RLTK version 2020-04-17 20:35:31 -04:00
5593c4d14e add player name 2020-04-11 11:43:09 -04:00
fe626c8a33 turn based actions and monsters react on sight only 2020-04-03 21:41:49 -04:00
c454bab2b1 initial monster AI system 2020-04-01 23:28:53 -04:00
4f6151307c monster variety 2020-04-01 23:10:56 -04:00
3929824670 hide monsters when not within visible range 2020-03-30 21:42:41 -04:00
e240e08869 place simple monsters, add dirty flag for viewshed 2020-03-30 21:35:40 -04:00
222e2e5e74 visibile area highlighting 2020-03-29 20:36:52 -04:00
bafe3fcbc9 handling visibility, also fixed corridor issue 2020-03-29 15:48:52 -04:00
6a5514d6bb remove annoying left walker stuff 2020-03-28 22:34:19 -04:00
2727f417ea set up visibility system 2020-03-28 21:24:02 -04:00
419ab1ba35 move components to separate file 2020-03-28 21:12:18 -04:00
217c4f7c3a add backup files 2020-03-28 20:51:54 -04:00
648040095d refactor map into struct with impl trait 2020-03-28 19:57:47 -04:00
afdb14ca77 locate player in room 2020-03-27 22:43:36 -04:00
6132fa522b improving map slightly 2020-03-26 23:20:49 -04:00
ea5041d85a no longer walking through walls 2020-03-15 16:55:45 -04:00
10 changed files with 782 additions and 353 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,199 +1,211 @@
use rltk::{Console, GameState, Rltk, RGB, VirtualKeyCode}; 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;
pub use rect::*;
mod 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)]
struct Position {
x: i32,
y: i32,
}
#[derive(Component)]
struct Renderable {
glyph: u8,
fg: RGB,
bg: RGB,
}
struct State {
ecs: World
}
#[derive(Component)]
struct LeftMover {}
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
enum TileType { pub enum RunState {
Wall, Floor Paused,
Running,
} }
pub fn xy_idx(x: i32, y: i32) -> usize { pub struct State {
(y as usize * 80) + x as usize ecs: World,
runstate: RunState,
} }
fn new_map() -> Vec<TileType> { pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
let mut map = vec![TileType::Floor; 80*50]; let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut viewsheds = ecs.write_storage::<Viewshed>();
let map = ecs.fetch::<Map>();
// Make the boundaries walls for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
for x in 0..80 { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
map[xy_idx(x, 0)] = TileType::Wall; if map.tiles[destination_idx] != TileType::Wall {
map[xy_idx(x, 49)] = TileType::Wall; pos.x = min(79, max(0, pos.x + delta_x));
} pos.y = min(49, max(0, pos.y + delta_y));
for y in 0..50 { let mut ppos = ecs.write_resource::<Point>();
map[xy_idx(0, y)] = TileType::Wall; ppos.x = pos.x;
map[xy_idx(79, y)] = TileType::Wall; ppos.y = pos.y;
} viewshed.dirty = true;
// Now we'll randomly splat a bunch of walls. It won't be pretty, but it's a decent illustration.
// First, obtain the thread-local RNG:
let mut rng = rltk::RandomNumberGenerator::new();
for _i in 0..400 {
let x = rng.roll_dice(1, 79);
let y = rng.roll_dice(1, 49);
let idx = xy_idx(x, y);
if idx != xy_idx(40, 25) {
map[idx] = TileType::Wall;
} }
} }
map
} }
pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
// Player movement
match ctx.key {
None => return RunState::Paused, // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad4 => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Numpad6 => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Numpad8 => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::K => 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::J => try_move_player(0, 1, &mut gs.ecs),
_ => return RunState::Paused,
},
}
RunState::Running
}
impl State {
fn run_systems(&mut self) {
let mut vis = VisibilitySystem {};
vis.run_now(&self.ecs);
let mut mob = MonsterAI {};
mob.run_now(&self.ecs);
self.ecs.maintain();
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut Rltk) {
ctx.cls();
if self.runstate == RunState::Running {
self.run_systems();
self.runstate = RunState::Paused;
} else {
self.runstate = player_input(self, ctx);
}
draw_map(&self.ecs, ctx);
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
let map = self.ecs.fetch::<Map>();
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);
}
}
}
}
pub fn draw_map(ecs: &World, ctx: &mut Rltk) {
let map = ecs.fetch::<Map>();
fn draw_map(map: &[TileType], ctx : &mut Rltk) {
let mut y = 0; let mut y = 0;
let mut x = 0; let mut x = 0;
for tile in map.iter() { for (idx, tile) in map.tiles.iter().enumerate() {
// Render a tile depending upon the tile type // Render a tile depending upon the tile type, if revealed
match tile { if map.revealed_tiles[idx] {
TileType::Floor => { let glyph;
ctx.set(x, y, RGB::from_f32(0.5, 0.5, 0.5), RGB::from_f32(0., 0., 0.), rltk::to_cp437('.')); 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);
}
} }
TileType::Wall => { if !map.visible_tiles[idx] {
ctx.set(x, y, RGB::from_f32(0.0, 1.0, 0.0), RGB::from_f32(0., 0., 0.), rltk::to_cp437('#')); fg = fg.to_greyscale()
} }
ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
} }
// Move the coordinates // Move the coordinates
x += 1; x += 1;
if x > 79 { if x >= map.width {
x = 0; x = 0;
y += 1; y += 1;
} }
} }
} }
fn main() -> rltk::BError {
#[derive(Component, Debug)]
struct Player {}
fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
for (_player, pos) in (&mut players, &mut positions).join() {
pos.x = min(79 , max(0, pos.x + delta_x));
pos.y = min(49, max(0, pos.y + delta_y));
}
}
fn player_input(gs: &mut State, ctx: &mut Rltk) {
// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs),
VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs),
VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs),
VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs),
_ => {}
},
}
}
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 {
fn run_systems(&mut self) {
let mut lw = LeftWalker{};
lw.run_now(&self.ecs);
self.ecs.maintain();
}
}
impl GameState for State {
fn tick(&mut self, ctx : &mut Rltk) {
ctx.cls();
self.run_systems();
player_input(self, ctx);
let map = self.ecs.fetch::<Vec<TileType>>();
draw_map(&map, ctx);
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
for (pos, render) in (&positions, &renderables).join() {
ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
}
}
fn main() {
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 { let mut gs = State {
ecs: World::new() ecs: World::new(),
runstate: RunState::Running,
}; };
let map = Map::new_map_rooms_and_corridors();
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()
.with(Position { x: 40, y: 25 }) .with(Position {
x: player_x,
y: player_y,
})
.with(Renderable { .with(Renderable {
glyph: rltk::to_cp437('@'), glyph: rltk::to_cp437('@'),
fg: RGB::named(rltk::YELLOW), fg: RGB::named(rltk::YELLOW),
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 {
.build(); visible_tiles: Vec::new(),
range: 8,
dirty: true,
})
.with(Monster {})
.with(Name {
name: format!("{} #{}", &name, idx),
})
.build();
} }
gs.ecs.insert(new_map()); gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y));
rltk::main_loop(context, gs);
}
rltk::main_loop(context, gs)
}

153
src/map.rs Normal file
View File

@@ -0,0 +1,153 @@
use super::rect::*;
use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator};
use std::cmp::{max, min};
#[derive(PartialEq, Copy, Clone)]
pub enum TileType {
Wall,
Floor,
}
#[derive(Default)]
pub struct Map {
pub tiles: Vec<TileType>,
pub rooms: Vec<Rect>,
pub width: i32,
pub height: i32,
pub revealed_tiles: Vec<bool>,
pub visible_tiles: Vec<bool>,
}
impl Map {
pub fn xy_idx(&self, x: i32, y: i32) -> usize {
((y * self.width) + x) as usize
}
pub fn tile_at(&self, x: i32, y: i32) -> TileType {
self.tiles[self.xy_idx(x, y)]
}
fn apply_room_to_map(&mut self, room: &Rect) {
for x in room.x1..room.x2 {
for y in room.y1..room.y2 {
let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor
}
}
}
fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) {
for x in min(x1, x2)..=max(x1, x2) {
let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor
}
}
fn apply_vertical_tunnel(&mut self, x: i32, y1: i32, y2: i32) {
for y in min(y1, y2)..=max(y1, y2) {
let idx = self.xy_idx(x, y);
self.tiles[idx] = TileType::Floor
}
}
pub fn new_map_rooms_and_corridors() -> Map {
let mut map = Map {
tiles: vec![TileType::Wall; 80 * 50],
rooms: Vec::new(),
width: 80,
height: 50,
revealed_tiles: vec![false; 80 * 50],
visible_tiles: vec![false; 80 * 50],
};
const MAX_ROOMS: i32 = 30;
const MIN_SIZE: i32 = 6;
const MAX_SIZE: i32 = 10;
let mut rng = RandomNumberGenerator::new();
for _ in 0..MAX_ROOMS {
let w = rng.range(MIN_SIZE, MAX_SIZE);
let h = rng.range(MIN_SIZE, MAX_SIZE);
let x = rng.roll_dice(1, 80 - w - 1) - 1;
let y = rng.roll_dice(1, 50 - h - 1) - 1;
let new_room = Rect::new(x, y, w, h);
let mut ok = true;
for other_room in map.rooms.iter() {
if new_room.intersect(other_room) {
ok = false
}
}
if ok {
map.apply_room_to_map(&new_room);
if map.rooms.len() > 0 {
let r1_center = new_room.center();
let r2_center = map.rooms[map.rooms.len() - 1].center();
if rng.range(0, 2) == 1 {
map.apply_horizontal_tunnel(r1_center.0, r2_center.0, r1_center.1);
map.apply_vertical_tunnel(r2_center.0, r1_center.1, r2_center.1);
} else {
map.apply_vertical_tunnel(r1_center.0, r1_center.1, r2_center.1);
map.apply_horizontal_tunnel(r1_center.0, r2_center.0, r2_center.1);
}
}
map.rooms.push(new_room);
}
}
map
}
fn is_exit_valid(&self, x: i32, y: i32) -> bool {
if x < 1 || x > self.width - 1 || y < 1 || y > self.height - 1 {
return false;
}
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));
}
}
}
}

25
src/rect.rs Normal file
View File

@@ -0,0 +1,25 @@
pub struct Rect {
pub x1: i32,
pub x2: i32,
pub y1: i32,
pub y2: i32,
}
impl Rect {
pub fn new(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x1: x,
y1: y,
x2: x + w,
y2: y + h,
}
}
pub fn intersect(&self, other: &Rect) -> bool {
self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1
}
pub fn center(&self) -> (i32, i32) {
((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)
}
}

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;
}
}
}
}
}
}