diff --git a/src/main.rs b/src/main.rs index 083834b..e4e349e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ const SCREEN_WIDTH: i32 = 80; const SCREEN_HEIGHT: i32 = 50; const FPS: i32 = 20; const MAP_WIDTH: i32 = 80; -const MAP_HEIGHT: i32 = 45; +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, @@ -38,10 +38,18 @@ 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; struct Tcod { root: Root, con: Offscreen, + panel: Offscreen, fov: FovMap, } @@ -94,7 +102,7 @@ impl Object { ((dx.pow(2) + dy.pow(2)) as f32).sqrt() } - pub fn take_damage(&mut self, damage: i32) { + 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 { @@ -106,26 +114,33 @@ impl Object { if let Some(fighter) = self.fighter { if fighter.hp <= 0 { self.alive = false; - fighter.on_death.callback(self); + fighter.on_death.callback(self, game); } } } - pub fn attack(&mut self, target: &mut Object) { + 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 - println!( - "{} attacks {} for {} hit points.", - self.name, target.name, damage + game.messages.add( + format!( + "{} attacks {} for {} hit points.", + self.name, target.name, damage + ), + WHITE, ); - target.take_damage(damage); + + target.take_damage(damage, game); } else { - println!( - "{} attacks {} but it has no effect!", - self.name, target.name - ) + game.messages.add( + format!( + "{} attacks {} but it has no effect!", + self.name, target.name + ), + WHITE, + ); } } } @@ -159,6 +174,7 @@ type Map = Vec>; struct Game { map: Map, + messages: Messages, } // A rectangle on the map, used to characterise a room. @@ -201,6 +217,26 @@ struct Fighter { 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, @@ -220,17 +256,17 @@ enum DeathCallback { } impl DeathCallback { - fn callback(self, object: &mut Object) { + fn callback(self, object: &mut Object, game: &mut Game) { use DeathCallback::*; - let callback: fn(&mut Object) = match self { + let callback = match self { Player => player_death, Monster => monster_death, }; - callback(object); + callback(object, game); } } -fn handle_keys(tcod: &mut Tcod, game: &Game, objects: &mut Vec) -> PlayerAction { +fn handle_keys(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec) -> PlayerAction { use tcod::input::Key; use tcod::input::KeyCode::*; use PlayerAction::*; @@ -400,7 +436,7 @@ fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) { } } -fn player_move_or_attack(dx: i32, dy: i32, game: &Game, objects: &mut [Object]) { +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; @@ -414,7 +450,7 @@ fn player_move_or_attack(dx: i32, dy: i32, game: &Game, objects: &mut [Object]) match target_id { Some(target_id) => { let (player, target) = mut_two(PLAYER, target_id, objects); - player.attack(target); + player.attack(target, game); } None => { move_by(PLAYER, dx, dy, &game.map, objects); @@ -435,7 +471,7 @@ fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mu move_by(id, dx, dy, map, objects); } -fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &Game, objects: &mut [Object]) { +fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) { // 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) { @@ -446,7 +482,7 @@ fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &Game, objects: &mut [Obje } 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); + monster.attack(player, game); } } } @@ -462,19 +498,21 @@ fn mut_two(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut } } -fn player_death(player: &mut Object) { +fn player_death(player: &mut Object, game: &mut Game) { // The game ended! - println!("You died!"); + 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) { +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 - println!("{} is dead!", monster.name); + game + .messages + .add(format!("{} is dead!", monster.name), ORANGE); monster.char = '%'; monster.color = DARK_RED; monster.blocks = false; @@ -483,6 +521,41 @@ fn monster_death(monster: &mut Object) { monster.name = format!("remains of {}", monster.name); } +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 render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) { if fov_recompute { let player = &objects[PLAYER]; @@ -530,18 +603,6 @@ fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recomput } } - // Show the player's stats - tcod.root.set_default_foreground(WHITE); - if let Some(fighter) = objects[PLAYER].fighter { - tcod.root.print_ex( - 1, - SCREEN_HEIGHT - 2, - BackgroundFlag::None, - TextAlignment::Left, - format!("HP: {}/{} ", fighter.hp, fighter.max_hp), - ); - } - // Blit contents of "con" to the root console blit( &tcod.con, @@ -552,6 +613,48 @@ fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recomput 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, + ); + + // 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 main() { @@ -567,6 +670,7 @@ fn main() { 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), }; @@ -587,6 +691,7 @@ fn main() { let mut game = Game { // Generate map map: make_map(&mut objects), + messages: Messages::new(), }; // Populate FOV map, according to generated map @@ -603,6 +708,12 @@ fn main() { let mut previous_player_position = (-1, -1); + // A warm welcoming message! + game.messages.add( + "Welcome stranger! Prepare to perish in the Tomb of Doom.", + RED, + ); + // Main game loop while !tcod.root.window_closed() { tcod.con.clear(); @@ -612,7 +723,7 @@ fn main() { tcod.root.flush(); previous_player_position = objects[PLAYER].pos(); - let player_action = handle_keys(&mut tcod, &game, &mut objects); + let player_action = handle_keys(&mut tcod, &mut game, &mut objects); if player_action == PlayerAction::Exit { break; } @@ -621,7 +732,7 @@ fn main() { 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, &mut objects); + ai_take_turn(id, &tcod, &mut game, &mut objects); } } }