#![allow(unused_imports)] #![allow(unused_variables)] #![allow(unused_mut)] #![allow(dead_code)] use rand::Rng; use std::cmp; use tcod::colors::*; use tcod::console::*; use tcod::input::{self, Event, Key, Mouse}; use tcod::map::{FovAlgorithm, Map as FovMap}; const SCREEN_WIDTH: i32 = 80; const SCREEN_HEIGHT: i32 = 50; const FPS: i32 = 20; const MAP_WIDTH: i32 = 80; const MAP_HEIGHT: i32 = 43; const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 }; 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; const MAX_ROOM_MONSTERS: i32 = 3; const PLAYER: usize = 0; // Sizes and coordinates relevant for the GUI const BAR_WIDTH: i32 = 20; const PANEL_HEIGHT: i32 = 7; const PANEL_Y: i32 = SCREEN_HEIGHT - PANEL_HEIGHT; const MSG_X: i32 = BAR_WIDTH + 2; const MSG_WIDTH: i32 = SCREEN_WIDTH - BAR_WIDTH - 2; const MSG_HEIGHT: usize = PANEL_HEIGHT as usize - 1; const MAX_ROOM_ITEMS: i32 = 2; const INVENTORY_WIDTH: i32 = 50; const HEAL_AMOUNT: i32 = 4; const LIGHTNING_DAMAGE: i32 = 40; const LIGHTNING_RANGE: i32 = 5; const CONFUSE_RANGE: i32 = 8; const CONFUSE_NUM_TURNS: i32 = 10; const FIREBALL_RADIUS: i32 = 3; const FIREBALL_DAMAGE: i32 = 12; struct Tcod { root: Root, con: Offscreen, panel: Offscreen, fov: FovMap, key: Key, mouse: Mouse, } #[derive(Debug)] struct Object { x: i32, y: i32, char: char, color: Color, name: String, blocks: bool, alive: bool, fighter: Option, ai: Option, item: Option, } impl Object { pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self { Object { x: x, y: y, char: char, color: color, name: name.into(), blocks: blocks, alive: false, fighter: None, ai: None, item: 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); } pub fn pos(&self) -> (i32, i32) { (self.x, self.y) } pub fn set_pos(&mut self, x: i32, y: i32) { self.x = x; self.y = y; } pub fn distance_to(&self, other: &Object) -> f32 { let dx = other.x - self.x; let dy = other.y - self.y; ((dx.pow(2) + dy.pow(2)) as f32).sqrt() } pub fn take_damage(&mut self, damage: i32, game: &mut Game) { // Apply damage if possible if let Some(fighter) = self.fighter.as_mut() { if damage > 0 { fighter.hp -= damage; } } // Check for death, call the death function if let Some(fighter) = self.fighter { if fighter.hp <= 0 { self.alive = false; fighter.on_death.callback(self, game); } } } pub fn attack(&mut self, target: &mut Object, game: &mut Game) { // A simple formula for attack damage let damage = self.fighter.map_or(0, |f| f.power) - target.fighter.map_or(0, |f| f.defense); if damage > 0 { // Make the target take some damage game.messages.add( format!( "{} attacks {} for {} hit points.", self.name, target.name, damage ), WHITE, ); target.take_damage(damage, game); } else { game.messages.add( format!( "{} attacks {} but it has no effect!", self.name, target.name ), WHITE, ); } } pub fn heal(&mut self, amount: i32) { if let Some(ref mut fighter) = self.fighter { fighter.hp += amount; if fighter.hp > fighter.max_hp { fighter.hp = fighter.max_hp; } } } // Return the distance to some coordinates pub fn distance(&self, x: i32, y: i32) -> f32 { (((x - self.x).pow(2) + (y - self.y).pow(2)) as f32).sqrt() } } #[derive(Clone, Copy, Debug)] struct Tile { blocked: bool, explored: bool, block_sight: bool, } impl Tile { pub fn empty() -> Self { Tile { blocked: false, explored: false, block_sight: false, } } pub fn wall() -> Self { Tile { blocked: true, explored: false, block_sight: true, } } } type Map = Vec>; struct Game { map: Map, messages: Messages, inventory: Vec, } // A rectangle on the map, used to characterise a room. #[derive(Clone, Copy, Debug)] struct Rect { 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 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) } } // Combat related properties and methods (monster, player, NPC). #[derive(Clone, Copy, Debug, PartialEq)] struct Fighter { max_hp: i32, hp: i32, defense: i32, power: i32, on_death: DeathCallback, } struct Messages { messages: Vec<(String, Color)>, } impl Messages { pub fn new() -> Self { Self { messages: vec![] } } // Add the new message as a tuple, with the text and the color pub fn add>(&mut self, message: T, color: Color) { self.messages.push((message.into(), color)); } // Create a `DoubleEndedIterator` over the messages pub fn iter(&self) -> impl DoubleEndedIterator { self.messages.iter() } } #[derive(Clone, Copy, Debug, PartialEq)] enum PlayerAction { TookTurn, DidntTakeTurn, Exit, } #[derive(Clone, Debug, PartialEq)] enum Ai { Basic, Confused { previous_ai: Box, num_turns: i32, }, } #[derive(Clone, Copy, Debug, PartialEq)] enum DeathCallback { Player, Monster, } impl DeathCallback { fn callback(self, object: &mut Object, game: &mut Game) { use DeathCallback::*; let callback = match self { Player => player_death, Monster => monster_death, }; callback(object, game); } } #[derive(Clone, Copy, Debug, PartialEq)] enum Item { Heal, Lightning, Confuse, Fireball, } enum UseResult { UsedUp, Cancelled, } fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> PlayerAction { use tcod::input::Key; use tcod::input::KeyCode::*; use PlayerAction::*; let player_alive = objects[PLAYER].alive; match (tcod.key, tcod.key.text(), player_alive) { ( Key { code: Enter, alt: true, .. }, _, _, ) => { // Alt+Enter: toggle fullscreen let fullscreen = tcod.root.is_fullscreen(); tcod.root.set_fullscreen(!fullscreen); DidntTakeTurn } (Key { code: Escape, .. }, _, _) => Exit, // exit game (Key { code: Up, .. }, _, true) => { player_move_or_attack(0, -1, game, objects); TookTurn } (Key { code: Down, .. }, _, true) => { player_move_or_attack(0, 1, game, objects); TookTurn } (Key { code: Left, .. }, _, true) => { player_move_or_attack(-1, 0, game, objects); TookTurn } (Key { code: Right, .. }, _, true) => { player_move_or_attack(1, 0, game, objects); TookTurn } (Key { code: Text, .. }, "g", true) => { // Pick up an item let item_id = objects .iter() .position(|object| object.pos() == objects[PLAYER].pos() && object.item.is_some()); if let Some(item_id) = item_id { pick_item_up(item_id, game, objects); } DidntTakeTurn } (Key { code: Text, .. }, "i", true) => { // Show the inventory let inventory_index = inventory_menu( &game.inventory, "Press the key next to an item to use it, or any other to cancel.\n", &mut tcod.root, ); if let Some(inventory_index) = inventory_index { use_item(inventory_index, tcod, game, objects); } DidntTakeTurn } (Key { code: Text, .. }, "d", true) => { // Show the inventory; if an item is selected, drop it let inventory_index = inventory_menu( &game.inventory, "Press the key next to an item to drop it, or any other to cancel.\n", &mut tcod.root, ); if let Some(inventory_index) = inventory_index { drop_item(inventory_index, game, objects); } DidntTakeTurn } _ => DidntTakeTurn, } } fn make_map(objects: &mut Vec) -> Map { // Fill the map with unblocked tiles let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; let mut rooms = vec![]; 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); place_objects(new_room, &map, objects); let (new_x, new_y) = new_room.center(); if rooms.is_empty() { objects[PLAYER].set_pos(new_x, 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(); } } } 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(); } } 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(); } } fn place_objects(room: Rect, map: &Map, objects: &mut Vec) { // Generate some monsters let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1); for _ in 0..num_monsters { let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); if !is_blocked(x, y, map, objects) { let mut monster = if rand::random::() < 0.8 { // 80% chance of spawning an orc let mut orc = Object::new(x, y, 'o', "orc", DESATURATED_GREEN, true); orc.fighter = Some(Fighter { max_hp: 10, hp: 10, defense: 0, power: 3, on_death: DeathCallback::Monster, }); orc.ai = Some(Ai::Basic); orc } else { // 20% chance of spawning a troll let mut troll = Object::new(x, y, 'T', "troll", DARKER_GREEN, true); troll.fighter = Some(Fighter { max_hp: 16, hp: 16, defense: 1, power: 4, on_death: DeathCallback::Monster, }); troll.ai = Some(Ai::Basic); troll }; monster.alive = true; objects.push(monster); } } // Generate some items let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1); for _ in 0..num_items { // Choose random spot for this item let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); // Only place it if the tile is note blocked if !is_blocked(x, y, map, objects) { let dice = rand::random::(); let item = if dice < 0.7 { // Create a healing potion (70% chance) let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false); object.item = Some(Item::Heal); object } else if dice < 0.7 + 0.1 { // Create a lightning bolt scroll (10% chance) let mut object = Object::new(x, y, '#', "scroll of lightning bolt", LIGHT_YELLOW, false); object.item = Some(Item::Lightning); object } else if dice < 0.7 + 0.1 + 0.1 { // Create a fireball scroll (10% chance) let mut object = Object::new(x, y, '#', "scroll of fireball", LIGHT_YELLOW, false); object.item = Some(Item::Fireball); object } else { // Create a confuse scroll (10% chance) let mut object = Object::new(x, y, '#', "scroll of confusion", LIGHT_YELLOW, false); object.item = Some(Item::Confuse); object }; objects.push(item); } } } fn is_blocked(x: i32, y: i32, map: &Map, objects: &[Object]) -> bool { if map[x as usize][y as usize].blocked { return true; } objects .iter() .any(|object| object.blocks && object.pos() == (x, y)) } // Move by given amount, if the destination is not blocked fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) { let (x, y) = objects[id].pos(); if !is_blocked(x + dx, y + dy, map, objects) { objects[id].set_pos(x + dx, y + dy); } } fn player_move_or_attack(dx: i32, dy: i32, game: &mut Game, objects: &mut [Object]) { // The coordinates the player is moving to / attacking let x = objects[PLAYER].x + dx; let y = objects[PLAYER].y + dy; // Try to find an attackable object there let target_id = objects .iter() .position(|object| object.fighter.is_some() && object.pos() == (x, y)); // Attack target if found, move otherwise match target_id { Some(target_id) => { let (player, target) = mut_two(PLAYER, target_id, objects); player.attack(target, game); } None => { move_by(PLAYER, dx, dy, &game.map, objects); } } } fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mut [Object]) { // Vector from this object to the target, and distance let dx = target_x - objects[id].x; let dy = target_y - objects[id].y; let distance = ((dx.pow(2) + dy.pow(2)) as f32).sqrt(); // Normalize it to length 1 (preserving direction), then round it and // convert it to integer so the movement is restricted to the map grid let dx = (dx as f32 / distance).round() as i32; let dy = (dy as f32 / distance).round() as i32; move_by(id, dx, dy, map, objects); } fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) { use Ai::*; if let Some(ai) = objects[monster_id].ai.take() { let new_ai = match ai { Basic => ai_basic(monster_id, tcod, game, objects), Confused { previous_ai, num_turns, } => ai_confused(monster_id, tcod, game, objects, previous_ai, num_turns), }; objects[monster_id].ai = Some(new_ai); } } fn ai_basic(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) -> Ai { // A basic monster takes its turn. If you can see it, it can see you. let (monster_x, monster_y) = objects[monster_id].pos(); if tcod.fov.is_in_fov(monster_x, monster_y) { if objects[monster_id].distance_to(&objects[PLAYER]) >= 2.0 { // Move towards player if far away let (player_x, player_y) = objects[PLAYER].pos(); move_towards(monster_id, player_x, player_y, &game.map, objects); } else if objects[PLAYER].fighter.map_or(false, |f| f.hp > 0) { // Close enough, attach! (if the player is still alive.) let (monster, player) = mut_two(monster_id, PLAYER, objects); monster.attack(player, game); } } Ai::Basic } fn ai_confused( monster_id: usize, _tcod: &Tcod, game: &mut Game, objects: &mut [Object], previous_ai: Box, num_turns: i32, ) -> Ai { if num_turns >= 0 { // Still confused ... // Move in a random direction, and decrease the number of turns confused move_by( monster_id, rand::thread_rng().gen_range(-1, 2), rand::thread_rng().gen_range(-1, 2), &game.map, objects, ); Ai::Confused { previous_ai: previous_ai, num_turns: num_turns - 1, } } else { // Restore previous AI (this one will be deleted) game.messages.add( format!("The {} is no longer confused!", objects[monster_id].name), RED, ); *previous_ai } } fn mut_two(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut T, &mut T) { assert!(first_index != second_index); let split_at_index = cmp::max(first_index, second_index); let (first_slice, second_slice) = items.split_at_mut(split_at_index); if first_index < second_index { (&mut first_slice[first_index], &mut second_slice[0]) } else { (&mut second_slice[0], &mut first_slice[second_index]) } } fn player_death(player: &mut Object, game: &mut Game) { // The game ended! game.messages.add("You died!", RED); // For added effect, transform the player into corpse! player.char = '%'; player.color = DARK_RED; } fn monster_death(monster: &mut Object, game: &mut Game) { // Transform it into a nasty corpse! It doesn't block, can't be // attacked and doesn't move game .messages .add(format!("{} is dead!", monster.name), ORANGE); monster.char = '%'; monster.color = DARK_RED; monster.blocks = false; monster.fighter = None; monster.ai = None; monster.name = format!("remains of {}", monster.name); } fn menu>(header: &str, options: &[T], width: i32, root: &mut Root) -> Option { // Limit the amount of menu options assert!( options.len() <= 26, "Cannot have a menu with more than 26 options." ); // Calculate total height for the header (after auto-wrap) and one line per option let header_height = if header.is_empty() { 0 } else { root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header) }; let height = options.len() as i32 + header_height; // Create an off-screen console that represents the menu's window let mut window = Offscreen::new(width, height); // Print the header, with auto-wrap window.set_default_foreground(WHITE); window.print_rect_ex( 0, 0, width, height, BackgroundFlag::None, TextAlignment::Left, header, ); // Print all the options for (index, option_text) in options.iter().enumerate() { let menu_letter = (b'a' + index as u8) as char; let text = format!("({}) {}", menu_letter, option_text.as_ref()); window.print_ex( 0, header_height + index as i32, BackgroundFlag::None, TextAlignment::Left, text, ); } // Blit the contents of the menu window to the root console let x = SCREEN_WIDTH / 2 - width / 2; let y = SCREEN_HEIGHT / 2 - height / 2; blit(&window, (0, 0), (width, height), root, (x, y), 1.0, 0.7); // Present the root console to the player and wait for a key-press root.flush(); let key = root.wait_for_keypress(true); // Convert the ASCII code to an index; if it corresponds to an option, return it if key.printable.is_alphabetic() { let index = key.printable.to_ascii_lowercase() as usize - 'a' as usize; if index < options.len() { Some(index) } else { None } } else { None } } fn inventory_menu(inventory: &[Object], header: &str, root: &mut Root) -> Option { // Show a menu with each inventory item as an option let options = if inventory.len() == 0 { vec!["Inventory is empty.".into()] } else { inventory.iter().map(|item| item.name.clone()).collect() }; let inventory_index = menu(header, &options, INVENTORY_WIDTH, root); // If an item was chosen, return it if inventory.len() > 0 { inventory_index } else { None } } fn render_bar( panel: &mut Offscreen, x: i32, y: i32, total_width: i32, name: &str, value: i32, maximum: i32, bar_color: Color, back_color: Color, ) { // Render a bar (HP, experience, etc). First calculate the width of the bar let bar_width = (value as f32 / maximum as f32 * total_width as f32) as i32; // Render the background first panel.set_default_background(back_color); panel.rect(x, y, total_width, 1, false, BackgroundFlag::Screen); // Now render the bar on top panel.set_default_background(bar_color); if bar_width > 0 { panel.rect(x, y, bar_width, 1, false, BackgroundFlag::Screen); } // Finally, some centered text with the values panel.set_default_foreground(WHITE); panel.print_ex( x + total_width / 2, y, BackgroundFlag::None, TextAlignment::Center, &format!("{}: {}/{}", name, value, maximum), ); } fn get_names_under_mouse(mouse: Mouse, objects: &[Object], fov_map: &FovMap) -> String { let (x, y) = (mouse.cx as i32, mouse.cy as i32); // Create a list with the names of all objects at the mouse's coordinates and in FOV let names = objects .iter() .filter(|obj| obj.pos() == (x, y) && fov_map.is_in_fov(obj.x, obj.y)) .map(|obj| obj.name.clone()) .collect::>(); names.join(", ") } fn pick_item_up(object_id: usize, game: &mut Game, objects: &mut Vec) { if game.inventory.len() >= 26 { game.messages.add( format!( "Your inventory is full, cannot pick up {}.", objects[object_id].name ), RED, ); } else { let item = objects.swap_remove(object_id); game .messages .add(format!("You picked up a {}!", item.name), GREEN); game.inventory.push(item); } } fn use_item(inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) { use Item::*; // just call the "use_function" if it is defined if let Some(item) = game.inventory[inventory_id].item { let on_use = match item { Heal => cast_heal, Lightning => cast_lightning, Confuse => cast_confuse, Fireball => cast_fireball, }; match on_use(inventory_id, tcod, game, objects) { UseResult::UsedUp => { // Destroy after use, unless it was cancelled for some reason game.inventory.remove(inventory_id); } UseResult::Cancelled => { game.messages.add("Cancelled", WHITE); } } } else { game.messages.add( format!("The {} cannot be used.", game.inventory[inventory_id].name), WHITE, ); } } fn cast_heal( _inventory_id: usize, _tcod: &mut Tcod, game: &mut Game, objects: &mut [Object], ) -> UseResult { // Heal the player if let Some(fighter) = objects[PLAYER].fighter { if fighter.hp == fighter.max_hp { game.messages.add("You are already at full health.", RED); return UseResult::Cancelled; } game .messages .add("Your wounds start to feel better!", LIGHT_VIOLET); objects[PLAYER].heal(HEAL_AMOUNT); return UseResult::UsedUp; } UseResult::Cancelled } fn cast_lightning( _inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object], ) -> UseResult { // Find closest enemy (inside a maximum range) and damage it let monster_id = closest_monster(tcod, objects, LIGHTNING_RANGE); if let Some(monster_id) = monster_id { // Zap it ! game.messages.add( format!( "A lightning bolt strikes the {} with a loud thunder! \ The damage is {} hit points.", objects[monster_id].name, LIGHTNING_DAMAGE ), LIGHT_BLUE, ); objects[monster_id].take_damage(LIGHTNING_DAMAGE, game); UseResult::UsedUp } else { // No enemy found within maximum range game .messages .add("No enemy is close enough to strike.", RED); UseResult::Cancelled } } fn cast_confuse( _inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object], ) -> UseResult { game.messages.add( "Left-click an enemy to confuse it, or right click to cancel.", LIGHT_CYAN, ); let monster_id = target_monster(tcod, game, objects, Some(CONFUSE_RANGE as f32)); if let Some(monster_id) = monster_id { let old_ai = objects[monster_id].ai.take().unwrap_or(Ai::Basic); // Replace the monster's AI with a "confused" one; after // some turns it will restore the old AI objects[monster_id].ai = Some(Ai::Confused { previous_ai: Box::new(old_ai), num_turns: CONFUSE_NUM_TURNS, }); game.messages.add( format!( "The eyes of {} look vacant, as he starts to stumble around!", objects[monster_id].name ), LIGHT_GREEN, ); UseResult::UsedUp } else { // No enemy found within maximum range game .messages .add("No enemy is close enough to strike.", RED); UseResult::Cancelled } } fn cast_fireball( _inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object], ) -> UseResult { // Ask the player for a target tile to throw a fireball at game.messages.add( "Left-click a target tile for the fireball, or right-click to cancel.", LIGHT_CYAN, ); let (x, y) = match target_tile(tcod, game, objects, None) { Some(tile_pos) => tile_pos, None => return UseResult::Cancelled, }; game.messages.add( format!( "The fireball explodes, burning everything within {} tiles!", FIREBALL_RADIUS ), ORANGE, ); for obj in objects { if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() { game.messages.add( format!( "The {} gets burned for {} hit points.", obj.name, FIREBALL_DAMAGE ), ORANGE, ); obj.take_damage(FIREBALL_DAMAGE, game); } } UseResult::UsedUp } // Find closest enemy, up to a maximum range, and in the player's FOV fn closest_monster(tcod: &Tcod, objects: &[Object], max_range: i32) -> Option { let mut closest_enemy = None; // Start with (slightly more) than maximum range let mut closest_dist = (max_range + 1) as f32; for (id, object) in objects.iter().enumerate() { if (id != PLAYER) && object.fighter.is_some() && object.ai.is_some() && tcod.fov.is_in_fov(object.x, object.y) { // Calculate the distance between this object and the player let dist = objects[PLAYER].distance_to(object); if dist < closest_dist { // It's closer, so remember it closest_enemy = Some(id); closest_dist = dist; } } } closest_enemy } // Return the position of a tile left-clicked in player's FOV (optionally in a // range), or (None, None) if right-clicked. fn target_tile( tcod: &mut Tcod, game: &mut Game, objects: &[Object], max_range: Option, ) -> Option<(i32, i32)> { use tcod::input::KeyCode::Escape; loop { // Render the screen. This erases the inventory and shows the names of // objects under the mouse. tcod.root.flush(); let event = input::check_for_event(input::KEY_PRESS | input::MOUSE).map(|e| e.1); match event { Some(Event::Mouse(m)) => tcod.mouse = m, Some(Event::Key(k)) => tcod.key = k, None => tcod.key = Default::default(), } render_all(tcod, game, objects, false); let (x, y) = (tcod.mouse.cx as i32, tcod.mouse.cy as i32); // Accept the target if the player clicked in FOV, and in case a range // is specified, if it's in that range let in_fov = (x < MAP_WIDTH) && (y < MAP_HEIGHT) && tcod.fov.is_in_fov(x, y); let in_range = max_range.map_or(true, |range| objects[PLAYER].distance(x, y) <= range); if tcod.mouse.lbutton_pressed && in_fov && in_range { return Some((x, y)); } if tcod.mouse.rbutton_pressed || tcod.key.code == Escape { return None; // Cancel if the player right-clicked or pressed escape. } } } // Returns a clicked monster inside FOV up to a range, or None if right-clicked fn target_monster( tcod: &mut Tcod, game: &mut Game, objects: &[Object], max_range: Option, ) -> Option { loop { match target_tile(tcod, game, objects, max_range) { Some((x, y)) => { // Return the first clicked monster, otherwise continue looping for (id, obj) in objects.iter().enumerate() { if obj.pos() == (x, y) && obj.fighter.is_some() && id != PLAYER { return Some(id); } } } None => return None, } } } fn drop_item(inventory_id: usize, game: &mut Game, objects: &mut Vec) { let mut item = game.inventory.remove(inventory_id); item.set_pos(objects[PLAYER].x, objects[PLAYER].y); game .messages .add(format!("You dropped a {}.", item.name), YELLOW); objects.push(item); } fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) { if fov_recompute { let player = &objects[PLAYER]; tcod .fov .compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO); } // Only render objects in fov let mut to_draw: Vec<_> = objects .iter() .filter(|o| tcod.fov.is_in_fov(o.x, o.y)) .collect(); // Sort so that non-blocking objects are rendered behind blocking objects to_draw.sort_by(|o1, o2| o1.blocks.cmp(&o2.blocks)); // Draw objects in the list for object in &to_draw { object.draw(&mut tcod.con); } // 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, ); // Prepare to render the GUI panel tcod.panel.set_default_background(BLACK); tcod.panel.clear(); // Show the player's stats let hp = objects[PLAYER].fighter.map_or(0, |f| f.hp); let max_hp = objects[PLAYER].fighter.map_or(0, |f| f.max_hp); render_bar( &mut tcod.panel, 1, 1, BAR_WIDTH, "HP", hp, max_hp, LIGHT_RED, DARKER_RED, ); // Display names of objects under the mouse tcod.panel.set_default_foreground(LIGHT_GREY); tcod.panel.print_ex( 1, 0, BackgroundFlag::None, TextAlignment::Left, get_names_under_mouse(tcod.mouse, objects, &tcod.fov), ); // Print the game messages, one line at a time let mut y = MSG_HEIGHT as i32; for &(ref msg, color) in game.messages.iter().rev() { let msg_height = tcod.panel.get_height_rect(MSG_X, y, MSG_WIDTH, 0, msg); y -= msg_height; if y < 0 { break; } tcod.panel.set_default_foreground(color); tcod.panel.print_rect(MSG_X, y, MSG_WIDTH, 0, msg); } // Blit contents of "panel" to the root console blit( &tcod.panel, (0, 0), (SCREEN_WIDTH, PANEL_HEIGHT), &mut tcod.root, (0, PANEL_Y), 1.0, 1.0, ); } fn initialise_fov(tcod: &mut Tcod, map: &Map) { // Populate FOV map, according to generated map for y in 0..MAP_HEIGHT { for x in 0..MAP_WIDTH { tcod.fov.set( x, y, !map[x as usize][y as usize].block_sight, !map[x as usize][y as usize].blocked, ) } } } fn new_game(tcod: &mut Tcod) -> (Game, Vec) { // Create player object let mut player = Object::new(0, 0, '@', "player", WHITE, true); player.alive = true; player.fighter = Some(Fighter { max_hp: 30, hp: 30, defense: 2, power: 5, on_death: DeathCallback::Player, }); // List of objects in the world let mut objects = vec![player]; let mut game = Game { // Generate map map: make_map(&mut objects), messages: Messages::new(), inventory: vec![], }; initialise_fov(tcod, &game.map); // A warm welcoming message! game.messages.add( "Welcome stranger! Prepare to perish in the Tomb of Doom.", RED, ); (game, objects) } fn play_game(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) { // force FOV "recompute" first time through the game loop let mut previous_player_position = (-1, -1); // Main game loop while !tcod.root.window_closed() { // clear the screen of the previous frame tcod.con.clear(); match input::check_for_event(input::MOUSE | input::KEY_PRESS) { Some((_, Event::Mouse(m))) => tcod.mouse = m, Some((_, Event::Key(k))) => tcod.key = k, _ => tcod.key = Default::default(), } // Render the screen let fov_recompute = previous_player_position != (objects[PLAYER].pos()); render_all(tcod, game, &objects, fov_recompute); tcod.root.flush(); // Handle keys and exit game if needed previous_player_position = objects[PLAYER].pos(); let player_action = handle_keys(tcod, game, objects); if player_action == PlayerAction::Exit { break; } // Let monsters take their turn if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn { for id in 0..objects.len() { if objects[id].ai.is_some() { ai_take_turn(id, tcod, game, objects); } } } } } fn main_menu(tcod: &mut Tcod) { let img = tcod::image::Image::from_file("menu_background.png") .ok() .expect("Background image not found"); while !tcod.root.window_closed() { // Show the background image, at twice the regular console resolution tcod::image::blit_2x(&img, (0, 0), (-1, -1), &mut tcod.root, (0, 0)); tcod.root.set_default_foreground(LIGHT_YELLOW); tcod.root.print_ex( SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 4, BackgroundFlag::None, TextAlignment::Center, "TOMB OF DOOM", ); tcod.root.print_ex( SCREEN_WIDTH / 2, SCREEN_HEIGHT - 2, BackgroundFlag::None, TextAlignment::Center, "By m", ); // Show options and wait for the player's choice let choices = &["Play a new game", "Continue last game", "Quit"]; let choice = menu("", choices, 24, &mut tcod.root); match choice { Some(0) => { // New game let (mut game, mut objects) = new_game(tcod); play_game(tcod, &mut game, &mut objects); } Some(2) => { // Quit break; } _ => {} } } } fn main() { 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 mut tcod = Tcod { root, con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT), panel: Offscreen::new(SCREEN_WIDTH, PANEL_HEIGHT), fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT), key: Default::default(), mouse: Default::default(), }; main_menu(&mut tcod); }