diff --git a/Cargo.lock b/Cargo.lock index 395ec46..dc00aa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,22 +14,82 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "lazy_static" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" +[[package]] +name = "libc" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" + [[package]] name = "pkg-config" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "roguelike" version = "0.1.0" dependencies = [ + "rand 0.3.23", "tcod", ] @@ -53,3 +113,25 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 0d440b6..450bdaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ edition = "2018" [dependencies] tcod = "*" +rand = "0.3.9" diff --git a/src/main.rs b/src/main.rs index 78a23ef..e264a72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,11 @@ #![allow(unused_mut)] #![allow(dead_code)] +use rand::Rng; use std::cmp; use tcod::colors::*; use tcod::console::*; +use tcod::map::{FovAlgorithm, Map as FovMap}; const SCREEN_WIDTH: i32 = 80; const SCREEN_HEIGHT: i32 = 50; @@ -13,232 +15,325 @@ const FPS: i32 = 20; const MAP_WIDTH: i32 = 80; const MAP_HEIGHT: i32 = 45; const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 }; -const COLOR_DARK_GROUND: Color = Color { - r: 50, - g: 50, - b: 150, +const COLOR_LIGHT_WALL: Color = Color { + r: 130, + g: 110, + b: 50, }; +const COLOR_DARK_GROUND: Color = Color { + r: 50, + g: 50, + b: 150, +}; +const COLOR_LIGHT_GROUND: Color = Color { + r: 200, + g: 180, + b: 50, +}; +const ROOM_MAX_SIZE: i32 = 10; +const ROOM_MIN_SIZE: i32 = 6; +const MAX_ROOMS: i32 = 30; +const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic; +const FOV_LIGHT_WALLS: bool = true; +const TORCH_RADIUS: i32 = 10; struct Tcod { - root: Root, - con: Offscreen, + root: Root, + con: Offscreen, + fov: FovMap, } #[derive(Debug)] struct Object { - x: i32, - y: i32, - char: char, - color: Color, + x: i32, + y: i32, + char: char, + color: Color, } impl Object { - pub fn new(x: i32, y: i32, char: char, color: Color) -> Self { - Object { x, y, char, color } - } + pub fn new(x: i32, y: i32, char: char, color: Color) -> Self { + Object { x, y, char, color } + } - // Move by given amount, if the destination is not blocked - pub fn move_by(&mut self, dx: i32, dy: i32, game: &Game) { - if !game.map[(self.x + dx) as usize][(self.y + dy) as usize].blocked { - self.x += dx; - self.y += dy; - } + // Move by given amount, if the destination is not blocked + pub fn move_by(&mut self, dx: i32, dy: i32, game: &Game) { + if !game.map[(self.x + dx) as usize][(self.y + dy) as usize].blocked { + self.x += dx; + self.y += dy; } + } - // Set the color and then draw the character that represents this object at its position - pub fn draw(&self, con: &mut dyn Console) { - con.set_default_foreground(self.color); - con.put_char(self.x, self.y, self.char, BackgroundFlag::None); - } + // Set the color and then draw the character that represents this object at its position + pub fn draw(&self, con: &mut dyn Console) { + con.set_default_foreground(self.color); + con.put_char(self.x, self.y, self.char, BackgroundFlag::None); + } } #[derive(Clone, Copy, Debug)] struct Tile { - blocked: bool, - block_sight: bool, + blocked: bool, + explored: bool, + block_sight: bool, } impl Tile { - pub fn empty() -> Self { - Tile { - blocked: false, - block_sight: false, - } + pub fn empty() -> Self { + Tile { + blocked: false, + explored: false, + block_sight: false, } + } - pub fn wall() -> Self { - Tile { - blocked: true, - block_sight: true, - } + pub fn wall() -> Self { + Tile { + blocked: true, + explored: false, + block_sight: true, } + } } type Map = Vec>; struct Game { - map: Map, + map: Map, } // A rectangle on the map, used to characterise a room. #[derive(Clone, Copy, Debug)] struct Rect { - x1: i32, - y1: i32, - x2: i32, - y2: i32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, } impl Rect { - pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { - Rect { - x1: x, - y1: y, - x2: x + w, - y2: y + h, - } + pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { + Rect { + x1: x, + y1: y, + x2: x + w, + y2: y + h, } + } + + pub fn center(&self) -> (i32, i32) { + let center_x = (self.x1 + self.x2) / 2; + let center_y = (self.y1 + self.y2) / 2; + (center_x, center_y) + } + + pub fn intersects_with(&self, other: &Rect) -> bool { + (self.x1 <= other.x2) && (self.x2 >= other.x1) && (self.y1 <= other.y2) && (self.y2 >= other.y1) + } } fn handle_keys(tcod: &mut Tcod, game: &Game, player: &mut Object) -> bool { - use tcod::input::Key; - use tcod::input::KeyCode::*; + use tcod::input::Key; + use tcod::input::KeyCode::*; - let key = tcod.root.wait_for_keypress(true); - match key { - Key { - code: Enter, - alt: true, - .. - } => { - // Alt+Enter: toggle fullscreen - let fullscreen = tcod.root.is_fullscreen(); - tcod.root.set_fullscreen(!fullscreen); - } - Key { code: Escape, .. } => return true, // exit game - - Key { code: Up, .. } => player.move_by(0, -1, game), - Key { code: Down, .. } => player.move_by(0, 1, game), - Key { code: Left, .. } => player.move_by(-1, 0, game), - Key { code: Right, .. } => player.move_by(1, 0, game), - - _ => {} + let key = tcod.root.wait_for_keypress(true); + match key { + Key { + code: Enter, + alt: true, + .. + } => { + // Alt+Enter: toggle fullscreen + let fullscreen = tcod.root.is_fullscreen(); + tcod.root.set_fullscreen(!fullscreen); } - false + Key { code: Escape, .. } => return true, // exit game + + Key { code: Up, .. } => player.move_by(0, -1, game), + Key { code: Down, .. } => player.move_by(0, 1, game), + Key { code: Left, .. } => player.move_by(-1, 0, game), + Key { code: Right, .. } => player.move_by(1, 0, game), + + _ => {} + } + false } -fn make_map() -> Map { - // Fill the map with unblocked tiles - let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; +fn make_map(player: &mut Object) -> Map { + // Fill the map with unblocked tiles + let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; - // Create two rooms - let room1 = Rect::new(20, 15, 10, 15); - let room2 = Rect::new(50, 15, 10, 15); - create_room(room1, &mut map); - create_room(room2, &mut map); - create_h_tunnel(25, 55, 23, &mut map); + let mut rooms = vec![]; - map + for _ in 0..MAX_ROOMS { + let w = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); + let h = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); + let x = rand::thread_rng().gen_range(0, MAP_WIDTH - w); + let y = rand::thread_rng().gen_range(0, MAP_HEIGHT - h); + + let new_room = Rect::new(x, y, w, h); + let failed = rooms + .iter() + .any(|other_room| new_room.intersects_with(other_room)); + + if !failed { + create_room(new_room, &mut map); + let (new_x, new_y) = new_room.center(); + + if rooms.is_empty() { + player.x = new_x; + player.y = new_y; + } else { + let (prev_x, prev_y) = rooms[rooms.len() - 1].center(); + + if rand::random() { + create_h_tunnel(prev_x, new_x, prev_y, &mut map); + create_v_tunnel(prev_y, new_y, new_x, &mut map); + } else { + create_v_tunnel(prev_y, new_y, prev_x, &mut map); + create_h_tunnel(prev_x, new_x, new_y, &mut map); + } + } + rooms.push(new_room); + } + } + + map } fn create_room(room: Rect, map: &mut Map) { - // Go through the tiles in the rectangle and make them passable - for x in (room.x1 + 1)..room.x2 { - for y in (room.y1 + 1)..room.y2 { - map[x as usize][y as usize] = Tile::empty(); - } + // Go through the tiles in the rectangle and make them passable + for x in (room.x1 + 1)..room.x2 { + for y in (room.y1 + 1)..room.y2 { + map[x as usize][y as usize] = Tile::empty(); } + } } fn create_h_tunnel(x1: i32, x2: i32, y: i32, map: &mut Map) { - // Horizontal tunnel. `min()` and `max()` are used in case - // `x1 > x2` - for x in cmp::min(x1, x2)..(cmp::max(x1, x2) + 1) { - map[x as usize][y as usize] = Tile::empty(); - } + // Horizontal tunnel. `min()` and `max()` are used in case + // `x1 > x2` + for x in cmp::min(x1, x2)..(cmp::max(x1, x2) + 1) { + map[x as usize][y as usize] = Tile::empty(); + } } fn create_v_tunnel(y1: i32, y2: i32, x: i32, map: &mut Map) { - // Vertical tunnel - for y in cmp::min(y1, y2)..(cmp::max(y1, y2) + 1) { - map[x as usize][y as usize] = Tile::empty(); - } + // Vertical tunnel + for y in cmp::min(y1, y2)..(cmp::max(y1, y2) + 1) { + map[x as usize][y as usize] = Tile::empty(); + } } -fn render_all(tcod: &mut Tcod, game: &Game, objects: &[Object]) { - // Draw all objects in the list - for object in objects { - object.draw(&mut tcod.con); - } +fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) { + if fov_recompute { + let player = &objects[0]; + tcod + .fov + .compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO); + } - // Go through all tiles and set their background color - for y in 0..MAP_HEIGHT { - for x in 0..MAP_WIDTH { - let wall = game.map[x as usize][y as usize].block_sight; - if wall { - tcod.con - .set_char_background(x, y, COLOR_DARK_WALL, BackgroundFlag::Set); - } else { - tcod.con - .set_char_background(x, y, COLOR_DARK_GROUND, BackgroundFlag::Set) - } - } + // Draw all objects in the list + for object in objects { + if tcod.fov.is_in_fov(object.x, object.y) { + object.draw(&mut tcod.con); } + } - // Blit contents of "con" to the root console - blit( - &tcod.con, - (0, 0), - (MAP_WIDTH, MAP_HEIGHT), - &mut tcod.root, - (0, 0), - 1.0, - 1.0, - ); + // Go through all tiles and set their background color + for y in 0..MAP_HEIGHT { + for x in 0..MAP_WIDTH { + let visible = tcod.fov.is_in_fov(x, y); + let wall = game.map[x as usize][y as usize].block_sight; + let color = match (visible, wall) { + // Outside fov: + (false, true) => COLOR_DARK_WALL, + (false, false) => COLOR_DARK_GROUND, + // INside fov: + (true, true) => COLOR_LIGHT_WALL, + (true, false) => COLOR_LIGHT_GROUND, + }; + let explored = &mut game.map[x as usize][y as usize].explored; + if visible { + *explored = true; + } + if *explored { + tcod + .con + .set_char_background(x, y, color, BackgroundFlag::Set); + } + } + } + + // Blit contents of "con" to the root console + blit( + &tcod.con, + (0, 0), + (MAP_WIDTH, MAP_HEIGHT), + &mut tcod.root, + (0, 0), + 1.0, + 1.0, + ); } fn main() { - tcod::system::set_fps(FPS); + tcod::system::set_fps(FPS); - let root = Root::initializer() - .font("arial10x10.png", FontLayout::Tcod) - .font_type(FontType::Greyscale) - .size(SCREEN_WIDTH, SCREEN_HEIGHT) - .title("Rust/libtcod tutorial") - .init(); + let root = Root::initializer() + .font("arial10x10.png", FontLayout::Tcod) + .font_type(FontType::Greyscale) + .size(SCREEN_WIDTH, SCREEN_HEIGHT) + .title("Rust/libtcod tutorial") + .init(); - let con = Offscreen::new(MAP_WIDTH, MAP_HEIGHT); + let mut tcod = Tcod { + root, + con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT), + fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT), + }; - let mut tcod = Tcod { root, con }; + // Create player object + let player = Object::new(0, 0, '@', WHITE); - // Create player object - let player = Object::new(25, 23, '@', WHITE); + // // Create an NPC + let npc = Object::new(SCREEN_WIDTH / 2 - 5, SCREEN_HEIGHT / 2, '@', YELLOW); - // // Create an NPC - let npc = Object::new(SCREEN_WIDTH / 2 - 5, SCREEN_HEIGHT / 2, '@', YELLOW); + // List of objects in the world + let mut objects = [player, npc]; - // List of objects in the world - let mut objects = [player, npc]; + let mut game = Game { + // Generate map + map: make_map(&mut objects[0]), + }; - let game = Game { - // Generate map - map: make_map(), - }; - - while !tcod.root.window_closed() { - tcod.con.clear(); - - for object in &objects { - object.draw(&mut tcod.con) - } - - render_all(&mut tcod, &game, &objects); - tcod.root.flush(); - - let player = &mut objects[0]; - let exit = handle_keys(&mut tcod, &game, player); - if exit { - break; - } + // Populate FOV map, according to generated map + for y in 0..MAP_HEIGHT { + for x in 0..MAP_WIDTH { + tcod.fov.set( + x, + y, + !game.map[x as usize][y as usize].block_sight, + !game.map[x as usize][y as usize].blocked, + ) } + } + + let mut previous_player_position = (-1, -1); + + // Main game loop + while !tcod.root.window_closed() { + tcod.con.clear(); + + let fov_recompute = previous_player_position != (objects[0].x, objects[0].y); + render_all(&mut tcod, &mut game, &objects, fov_recompute); + tcod.root.flush(); + + let player = &mut objects[0]; + previous_player_position = (player.x, player.y); + let exit = handle_keys(&mut tcod, &game, player); + if exit { + break; + } + } }