├── .gitignore ├── arial10x10.png ├── prestige10x10.png ├── terminal10x10.png ├── terminal8x12_gs_tc.png ├── terminal8x8_gs_ro.png ├── consolas10x10_gs_tc.png ├── consolas12x12_gs_tc.png ├── screenshots ├── sorted_connectors.png ├── 2019-07-06-prototype.gif ├── randomly_collected_connectors.png ├── successful-seeded-generator.png ├── Screenshot 2019-06-28 08.07.57.png ├── Screenshot 2019-06-28 08.08.06.png ├── Screenshot 2019-06-28 08.08.13.png └── Screenshot 2019-07-05 15.10.53.png ├── Cargo.toml ├── src ├── data │ └── gamedata.json └── main.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.code-workspace 4 | *.lock 5 | TCOD-Reference.txt -------------------------------------------------------------------------------- /arial10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/arial10x10.png -------------------------------------------------------------------------------- /prestige10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/prestige10x10.png -------------------------------------------------------------------------------- /terminal10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/terminal10x10.png -------------------------------------------------------------------------------- /terminal8x12_gs_tc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/terminal8x12_gs_tc.png -------------------------------------------------------------------------------- /terminal8x8_gs_ro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/terminal8x8_gs_ro.png -------------------------------------------------------------------------------- /consolas10x10_gs_tc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/consolas10x10_gs_tc.png -------------------------------------------------------------------------------- /consolas12x12_gs_tc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/consolas12x12_gs_tc.png -------------------------------------------------------------------------------- /screenshots/sorted_connectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/sorted_connectors.png -------------------------------------------------------------------------------- /screenshots/2019-07-06-prototype.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/2019-07-06-prototype.gif -------------------------------------------------------------------------------- /screenshots/randomly_collected_connectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/randomly_collected_connectors.png -------------------------------------------------------------------------------- /screenshots/successful-seeded-generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/successful-seeded-generator.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2019-06-28 08.07.57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/Screenshot 2019-06-28 08.07.57.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2019-06-28 08.08.06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/Screenshot 2019-06-28 08.08.06.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2019-06-28 08.08.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/Screenshot 2019-06-28 08.08.13.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2019-07-05 15.10.53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graysentinel/roguelike-rust/HEAD/screenshots/Screenshot 2019-07-05 15.10.53.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roguelike" 3 | version = "0.1.0" 4 | authors = ["'graysentinel' "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tcod = "0.14" 9 | rand = "0.6.5" 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | rand_pcg = "0.2.0" 13 | rand_core = "0.5.0" -------------------------------------------------------------------------------- /src/data/gamedata.json: -------------------------------------------------------------------------------- 1 | { 2 | "player" : { 3 | "max_hp": 30, 4 | "hp": 30, 5 | "defense": 2, 6 | "power": 5, 7 | "on_death": "Player" 8 | }, 9 | "worm": { 10 | "max_hp": 10, 11 | "hp": 10, 12 | "defense": 0, 13 | "power": 3, 14 | "on_death": "Monster" 15 | }, 16 | "virus": { 17 | "max_hp": 16, 18 | "hp": 16, 19 | "defense": 1, 20 | "power": 4, 21 | "on_death": "Monster" 22 | } 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Roguelike Tutorial In Rust 2 | 3 | This is an implementation of the Rust Roguelike tutorial from [this](https://tomassedovic.github.io/roguelike-tutorial/) tutorial provided by Tomas Sedovic. 4 | 5 | It has a few idiosyncracies and things implemented differently, as I am personally using this as a launching point to learn more about the Rust language and what it has to offer. 6 | 7 | So far, I have to say (as somebody who is dangerous in C# but only mostly Python-fluent) I LIKE WHAT I SEE! =) 8 | 9 | This project is undertaken as part of the 2019 [r/roguelikedev does the Roguelike Tutorial 2019](https://www.reddit.com/r/roguelikedev/comments/br1sv3/roguelikedev_does_the_complete_roguelike_tutorial/) as a learning process for myself. 10 | 11 | I have previously done the Roguelike tutorial in Python: see my [bltilerogue](https://github.com/graysentinel/bltilerogue) repository for an incomplete Python implementation using bearlibterminal for visual output. 12 | 13 | Copyright (c) 2019 GraySentinel 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use tcod::colors::*; 2 | use tcod::console::*; 3 | use tcod::map::{FovAlgorithm, Map as FovMap}; 4 | use tcod::input::{self, Event, Key}; 5 | use std::cmp; 6 | use rand::Rng; 7 | use serde::{Deserialize}; 8 | use serde_json::{Result, Value}; 9 | use std::collections::HashMap; 10 | use std::slice::Iter; 11 | use rand_core::RngCore; 12 | use std::fmt; 13 | 14 | const SCREEN_WIDTH: i32 = 110; 15 | const SCREEN_HEIGHT: i32 = 50; 16 | const LIMIT_FPS: i32 = 20; 17 | const MAP_WIDTH: i32 = 79; 18 | const MAP_HEIGHT: i32 = 43; 19 | const COLOR_DARK_WALL: Color = Color {r: 10, g: 55, b: 10}; 20 | const COLOR_LIGHT_WALL: Color = Color {r: 30, g: 100, b: 30}; 21 | const COLOR_DARK_GROUND: Color = Color {r: 5, g: 15, b: 5}; 22 | const COLOR_LIGHT_GROUND: Color = Color {r: 7,g: 35,b: 7}; 23 | const ROOM_MAX_SIZE: i32 = 10; 24 | const ROOM_MIN_SIZE: i32 = 6; 25 | const MAX_ROOMS: i32 = 30; 26 | const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic; 27 | const FOV_LIGHT_WALLS: bool = true; 28 | const TORCH_RADIUS: i32 = 3; 29 | 30 | const MAX_ROOM_MONSTERS: i32 = 3; 31 | const MAX_ROOM_ITEMS: i32 = 2; 32 | 33 | const PLAYER: usize = 0; 34 | const GAME_DATA: &str = include_str!("data/gamedata.json"); 35 | 36 | const BAR_WIDTH: i32 = 20; 37 | const PANEL_HEIGHT: i32 = 7; 38 | const PANEL_Y: i32 = SCREEN_HEIGHT - PANEL_HEIGHT; 39 | const MSG_X: i32 = BAR_WIDTH + 2; 40 | const MSG_WIDTH: i32 = SCREEN_WIDTH - BAR_WIDTH - V_PANEL_WIDTH - 2; 41 | const MSG_HEIGHT: usize = PANEL_HEIGHT as usize - 1; 42 | const V_PANEL_WIDTH: i32 = SCREEN_WIDTH - MAP_WIDTH - 1; 43 | const V_PANEL_X: i32 = MAP_WIDTH + 1; 44 | const V_PANEL_HEIGHT: i32 = SCREEN_HEIGHT; 45 | 46 | const HEAL_AMOUNT: i32 = 4; 47 | 48 | 49 | // Data Types 50 | #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] 51 | struct Point { 52 | x: i32, 53 | y: i32, 54 | } 55 | 56 | impl Point { 57 | pub fn new(x: i32, y: i32) -> Self { 58 | Point { x, y } 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | struct Object { 64 | x: i32, 65 | y: i32, 66 | name: String, 67 | character: char, 68 | color: Color, 69 | id: i32, 70 | blocks: bool, 71 | alive: bool, 72 | fighter: Option, 73 | ai: Option, 74 | item: Option, 75 | item_type: Option, 76 | } 77 | 78 | impl Object { 79 | pub fn new(x: i32, y: i32, name: &str, ch: char, color: Color, id: i32, blocks: bool, alive: bool) -> Self { 80 | Object { x, y, name: name.into(), character: ch, color, id, blocks, alive, fighter: None, ai: None, item: None, item_type: None } 81 | } 82 | 83 | pub fn new_player(x: i32, y: i32, name: &str, ch: char, color: Color, id: i32, blocks: bool, alive: bool) -> Self { 84 | let player_fighter = Fighter::new(name); 85 | Object { 86 | x, 87 | y, 88 | name: name.into(), 89 | character: ch, 90 | color, 91 | id, 92 | blocks, 93 | alive, 94 | fighter: Some(player_fighter), 95 | ai: None, 96 | item: None, 97 | item_type: None, 98 | } 99 | } 100 | 101 | pub fn new_monster(x: i32, y: i32, name: &str, ch: char, color: Color, id: i32, blocks: bool, alive: bool) -> Self { 102 | let monster_fighter = Fighter::new(name); 103 | Object { 104 | x, 105 | y, 106 | name: name.into(), 107 | character: ch, 108 | color, 109 | id, 110 | blocks, 111 | alive, 112 | fighter: Some(monster_fighter), 113 | ai: Some(Ai), 114 | item: None, 115 | item_type: None, 116 | } 117 | } 118 | 119 | pub fn new_item(x: i32, y: i32, name: &str, ch: char, color: Color, id: i32, blocks: bool, alive: bool, item_function: Item, 120 | item_type: ItemType) -> Self { 121 | let item_func = Some(item_function); 122 | let item_t = Some(item_type); 123 | Object { 124 | x, 125 | y, 126 | name: name.into(), 127 | character: ch, 128 | color, 129 | id, 130 | blocks, 131 | alive, 132 | fighter: None, 133 | ai: None, 134 | item: item_func, 135 | item_type: item_t, 136 | } 137 | } 138 | 139 | pub fn draw(&self, con: &mut Console) { 140 | con.set_default_foreground(self.color); 141 | con.put_char(self.x, self.y, self.character, BackgroundFlag::None); 142 | } 143 | 144 | pub fn pos(&self) -> (i32, i32) { 145 | (self.x, self.y) 146 | } 147 | 148 | pub fn set_pos(&mut self, x: i32, y: i32) { 149 | self.x = x; 150 | self.y = y; 151 | } 152 | 153 | pub fn distance_to(&self, other: &Object) -> f32 { 154 | let dx = other.x - self.x; 155 | let dy = other.y - self.y; 156 | distance(dx, dy) 157 | } 158 | 159 | pub fn take_damage(&mut self, damage: i32, messages: &mut Vec) { 160 | if let Some(fighter) = self.fighter.as_mut() { 161 | if damage > 0 { 162 | fighter.hp -= damage; 163 | } 164 | } 165 | 166 | if let Some(fighter) = self.fighter { 167 | if fighter.hp <= 0 { 168 | self.alive = false; 169 | fighter.on_death.callback(self, messages); 170 | } 171 | } 172 | } 173 | 174 | pub fn attack(&mut self, target: &mut Object, messages: &mut Vec) { 175 | let damage = self.fighter.map_or(0, |f| f.power) - target.fighter.map_or(0, |f| f.defense); 176 | if damage > 0 { 177 | message(messages, 178 | format!("{} attacks {} for {} damage", self.name, target.name, damage), 179 | WHITE); 180 | target.take_damage(damage, messages); 181 | } else { 182 | message(messages, 183 | format!("{} attacks {}, but it has no effect!", self.name, target.name), 184 | WHITE); 185 | } 186 | } 187 | 188 | pub fn heal(&mut self, amount: i32) { 189 | if let Some(ref mut fighter) = self.fighter { 190 | fighter.hp += amount; 191 | if fighter.hp > fighter.max_hp { 192 | fighter.hp = fighter.max_hp; 193 | } 194 | } 195 | } 196 | } 197 | 198 | enum Directions { 199 | NORTH, 200 | SOUTH, 201 | EAST, 202 | WEST, 203 | } 204 | 205 | impl Directions { 206 | pub fn iterator() -> Iter<'static, Directions> { 207 | static DIRECTIONS: [Directions; 4] = [Directions::NORTH, Directions::SOUTH, Directions::EAST, Directions::WEST]; 208 | DIRECTIONS.into_iter() 209 | } 210 | } 211 | 212 | #[derive(Clone, Copy, Debug, PartialEq)] 213 | enum PlayerAction { 214 | TookTurn, 215 | NoTurn, 216 | Exit, 217 | } 218 | 219 | #[derive(Clone, Copy, Debug)] 220 | struct Tile { 221 | blocked: bool, 222 | block_sight: bool, 223 | explored: bool, 224 | } 225 | 226 | impl Tile { 227 | pub fn empty() -> Self { 228 | Tile{blocked: false, block_sight: false, explored: false} 229 | } 230 | 231 | pub fn wall() -> Self { 232 | Tile{blocked: true, block_sight: true, explored: false} 233 | } 234 | } 235 | 236 | type Map = Vec>; 237 | 238 | #[derive(Clone, Copy, Debug)] 239 | struct Rect { 240 | x1: i32, 241 | y1: i32, 242 | x2: i32, 243 | y2: i32, 244 | } 245 | 246 | impl Rect { 247 | pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { 248 | Rect { 249 | x1: x, 250 | y1: y, 251 | x2: x + w, 252 | y2: y + h, 253 | } 254 | } 255 | 256 | pub fn center(&self) -> (i32, i32) { 257 | let center_x = (self.x1 + self.x2) / 2; 258 | let center_y = (self.y1 + self.y2) / 2; 259 | (center_x, center_y) 260 | } 261 | 262 | pub fn intersects_with(&self, other: &Rect) -> bool { 263 | (self.x1 <= other.x2) 264 | && (self.x2 >= other.x1) 265 | && (self.y1 <= other.y2) 266 | && (self.y2 >= other.y1) 267 | } 268 | } 269 | 270 | #[derive(Clone, Copy, Debug, PartialEq, Deserialize)] 271 | struct Fighter { 272 | max_hp: i32, 273 | hp: i32, 274 | defense: i32, 275 | power: i32, 276 | on_death: DeathCallback 277 | } 278 | 279 | impl Fighter { 280 | pub fn new(name: &str) -> Self { 281 | let fighter_data = extract_node_from_gamedata(&name).unwrap(); 282 | let fighter: Fighter = serde_json::from_value(fighter_data).unwrap(); 283 | fighter 284 | } 285 | } 286 | 287 | #[derive(Clone, Copy, Debug, PartialEq, Deserialize)] 288 | enum DeathCallback { 289 | Player, 290 | Monster, 291 | } 292 | 293 | impl DeathCallback { 294 | fn callback(self, object: &mut Object, messages: &mut Vec) { 295 | use DeathCallback::*; 296 | let callback: fn(&mut Object, &mut Vec) = match self { 297 | Player => player_death, 298 | Monster => monster_death, 299 | }; 300 | callback(object, messages); 301 | } 302 | } 303 | 304 | fn extract_node_from_gamedata(node: &str) -> Result { 305 | let game_data: Value = serde_json::from_str(&GAME_DATA)?; 306 | let target_node: Value = game_data[node].clone(); 307 | 308 | Ok(target_node) 309 | } 310 | 311 | #[derive(Clone, Copy, Debug, PartialEq)] 312 | struct Ai; 313 | 314 | type Message = (String, Color); 315 | 316 | #[derive(Clone, Copy, Debug, PartialEq)] 317 | enum Item { 318 | Heal, 319 | } 320 | 321 | #[derive(Clone, Copy, Debug, PartialEq)] 322 | enum ItemType { 323 | Script, 324 | //App, 325 | } 326 | 327 | impl fmt::Display for ItemType { 328 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 329 | match *self { 330 | ItemType::Script => write!(f, "Script"), 331 | } 332 | } 333 | } 334 | 335 | enum UseResult { 336 | Used, 337 | Cancelled, 338 | } 339 | 340 | enum CommandType { 341 | Execute(T), 342 | Invalid(E), 343 | } 344 | 345 | 346 | // Main Function 347 | fn main() { 348 | let mut root = Root::initializer() 349 | .font("consolas12x12_gs_tc.png", FontLayout::Tcod) 350 | .font_type(FontType::Greyscale) 351 | .size(SCREEN_WIDTH, SCREEN_HEIGHT) 352 | .title("Connection Lost") 353 | .init(); 354 | 355 | let mut con = Offscreen::new(SCREEN_WIDTH, SCREEN_HEIGHT); 356 | let mut panel = Offscreen::new(SCREEN_WIDTH, PANEL_HEIGHT); 357 | let mut v_panel = Offscreen::new(V_PANEL_WIDTH, V_PANEL_HEIGHT); 358 | 359 | tcod::system::set_fps(LIMIT_FPS); 360 | let player = Object::new_player(0, 0, "player", '@', DARK_GREEN, 0, true, true); 361 | 362 | let mut objects = vec![player]; 363 | let mut inventory: Vec = Vec::new(); 364 | let mut messages: Vec = Vec::new(); 365 | let mut map = make_map_hauberk(&mut objects); 366 | //let mut map = make_map(&mut objects); 367 | 368 | message(&mut messages, 369 | "Connection Initiated. Processing...", 370 | YELLOW); 371 | 372 | let mut fov_map = FovMap::new(MAP_WIDTH, MAP_HEIGHT); 373 | create_fov(&mut fov_map, &map); 374 | 375 | let mut previous_player_position = (-1, -1); 376 | 377 | let mut key: Key = Default::default(); 378 | 379 | while !root.window_closed() { 380 | con.clear(); 381 | 382 | match input::check_for_event(input::KEY_PRESS) { 383 | Some((_, Event::Key(k))) => key = k, 384 | _ => key = Default::default(), 385 | } 386 | 387 | let fov_recompute = previous_player_position != (objects[PLAYER].pos()); 388 | render_all(&mut root, &mut con, &mut panel, &mut v_panel, &objects, &mut map, &mut fov_map, fov_recompute, &mut messages, &inventory); 389 | root.flush(); 390 | previous_player_position = objects[PLAYER].pos(); 391 | let player_action = handle_keys(key, &mut root, &mut map, &mut objects, &mut messages, &mut inventory); 392 | match player_action { 393 | PlayerAction::Exit => break, 394 | PlayerAction::TookTurn => { 395 | for id in 0..objects.len() { 396 | if objects[id].ai.is_some() { 397 | ai_take_turn(id, &map, &mut objects, &fov_map, &mut messages) 398 | } 399 | } 400 | }, 401 | PlayerAction::NoTurn => (), 402 | } 403 | } 404 | } 405 | 406 | fn handle_keys(key: Key, 407 | root: &mut Root, 408 | map: &mut Map, 409 | objects: &mut Vec, 410 | messages: &mut Vec, 411 | inventory: &mut Vec) 412 | -> PlayerAction { 413 | 414 | use tcod::input::KeyCode::*; 415 | use PlayerAction::*; 416 | use Directions::*; 417 | 418 | let player_alive = objects[PLAYER].alive; 419 | //let key = root.wait_for_keypress(true); 420 | match (key, player_alive) { 421 | (Key { 422 | code: Enter, 423 | alt: true, 424 | .. 425 | }, 426 | _, 427 | ) => { 428 | // Alt+Enter: toggle fullscreen 429 | let fullscreen = root.is_fullscreen(); 430 | root.set_fullscreen(!fullscreen); 431 | NoTurn 432 | } 433 | (Key { code: Escape, .. }, _) => return Exit, 434 | (Key { code: F1, .. }, _) => { 435 | reveal_map(map); 436 | NoTurn 437 | } 438 | (Key { code: Up, .. }, true) => { 439 | player_move_or_attack(get_direction(&NORTH), map, objects, messages); 440 | TookTurn 441 | } 442 | (Key { code: Down, .. }, true) => { 443 | player_move_or_attack(get_direction(&SOUTH), map, objects, messages); 444 | TookTurn 445 | }, 446 | (Key { code: Left, .. }, true) => { 447 | player_move_or_attack(get_direction(&WEST), map, objects, messages); 448 | TookTurn 449 | }, 450 | (Key { code: Right, .. }, true) => { 451 | player_move_or_attack(get_direction(&EAST), map, objects, messages); 452 | TookTurn 453 | }, 454 | (Key { printable: ',', ..}, true) => { 455 | let item_id = objects 456 | .iter() 457 | .position(|object| object.pos() == objects[PLAYER].pos() && object.item.is_some()); 458 | if let Some(item_id) = item_id { 459 | pick_up_item(item_id, objects, inventory, messages); 460 | } 461 | NoTurn 462 | }, 463 | (Key { printable: '`', ..}, true) => { 464 | let command = command_prompt(root); 465 | match command { 466 | Some(cmd) => { 467 | let parsed = parse_command(cmd); 468 | match parsed { 469 | CommandType::Execute(c) => { 470 | let index = get_inventory_item_by_name(inventory, &c); 471 | match index { 472 | Some(i) => { 473 | use_item(i, inventory, objects, messages); 474 | return TookTurn 475 | } 476 | None => { message( 477 | messages, 478 | "File Not Found", 479 | YELLOW 480 | ); 481 | return NoTurn 482 | } 483 | }; 484 | }, 485 | CommandType::Invalid(_) => { 486 | message( 487 | messages, 488 | "Invalid Command Entered", 489 | YELLOW 490 | ); 491 | } 492 | } 493 | }, 494 | None => { 495 | //println!("Error encountered"); 496 | return NoTurn 497 | } 498 | } 499 | TookTurn 500 | } 501 | 502 | _ => NoTurn, 503 | } 504 | } 505 | 506 | // Movement Functions 507 | 508 | fn get_direction(d: &Directions) -> (i32, i32) { 509 | // chooses direction 510 | match d { 511 | Directions::NORTH => (0, -1), 512 | Directions::SOUTH => (0, 1), 513 | Directions::EAST => (1, 0), 514 | Directions::WEST => (-1, 0), 515 | } 516 | } 517 | 518 | fn out_of_bounds(x: i32, y: i32) -> bool { 519 | if x > 0 && x < MAP_WIDTH && y > 0 && y < MAP_HEIGHT { 520 | return false 521 | } 522 | true 523 | } 524 | 525 | fn move_by(id: usize, (dx, dy): (i32, i32), map: &Map, objects: &mut [Object]) { 526 | let (x, y) = objects[id].pos(); 527 | if !out_of_bounds(x + dx, y + dy) { 528 | if !is_blocked(x + dx, y + dy, map, objects) { 529 | objects[id].set_pos(x + dx, y + dy); 530 | } 531 | } 532 | 533 | } 534 | 535 | fn move_towards(id: usize, (target_x, target_y): (i32, i32), map: &Map, objects: &mut [Object]) { 536 | let dx = target_x - objects[id].x; 537 | let dy = target_y - objects[id].y; 538 | let distance = distance(dx, dy); 539 | 540 | let dx = (dx as f32 / distance).round() as i32; 541 | let dy = (dy as f32 / distance).round() as i32; 542 | move_by(id, (dx, dy), map, objects); 543 | } 544 | 545 | fn distance(dx: i32, dy: i32) -> f32 { 546 | ((dx.pow(2) + dy.pow(2)) as f32).sqrt() 547 | } 548 | 549 | fn is_blocked(x: i32, y: i32, map: &Map, objects: &[Object]) -> bool { 550 | // first test the map tile 551 | if map[x as usize][y as usize].blocked { 552 | return true; 553 | } 554 | // now check for any blocking objects 555 | objects 556 | .iter() 557 | .any(|object| object.blocks && object.pos() == (x, y)) 558 | } 559 | 560 | // Player Functions 561 | 562 | fn player_move_or_attack((dx, dy): (i32, i32), map: &Map, objects: &mut [Object], messages: &mut Vec) { 563 | let x = objects[PLAYER].x + dx; 564 | let y = objects[PLAYER].y + dy; 565 | 566 | let target_id = objects 567 | .iter() 568 | .position(|object| object.fighter.is_some() && object.pos() == (x, y)); 569 | 570 | match target_id { 571 | Some(target_id) => { 572 | let (player, target) = mut_two(PLAYER, target_id, objects); 573 | player.attack(target, messages); 574 | } 575 | None => { 576 | move_by(PLAYER, (dx, dy), map, objects); 577 | } 578 | } 579 | } 580 | 581 | fn player_death(player: &mut Object, messages: &mut Vec) { 582 | message(messages, 583 | format!("Connection Lost... Retrying..."), 584 | GREEN); 585 | 586 | player.character = '%'; 587 | player.color = DARK_GREY; 588 | } 589 | 590 | fn pick_up_item(object_id: usize, objects: &mut Vec, inventory: &mut Vec, messages: &mut Vec) { 591 | if inventory.len() >= 26 { 592 | message( 593 | messages, 594 | format!("No space in local filesystem! Cannot pick up `{}`.", objects[object_id].name), 595 | YELLOW 596 | ); 597 | } else { 598 | let item = objects.swap_remove(object_id); 599 | message( 600 | messages, 601 | format!("Moved file: `{}` to local filesystem.", item.name), 602 | GREEN, 603 | ); 604 | inventory.push(item); 605 | } 606 | } 607 | 608 | fn use_item(inventory_id: usize, inventory: &mut Vec, objects: &mut [Object], messages: &mut Vec) { 609 | use Item::*; 610 | 611 | if let Some(item) = inventory[inventory_id].item { 612 | let on_use = match item { 613 | Heal => cast_heal, 614 | }; 615 | match on_use(inventory_id, objects, messages) { 616 | UseResult::Used => { 617 | inventory.remove(inventory_id); 618 | }, 619 | UseResult::Cancelled => { 620 | message(messages, "Cancelled Item Use", WHITE); 621 | }, 622 | } 623 | } else { 624 | message( 625 | messages, 626 | format!("The {} cannot be used.", inventory[inventory_id].name), 627 | WHITE 628 | ); 629 | } 630 | } 631 | 632 | fn cast_heal(_inventory_id: usize, objects: &mut [Object], messages: &mut Vec) -> UseResult { 633 | if let Some(fighter) = objects[PLAYER].fighter { 634 | if fighter.hp == fighter.max_hp { 635 | message(messages, "Connection strength at maximum...", GREEN); 636 | return UseResult::Cancelled; 637 | } 638 | message( 639 | messages, 640 | "Connection to server strengthened.", 641 | DARKER_GREEN); 642 | objects[PLAYER].heal(HEAL_AMOUNT); 643 | return UseResult::Used; 644 | } 645 | UseResult::Cancelled 646 | } 647 | 648 | fn parse_command(command: String) -> CommandType { 649 | let parts = command.split(" "); 650 | let collected = parts.collect::>(); 651 | 652 | if collected.len() < 1 { 653 | return CommandType::Invalid("Invalid Command"); 654 | } else { 655 | match collected[0] { 656 | "exec" => { 657 | let return_value = collected[1]; 658 | CommandType::Execute(return_value.to_string()) 659 | } 660 | _ => CommandType::Invalid("Invalid Command"), 661 | } 662 | } 663 | } 664 | 665 | fn get_inventory_item_by_name(inventory: &mut Vec, item: &str) -> Option { 666 | let index = inventory.iter().position(|r| r.name.to_ascii_lowercase() == item.to_ascii_lowercase()); 667 | match index { 668 | Some(i) => Some(i), 669 | None => None, 670 | } 671 | } 672 | 673 | // AI Functions 674 | 675 | fn ai_take_turn(monster_id: usize, map: &Map, objects: &mut [Object], fov_map: &FovMap, messages: &mut Vec) { 676 | let (monster_x, monster_y) = objects[monster_id].pos(); 677 | if fov_map.is_in_fov(monster_x, monster_y) { 678 | if objects[monster_id].distance_to(&objects[PLAYER]) >= 2.0 { 679 | let (player_x, player_y) = objects[PLAYER].pos(); 680 | move_towards(monster_id, (player_x, player_y), map, objects); 681 | } else if objects[PLAYER].fighter.map_or(false, |f| f.hp > 0) { 682 | let (monster, player) = mut_two(monster_id, PLAYER, objects); 683 | monster.attack(player, messages); 684 | } 685 | } 686 | } 687 | 688 | fn monster_death(monster: &mut Object, messages: &mut Vec) { 689 | message(messages, 690 | format!("{} died!", monster.name), 691 | LIME); 692 | monster.character = '%'; 693 | monster.color = DARK_GREY; 694 | monster.blocks = false; 695 | monster.fighter = None; 696 | monster.ai = None; 697 | monster.name = format!("remains of {}", monster.name); 698 | } 699 | 700 | // System Functions 701 | 702 | fn mut_two(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut T, &mut T) { 703 | assert!(first_index != second_index); 704 | let split_at_index = cmp::max(first_index, second_index); 705 | let (first_slice, second_slice) = items.split_at_mut(split_at_index); 706 | if first_index < second_index { 707 | (&mut first_slice[first_index], &mut second_slice[0]) 708 | } else { 709 | (&mut second_slice[0], &mut first_slice[second_index]) 710 | } 711 | } 712 | 713 | // Rendering 714 | 715 | fn render_all(root: &mut Root, 716 | con: &mut Offscreen, 717 | panel: &mut Offscreen, 718 | v_panel: &mut Offscreen, 719 | objects: &[Object], 720 | map: &mut Map, 721 | fov_map: &mut FovMap, 722 | fov_recompute: bool, 723 | messages: &Vec, 724 | inventory: &Vec, 725 | ) { 726 | if fov_recompute { 727 | let player = &objects[PLAYER]; 728 | fov_map.compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO); 729 | } 730 | let characters = vec!['!', '#', '$', '&', '*', '+', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 731 | '[', ']', '{', '}', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 732 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 733 | for y in 0..MAP_HEIGHT { 734 | for x in 0..MAP_WIDTH { 735 | let visible = fov_map.is_in_fov(x, y); 736 | let wall = map[x as usize][y as usize].block_sight; 737 | let explored = &mut map[x as usize][y as usize].explored; 738 | if visible { 739 | *explored = true; 740 | } 741 | let color = match (visible, wall) { 742 | (false, true) => COLOR_DARK_WALL, 743 | (false, false) => COLOR_DARK_GROUND, 744 | (true, true) => COLOR_LIGHT_WALL, 745 | (true, false) => COLOR_LIGHT_GROUND, 746 | }; 747 | if *explored { 748 | con.set_char_background(x, y, color, BackgroundFlag::Set); 749 | } 750 | else { 751 | let random_chance = rand::thread_rng().gen_range(0, 100); 752 | if random_chance < 10 { 753 | let random_index = rand::thread_rng().gen_range(0, characters.len()); 754 | let chosen_char = &characters[random_index]; 755 | con.set_default_foreground(COLOR_DARK_WALL); 756 | con.put_char(x, y, *chosen_char, BackgroundFlag::Set); 757 | } 758 | } 759 | } 760 | } 761 | 762 | // Rendering Objects 763 | let mut to_draw: Vec<_> = objects 764 | .iter() 765 | .filter(|o| fov_map.is_in_fov(o.x, o.y)) 766 | .collect(); 767 | to_draw.sort_by(|o1, o2| {o1.blocks.cmp(&o2.blocks) }); 768 | for object in &to_draw { 769 | object.draw(con); 770 | } 771 | 772 | // Bottom Panel 773 | panel.set_default_background(BLACK); 774 | panel.clear(); 775 | 776 | let hp = objects[PLAYER].fighter.map_or(0, |f| f.hp); 777 | let max_hp = objects[PLAYER].fighter.map_or(0, |f| f.max_hp); 778 | panel.set_default_foreground(DARKER_GREEN); 779 | panel.print(1, 1, "Connection Strength"); 780 | render_bar( 781 | panel, 782 | 1, 783 | 2, 784 | BAR_WIDTH, 785 | None, 786 | hp, 787 | max_hp, 788 | LIGHT_YELLOW, 789 | DARKER_YELLOW, 790 | DARKEST_GREY, 791 | ); 792 | 793 | render_border(panel, DARKER_GREEN, MAP_WIDTH, PANEL_HEIGHT); 794 | 795 | let mut y = MSG_HEIGHT as i32; 796 | for &(ref msg, color) in messages.iter().rev() { 797 | let msg_height = panel.get_height_rect(MSG_X, y, MSG_WIDTH, 0, msg); 798 | y -= msg_height; 799 | if y < 1 { 800 | break; 801 | } 802 | 803 | panel.set_default_foreground(color); 804 | panel.print_rect(MSG_X, y, MSG_WIDTH, 0, msg); 805 | } 806 | 807 | // Right-Side Panel 808 | 809 | v_panel.set_default_background(BLACK); 810 | v_panel.clear(); 811 | 812 | v_panel.set_default_foreground(DARK_GREEN); 813 | v_panel.print_ex(2, 2, BackgroundFlag::None, TextAlignment::Left, "Current Server"); 814 | v_panel.print_ex(2, 3, BackgroundFlag::None, TextAlignment::Left, format!("{}", 0x844cfa4bf95ef68 as u64)); 815 | 816 | v_panel.print_ex(2, 7, BackgroundFlag::None, TextAlignment::Left, "Available Files"); 817 | let mut inv_y = 8 as i32; 818 | v_panel.set_default_foreground(GREEN); 819 | for item in inventory.iter() { 820 | v_panel.print_ex(2, inv_y, BackgroundFlag::None, TextAlignment::Left, format!("{} :: {}", &item.name, item.item_type.unwrap())); 821 | inv_y += 1; 822 | } 823 | 824 | let (player_x, player_y) = objects[PLAYER].pos(); 825 | v_panel.print_ex(V_PANEL_WIDTH / 2, SCREEN_HEIGHT - 3, BackgroundFlag::None, TextAlignment::Center, "Current Position"); 826 | v_panel.print_ex(V_PANEL_WIDTH / 2, SCREEN_HEIGHT - 2, BackgroundFlag::None, TextAlignment::Center, format!("({}, {})", player_x, player_y)); 827 | 828 | render_border(v_panel, DARKER_GREEN, V_PANEL_WIDTH-1, V_PANEL_HEIGHT); 829 | 830 | // Send panels to screen 831 | 832 | blit(panel, (0, 0), (SCREEN_WIDTH, PANEL_HEIGHT), root, (0, PANEL_Y), 1.0, 1.0); 833 | 834 | blit(con, (0, 0), (MAP_WIDTH, MAP_HEIGHT), root, (0, 0), 1.0, 1.0); 835 | 836 | blit(v_panel, (0, 0), (V_PANEL_WIDTH, V_PANEL_HEIGHT), root, (V_PANEL_X, 0), 1.0, 1.0); 837 | } 838 | 839 | fn create_fov(fov: &mut FovMap, map: &Map) { 840 | for y in 0..MAP_HEIGHT { 841 | for x in 0..MAP_WIDTH { 842 | fov.set( 843 | x, 844 | y, 845 | !map[x as usize][y as usize].block_sight, 846 | !map[x as usize][y as usize].blocked, 847 | ); 848 | } 849 | } 850 | } 851 | 852 | fn render_bar( 853 | panel: &mut Offscreen, 854 | x: i32, 855 | y: i32, 856 | total_width: i32, 857 | name: Option, 858 | value: i32, 859 | maximum: i32, 860 | bar_color: Color, 861 | back_color: Color, 862 | text_color: Color, 863 | ) { 864 | let bar_width = (value as f32 / maximum as f32 * total_width as f32) as i32; 865 | 866 | panel.set_default_background(back_color); 867 | panel.rect(x, y, total_width, 1, false, BackgroundFlag::Screen); 868 | 869 | panel.set_default_background(bar_color); 870 | if bar_width > 0 { 871 | panel.rect(x, y, bar_width, 1, false, BackgroundFlag::Screen); 872 | } 873 | 874 | panel.set_default_foreground(text_color); 875 | if name.is_some() { 876 | panel.print_ex( 877 | x + total_width / 2, 878 | y, 879 | BackgroundFlag::None, 880 | TextAlignment::Center, 881 | &format!("{}: {}/{}", name.unwrap(), value, maximum), 882 | ); 883 | } 884 | 885 | } 886 | 887 | fn render_border(panel: &mut Offscreen, border_color: Color, width: i32, height: i32) { 888 | panel.set_default_foreground(border_color); 889 | // Add the 4 corners 890 | panel.put_char(0, 0, 218u8 as char, BackgroundFlag::None); 891 | panel.put_char(width, 0, 191u8 as char, BackgroundFlag::None); 892 | panel.put_char(0, height - 1, 192u8 as char, BackgroundFlag::None); 893 | panel.put_char(width, height - 1, 217u8 as char, BackgroundFlag::None); 894 | 895 | // Draw top and bottom lines 896 | for x in 1..(width) { 897 | panel.put_char(x, 0, 196u8 as char, BackgroundFlag::None); 898 | panel.put_char(x, height - 1, 196u8 as char, BackgroundFlag::None); 899 | } 900 | for y in 1..(height - 1) { 901 | panel.put_char(0, y, 179u8 as char, BackgroundFlag::None); 902 | panel.put_char(width, y, 179u8 as char, BackgroundFlag::None); 903 | } 904 | } 905 | 906 | fn message>(messages: &mut Vec, message: T, color: Color) { 907 | if messages.len() == MSG_HEIGHT { 908 | messages.remove(0); 909 | } 910 | 911 | messages.push((message.into(), color)); 912 | } 913 | 914 | fn menu>(header: &str, options: &[T], width: i32, root: &mut Root) -> Option { 915 | assert!(options.len() <= 26, 916 | "Cannot have a menu with more than 26 options"); 917 | 918 | let header_height = root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header); 919 | let height = options.len() as i32 + header_height; 920 | 921 | let mut window = Offscreen::new(width, height); 922 | window.set_default_foreground(WHITE); 923 | window.print_rect_ex( 924 | 0, 925 | 0, 926 | width, 927 | height, 928 | BackgroundFlag::None, 929 | TextAlignment::Left, 930 | header, 931 | ); 932 | 933 | for (index, option_text) in options.iter().enumerate() { 934 | let menu_letter = (b'a' + index as u8) as char; 935 | let text = format!("({}) {}", menu_letter, option_text.as_ref()); 936 | window.print_ex( 937 | 0, 938 | header_height + index as i32, 939 | BackgroundFlag::None, 940 | TextAlignment::Left, 941 | text, 942 | ); 943 | } 944 | 945 | let x = SCREEN_WIDTH / 2 - width / 2; 946 | let y = SCREEN_HEIGHT / 2 - height / 2; 947 | blit(&mut window, (0, 0), (width, height), root, (x, y), 1.0, 0.7); 948 | 949 | root.flush(); 950 | let key = root.wait_for_keypress(true); 951 | 952 | if key.printable.is_alphabetic() { 953 | let index = key.printable.to_ascii_lowercase() as usize - 'a' as usize; 954 | if index < options.len() { 955 | Some(index) 956 | } else { 957 | None 958 | } 959 | } else { 960 | None 961 | } 962 | } 963 | 964 | fn command_prompt(root: &mut Root) -> Option { 965 | let mut prompt = Offscreen::new(MSG_WIDTH, 1); 966 | let cmd_prompt = vec![':', '>', ' ', '_']; 967 | let mut command = String::new(); 968 | 969 | let mut x = 0; 970 | prompt.set_default_foreground(GREEN); 971 | for ch in cmd_prompt.iter() { 972 | prompt.put_char(x, 0, *ch, BackgroundFlag::None); 973 | x += 1; 974 | } 975 | blit(&mut prompt, (0, 0), (MSG_WIDTH, 1), root, (MSG_X, PANEL_Y + 1), 1.0, 1.0); 976 | 977 | root.flush(); 978 | let mut key = root.wait_for_keypress(true); 979 | 980 | let mut cursor_x: usize = 3; 981 | let cursor: char = '_'; 982 | 983 | while key.code != tcod::input::KeyCode::Enter { 984 | if key.code == tcod::input::KeyCode::Escape { 985 | return None 986 | } 987 | 988 | if key.printable.is_alphanumeric() || key.printable == ' ' { 989 | let ch = key.printable.to_ascii_lowercase(); 990 | cursor_x += 1; 991 | prompt.put_char(cursor_x as i32 - 1, 0, ch, BackgroundFlag::None); 992 | prompt.put_char(cursor_x as i32, 0, cursor, BackgroundFlag::None); 993 | 994 | command.push(ch); 995 | } 996 | 997 | if key.code == tcod::input::KeyCode::Backspace && cursor_x > 3 { 998 | prompt.put_char(cursor_x as i32, 0, ' ', BackgroundFlag::None); 999 | cursor_x -= 1; 1000 | prompt.put_char(cursor_x as i32, 0, cursor, BackgroundFlag::None); 1001 | command.pop(); 1002 | } 1003 | 1004 | blit(&mut prompt, (0, 0), (MSG_WIDTH, 1), root, (MSG_X, PANEL_Y + 1), 1.0, 1.0); 1005 | root.flush(); 1006 | key = root.wait_for_keypress(true); 1007 | 1008 | } 1009 | Some(command) 1010 | } 1011 | 1012 | 1013 | // Map Functions 1014 | 1015 | fn make_map(objects: &mut Vec) -> Map { 1016 | // fill map with "blocked" tiles 1017 | let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; 1018 | // map algo 1019 | let mut rooms = vec![]; 1020 | for _ in 0..MAX_ROOMS { 1021 | let w = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 1022 | let h = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 1023 | let x = rand::thread_rng().gen_range(0, MAP_WIDTH - w); 1024 | let y = rand::thread_rng().gen_range(0, MAP_HEIGHT - h); 1025 | 1026 | let new_room = Rect::new(x, y, w, h); 1027 | 1028 | let failed = rooms 1029 | .iter() 1030 | .any(|other_room| new_room.intersects_with(other_room)); 1031 | 1032 | if !failed { 1033 | if rand::random::() < 0.15 { 1034 | create_circle_room(new_room, &mut map); 1035 | } else { 1036 | create_room(new_room, &mut map); 1037 | } 1038 | let room_center = new_room.center(); 1039 | place_objects(new_room, objects, &mut map); 1040 | 1041 | if rooms.is_empty() { 1042 | objects[PLAYER].set_pos(room_center.0, room_center.1); 1043 | } else { 1044 | let prev_center = rooms[rooms.len() - 1].center(); 1045 | if rand::random() { 1046 | create_h_tunnel(prev_center.0, room_center.0, prev_center.1, &mut map); 1047 | create_v_tunnel(prev_center.1, room_center.1, room_center.0, &mut map); 1048 | } else { 1049 | create_v_tunnel(prev_center.1, room_center.1, prev_center.0, &mut map); 1050 | create_h_tunnel(prev_center.0, room_center.0, room_center.1, &mut map); 1051 | } 1052 | } 1053 | 1054 | rooms.push(new_room); 1055 | } 1056 | } 1057 | 1058 | map 1059 | } 1060 | 1061 | fn create_room(room: Rect, map: &mut Map) { 1062 | for x in (room.x1 + 1)..room.x2 { 1063 | for y in (room.y1 + 1)..room.y2 { 1064 | map[x as usize][y as usize] = Tile::empty(); 1065 | } 1066 | } 1067 | } 1068 | 1069 | fn create_circle_room(room: Rect, map: &mut Map) { 1070 | // Code from @Agka on roguelikedev-help Discord channel - many thanks! 1071 | let rdx = room.x2 - room.x1; 1072 | let rdy = room.y2 - room.y1; 1073 | 1074 | let div_val = cmp::min(rdx, rdy); 1075 | 1076 | let radius: f32 = (div_val as f32 / 2.0) - 1.0; 1077 | let rad_floor: i32 = radius.floor() as i32; 1078 | let radsqr = radius.floor().powf(2.0) as i32; 1079 | let (center_x, center_y) = room.center(); 1080 | 1081 | let x_ratio = cmp::max(rdx / rdy, 1); 1082 | let y_ratio = cmp::max(rdy / rdx, 1); 1083 | 1084 | for x in center_x - rad_floor - 1..center_x + rad_floor + 1 { 1085 | for y in center_y - rad_floor - 1..center_y + rad_floor + 1 { 1086 | let dx = (x - center_x) / x_ratio; 1087 | let dy = (y - center_y) / y_ratio; 1088 | let distsqr = dx.pow(2) + dy.pow(2); 1089 | if distsqr < radsqr { 1090 | map[x as usize][y as usize] = Tile::empty(); 1091 | } 1092 | } 1093 | } 1094 | } 1095 | 1096 | fn create_h_tunnel(x1: i32, x2: i32, y: i32, map: &mut Map) { 1097 | for x in cmp::min(x1, x2)..(cmp::max(x1, x2) + 1) { 1098 | map[x as usize][y as usize] = Tile::empty(); 1099 | } 1100 | } 1101 | 1102 | fn create_v_tunnel(y1: i32, y2: i32, x: i32, map: &mut Map) { 1103 | for y in cmp::min(y1, y2)..(cmp::max(y1, y2) + 1) { 1104 | map[x as usize][y as usize] = Tile::empty(); 1105 | } 1106 | } 1107 | 1108 | fn place_objects(room: Rect, objects: &mut Vec, map: &mut Map) { 1109 | let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1); 1110 | 1111 | for _ in 0..num_monsters { 1112 | let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); 1113 | let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); 1114 | 1115 | let monster = if rand::random::() < 0.8 { 1116 | Object::new_monster(x, y, "worm", 'w', DESATURATED_GREEN, get_new_object_id(&objects), true, true) 1117 | } else { 1118 | Object::new_monster(x, y, "virus", 'v', DARKER_GREEN, get_new_object_id(&objects), true, true) 1119 | }; 1120 | 1121 | objects.push(monster); 1122 | } 1123 | 1124 | let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1); 1125 | //println!("Num items: {}", num_items); 1126 | 1127 | for _ in 0..num_items { 1128 | let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); 1129 | let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); 1130 | 1131 | let object = Object::new_item( 1132 | x, 1133 | y, 1134 | "Tracert", 1135 | '!', 1136 | DARK_GREEN, 1137 | get_new_object_id(&objects), 1138 | false, 1139 | false, 1140 | Item::Heal, 1141 | ItemType::Script); 1142 | objects.push(object); 1143 | //println!("Placed item at ({}, {})", x, y); 1144 | 1145 | } 1146 | } 1147 | 1148 | fn reveal_map(map: &mut Map) { 1149 | for x in 0..MAP_WIDTH { 1150 | for y in 0..MAP_HEIGHT { 1151 | map[x as usize][y as usize].explored = true; 1152 | } 1153 | } 1154 | } 1155 | 1156 | fn get_new_object_id(objects: &Vec) -> i32 { 1157 | let last_item = objects.last(); 1158 | match last_item { 1159 | Some(n) => n.id + 1, 1160 | None => 1, 1161 | } 1162 | } 1163 | 1164 | // The Hauberk Map Generater 1165 | 1166 | fn make_map_hauberk(objects: &mut Vec) -> Map { 1167 | 1168 | let num_room_tries = 100; 1169 | let extra_connector_chance = 25; 1170 | //let room_extra_size = 0; 1171 | let winding_percent = 10; 1172 | 1173 | let mut current_region: i32 = -1; 1174 | 1175 | let mut map_width: i32 = MAP_WIDTH; 1176 | let mut map_height: i32 = MAP_HEIGHT; 1177 | 1178 | // let seed: u64 = 0xcafef00dd15ea5e5; 1179 | // let state: u64 = 0xa02bdbf7bb3c0a7; 1180 | 1181 | /* let mut seed_rng = rand::thread_rng(); 1182 | let state: u64 = seed_rng.gen(); 1183 | let stream: u64 = seed_rng.gen(); */ 1184 | 1185 | let state: u64 = 0x844cfa4bf95ef68; 1186 | let stream: u64 = 0x2a04cd05868ddbcd; 1187 | println!("Using seed: {:#x} & state: {:#x}", state, stream); 1188 | let mut r = rand_pcg::Pcg32::new(state, stream); 1189 | 1190 | if map_width % 2 == 0 { 1191 | map_width -= 1; 1192 | } 1193 | 1194 | if map_height % 2 == 0 { 1195 | map_height -= 1; 1196 | } 1197 | 1198 | let mut map = vec![vec![Tile::wall(); map_height as usize]; MAP_WIDTH as usize]; 1199 | 1200 | type VecRegion = Vec>; 1201 | 1202 | let mut _regions = vec![vec![0; map_height as usize]; MAP_WIDTH as usize]; 1203 | 1204 | //fn on_decorate_room(room: Rect) {} 1205 | 1206 | fn pcg_choose(v: &Vec, r: &mut rand_pcg::Pcg32) -> Option { 1207 | if v.is_empty() { 1208 | return None 1209 | } else { 1210 | let vec_length: f64 = v.len() as f64; 1211 | let num_digits = vec_length.log10().round() as u32; 1212 | let modulo: u32 = (10 as u32).pow(num_digits); 1213 | let mut index_option = r.next_u32() % modulo; 1214 | while v.len()-1 < index_option as usize { 1215 | index_option = r.next_u32() % modulo; 1216 | } 1217 | return Some(index_option as usize) 1218 | } 1219 | } 1220 | 1221 | fn grow_maze(map: &mut Map, start: Point, current_region: &mut i32, winding_percent: i32, _regions: &mut VecRegion, 1222 | r: &mut rand_pcg::Pcg32) { 1223 | let mut cells = Vec::new(); 1224 | let mut last_dir = (0, 0); 1225 | 1226 | start_region(current_region); 1227 | carve(&start, map, _regions, current_region); 1228 | 1229 | cells.push(start); 1230 | 1231 | while !cells.is_empty() { 1232 | let cell = cells.last().unwrap(); 1233 | 1234 | let mut unmade_cells = Vec::new(); 1235 | for d in Directions::iterator() { 1236 | let (dx, dy) = get_direction(d); 1237 | let target_pos: Point = Point::new(cell.x + dx, cell.y + dy); 1238 | if can_carve(map, target_pos, d) { 1239 | unmade_cells.push((dx, dy)); 1240 | } 1241 | } 1242 | 1243 | if !unmade_cells.is_empty() { 1244 | let mut dir = (0, 0); 1245 | if unmade_cells.contains(&last_dir) && (r.next_u32() % 100) > winding_percent as u32 { 1246 | dir = last_dir; 1247 | } else { 1248 | let index_option = pcg_choose(&unmade_cells, r); 1249 | match index_option { 1250 | Some(i) => {dir = unmade_cells[i];} 1251 | None => () 1252 | } 1253 | /* let dir_choice = unmade_cells.choose(&mut rand::thread_rng()); 1254 | match dir_choice { 1255 | Some(d) => {dir = *d;} 1256 | None => () 1257 | } */ 1258 | } 1259 | 1260 | let close_pos = Point::new(cell.x + dir.0, cell.y + dir.1); 1261 | let far_pos = Point::new(cell.x + (dir.0 * 2), cell.y + (dir.1 * 2)); 1262 | carve(&close_pos, map, _regions, current_region); 1263 | carve(&far_pos, map, _regions, current_region); 1264 | 1265 | cells.push(far_pos); 1266 | 1267 | last_dir = dir; 1268 | 1269 | } else { 1270 | cells.pop(); 1271 | last_dir = (0, 0); 1272 | } 1273 | } 1274 | 1275 | } 1276 | 1277 | fn pcg_range(r: &mut rand_pcg::Pcg32, min: i32, max: i32) -> i32 { 1278 | let num_digits = (max as f32).log10().round() as u32; 1279 | let modulo: u32 = (10 as u32).pow(num_digits); 1280 | 1281 | let mut result: i32 = max + 1; 1282 | while !(result > min && result < max) { 1283 | let pcg = r.next_u32() % modulo; 1284 | result = pcg as i32; 1285 | } 1286 | 1287 | result 1288 | } 1289 | 1290 | fn add_rooms(objects: &mut Vec, map: &mut Map, tries: i32, current_region: &mut i32, _regions: &mut VecRegion, 1291 | map_width: i32, map_height: i32, r: &mut rand_pcg::Pcg32) { 1292 | let mut rooms = Vec::new(); 1293 | for _ in 0..=tries { 1294 | let w = pcg_range(r, ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 1295 | let h = pcg_range(r, ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 1296 | let x = (pcg_range(r, 0, map_width - w - 1) / 2)* 2 + 1; 1297 | let y = (pcg_range(r, 0, map_height - h - 1) / 2) * 2 + 1; 1298 | 1299 | let new_room = Rect::new(x, y, w, h); 1300 | 1301 | let failed = rooms 1302 | .iter() 1303 | .any(|other_room| new_room.intersects_with(other_room)); 1304 | 1305 | if rooms.is_empty() { 1306 | let room_center = new_room.center(); 1307 | objects[PLAYER].set_pos(room_center.0, room_center.1); 1308 | } 1309 | 1310 | if !failed { 1311 | place_objects(new_room, objects, map); 1312 | rooms.push(new_room); 1313 | start_region(current_region); 1314 | create_room_hauberk(&new_room, map, _regions, current_region); 1315 | } 1316 | } 1317 | } 1318 | 1319 | fn connect_regions(map: &mut Map, _regions: &mut VecRegion, current_region: i32, extra_chance: i32, 1320 | map_width: i32, map_height: i32, r: &mut rand_pcg::Pcg32) { 1321 | 1322 | let mut connector_regions = HashMap::new(); 1323 | 1324 | for x in 1..map_width-1 { 1325 | for y in 1..map_height-1 { 1326 | if !map[x as usize][y as usize].block_sight { continue; } 1327 | 1328 | let mut regions = Vec::new(); 1329 | for d in Directions::iterator() { 1330 | let (dx, dy) = get_direction(d); 1331 | let region = _regions[(x + dx) as usize][(y + dy) as usize]; 1332 | regions.push(region); 1333 | } 1334 | 1335 | if regions.len() < 2 { continue; } 1336 | 1337 | connector_regions.insert(Point::new(x, y), regions); 1338 | } 1339 | } 1340 | 1341 | let mut connectors: Vec<_> = connector_regions.keys().collect(); 1342 | connectors.sort(); 1343 | //println!("{:?}", connectors); 1344 | 1345 | let mut merged = HashMap::new(); 1346 | let mut open_regions = Vec::new(); 1347 | 1348 | //println!("{}", current_region); 1349 | 1350 | for i in 0..current_region { 1351 | merged.insert(i, i); 1352 | open_regions.push(i); 1353 | } 1354 | 1355 | //println!("{:?}", open_regions); 1356 | 1357 | while !open_regions.is_empty() { 1358 | let connector_idx = pcg_choose(&connectors, r); 1359 | let mut regions = Vec::new(); 1360 | match connector_idx { 1361 | Some(c) => { 1362 | let connector = connectors[c]; 1363 | add_junction(connector, map/*, _regions, current_region, r*/); 1364 | if connector_regions.contains_key(connector) { 1365 | for region in &connector_regions[connector] { 1366 | if merged.contains_key(®ion) { 1367 | let actual_region = merged[®ion]; 1368 | regions.push(actual_region); 1369 | } 1370 | } 1371 | } 1372 | 1373 | let dest = regions.first(); 1374 | let mut dest_region = 0; 1375 | match dest { 1376 | Some(region) => { dest_region = *region; } 1377 | None => () 1378 | } 1379 | 1380 | let sources: Vec<_> = regions[1..].iter().collect(); 1381 | 1382 | for i in 0..current_region { 1383 | if sources.contains(&&merged[&i]) { 1384 | if merged.contains_key(&i) { 1385 | merged.remove(&i); 1386 | merged.insert(i, dest_region); 1387 | } 1388 | } 1389 | } 1390 | 1391 | for s in sources { 1392 | let index = open_regions.iter().position(|x| *x == *s); 1393 | match index { 1394 | Some(n) => { 1395 | if open_regions.len() >= n { 1396 | open_regions.remove(n); 1397 | } 1398 | } 1399 | None => () 1400 | } 1401 | } 1402 | 1403 | let mut to_be_removed = Vec::new(); 1404 | for pos in &connectors { 1405 | if distance(connector.x-pos.x, connector.y-pos.y) < 2.0 { 1406 | to_be_removed.push(*pos); 1407 | continue; 1408 | } 1409 | 1410 | let mut local_regions = Vec::new(); 1411 | for r in &connector_regions[connector] { 1412 | if merged.contains_key(&r) { 1413 | let region_actual = merged[&r]; 1414 | local_regions.push(region_actual); 1415 | } 1416 | } 1417 | 1418 | if local_regions.len() > 1 { continue; } 1419 | 1420 | let new_junc = r.next_u32() % 100; 1421 | //let new_junc: u32 = 1; 1422 | if new_junc < extra_chance as u32 { 1423 | add_junction(pos, map/*, _regions, current_region, r*/); 1424 | } 1425 | 1426 | if local_regions.len() == 1 { 1427 | to_be_removed.push(*pos); 1428 | } 1429 | } 1430 | 1431 | connectors.retain(|&x| !to_be_removed.contains(&x)); 1432 | } 1433 | None => () 1434 | } 1435 | 1436 | } 1437 | 1438 | 1439 | } 1440 | 1441 | fn add_junction(pos: &Point, map: &mut Map/*, _regions: &mut VecRegion, current_region: i32, r: &mut rand_pcg::Pcg32*/) { 1442 | //println!("Adding junction at: ({}, {})", pos.x, pos.y); 1443 | map[pos.x as usize][pos.y as usize] = Tile::empty(); 1444 | } 1445 | 1446 | fn remove_dead_ends(map: &mut Map, map_width: i32, map_height: i32) { 1447 | let mut done = false; 1448 | 1449 | while !done { 1450 | done = true; 1451 | 1452 | for x in 1..map_width { 1453 | for y in 1..map_height { 1454 | if map[x as usize][y as usize].block_sight { continue; } 1455 | 1456 | let mut exits = 0; 1457 | for d in Directions::iterator() { 1458 | let (dx, dy) = get_direction(d); 1459 | let (target_x, target_y) = (x + dx, y + dy); 1460 | if !map[target_x as usize][target_y as usize].block_sight { exits += 1; } 1461 | } 1462 | 1463 | if exits != 1 { continue; } 1464 | 1465 | done = false; 1466 | map[x as usize][y as usize] = Tile::wall(); 1467 | } 1468 | } 1469 | } 1470 | 1471 | } 1472 | 1473 | fn can_carve(map: &mut Map, pos: Point, d: &Directions) -> bool { 1474 | let (dx, dy) = get_direction(d); 1475 | let test_point = (pos.x + (dx*3), pos.y + (dy*3)); 1476 | if out_of_bounds(test_point.0, test_point.1) { 1477 | return false 1478 | } 1479 | 1480 | let (target_x, target_y) = (pos.x + dx, pos.y + dy); 1481 | 1482 | return map[target_x as usize][target_y as usize].block_sight; 1483 | } 1484 | 1485 | fn start_region(i: &mut i32) { 1486 | *i = *i + 1; 1487 | } 1488 | 1489 | fn carve(pos: &Point, map: &mut Map, _regions: &mut VecRegion, current_region: &mut i32) { 1490 | map[pos.x as usize][pos.y as usize] = Tile::empty(); 1491 | //println!("Made ({}, {}) a floor tile", pos.x, pos.y); 1492 | _regions[pos.x as usize][pos.y as usize] = *current_region; 1493 | } 1494 | 1495 | fn create_room_hauberk(room: &Rect, map: &mut Map, _regions: &mut VecRegion, current_region: &mut i32) { 1496 | for x in room.x1..room.x2 { 1497 | for y in room.y1..room.y2 { 1498 | let target_point = Point::new(x, y); 1499 | carve(&target_point, map, _regions, current_region); 1500 | } 1501 | } 1502 | } 1503 | 1504 | add_rooms(objects, &mut map, num_room_tries, &mut current_region, &mut _regions, map_width, map_height, &mut r); 1505 | 1506 | for y in (1..map_height).step_by(2) { 1507 | for x in (1..map_width).step_by(2) { 1508 | if !map[x as usize][y as usize].block_sight { continue ; } 1509 | 1510 | let start = Point::new(x, y); 1511 | grow_maze(&mut map, start, &mut current_region, winding_percent, &mut _regions, &mut r); 1512 | } 1513 | } 1514 | 1515 | connect_regions(&mut map, &mut _regions, current_region, extra_connector_chance, map_width, map_height, &mut r); 1516 | 1517 | remove_dead_ends(&mut map, map_width, map_height); 1518 | 1519 | map 1520 | } --------------------------------------------------------------------------------