From a17cc6f2e62ab05e8c3b2c0139bc9c15ea595847 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 1 Nov 2021 14:46:45 -0400 Subject: [PATCH] Add basic ui with healthbar, game log, and mouse tooltips --- src/damage_system.rs | 14 +++- src/gamelog.rs | 3 + src/gui.rs | 149 +++++++++++++++++++++++++++++++++++++ src/main.rs | 8 ++ src/map.rs | 16 ++-- src/melee_combat_system.rs | 10 +-- 6 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 src/gamelog.rs create mode 100644 src/gui.rs diff --git a/src/damage_system.rs b/src/damage_system.rs index 26a71b6..0df8a6f 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -1,4 +1,4 @@ -use crate::{CombatStats, Player, SufferDamage}; +use crate::{gamelog::GameLog, CombatStats, Name, Player, SufferDamage}; use rltk::console; use specs::prelude::*; @@ -28,12 +28,22 @@ pub fn delete_the_dead(ecs: &mut World) { { let combat_stats = ecs.read_storage::(); let players = ecs.read_storage::(); + let names = ecs.read_storage::(); let entities = ecs.entities(); + let mut log = ecs.write_resource::(); + for (entity, stats) in (&entities, &combat_stats).join() { if stats.hp < 1 { let player = players.get(entity); match player { - None => dead.push(entity), + None => { + let victim_name = names.get(entity); + if let Some(victim_name) = victim_name { + log.entries.push(format!("{} is dead", &victim_name.name)); + } + + dead.push(entity) + } Some(_) => console::log("You are dead"), } } diff --git a/src/gamelog.rs b/src/gamelog.rs new file mode 100644 index 0000000..dbe28c0 --- /dev/null +++ b/src/gamelog.rs @@ -0,0 +1,3 @@ +pub struct GameLog { + pub entries: Vec, +} diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..5124b01 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,149 @@ +use crate::{gamelog::GameLog, CombatStats, Map, Name, Player, Position}; +use rltk::{Point, Rltk, RGB}; +use specs::prelude::*; + +pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { + ctx.draw_box( + 0, + 43, + 79, + 6, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + ); + + let combat_stats = ecs.read_storage::(); + let players = ecs.read_storage::(); + + // Display player health + for (_player, stats) in (&players, &combat_stats).join() { + let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp); + ctx.print_color( + 12, + 43, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + &health, + ); + + ctx.draw_bar_horizontal( + 29, + 43, + 51, + stats.hp, + stats.max_hp, + RGB::named(rltk::RED), + RGB::named(rltk::BLACK), + ); + } + + // Display logs + let log = ecs.fetch::(); + let mut y = 44; + for s in log.entries.iter().rev() { + if y < 49 { + ctx.print(2, y, s); + } + + y += 1; + } + + // Mouse cursor + let mouse_pos = ctx.mouse_pos(); + ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::MAGENTA)); + + draw_tooltips(ecs, ctx); +} + +fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { + let map = ecs.fetch::(); + let names = ecs.read_storage::(); + let positions = ecs.read_storage::(); + + let mouse_pos = ctx.mouse_pos(); + if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { + return; + } + let mut tooltip: Vec = Vec::new(); + + for (name, position) in (&names, &positions).join() { + let idx = map.xy_idx(position.x, position.y); + if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] { + tooltip.push(name.name.to_string()); + } + } + + if !tooltip.is_empty() { + let mut width: i32 = 0; + for s in tooltip.iter() { + if width < s.len() as i32 { + width = s.len() as i32; + } + } + width += 3; + + if mouse_pos.0 > 40 { + let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1); + let left_x = mouse_pos.0 - width; + let mut y = mouse_pos.1; + + for s in tooltip.iter() { + ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s); + let padding = (width - s.len() as i32) - 1; + for i in 0..padding { + ctx.print_color( + arrow_pos.x - i, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::GREY), + &" ".to_string(), + ); + } + + y += 1; + } + + ctx.print_color( + arrow_pos.x, + arrow_pos.y, + RGB::named(rltk::WHITE), + RGB::named(rltk::GREY), + &"->".to_string(), + ); + } else { + let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1); + let left_x = mouse_pos.0 + 3; + let mut y = mouse_pos.1; + + for s in tooltip.iter() { + ctx.print_color( + left_x + 1, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::GREY), + s, + ); + + let padding = (width - s.len() as i32) - 1; + for i in 0..padding { + ctx.print_color( + arrow_pos.x + 1 + i, + y, + RGB::named(rltk::WHITE), + RGB::named(rltk::GREY), + &" ".to_string(), + ); + } + y += 1; + } + + ctx.print_color( + arrow_pos.x, + arrow_pos.y, + RGB::named(rltk::WHITE), + RGB::named(rltk::GREY), + &"<-".to_string(), + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index 9733737..48b3bd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,9 @@ mod melee_combat_system; use melee_combat_system::MeleeCombatSystem; mod damage_system; use damage_system::DamageSystem; +mod gamelog; +mod gui; +pub use gamelog::GameLog; pub const MAP_SIZE: usize = 80 * 50; @@ -102,6 +105,8 @@ impl GameState for State { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) } } + + gui::draw_ui(&self.ecs, ctx); } } @@ -206,6 +211,9 @@ fn main() -> rltk::BError { gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(player_entity); gs.ecs.insert(RunState::PreRun); + gs.ecs.insert(gamelog::GameLog { + entries: vec!["Welcome to Rusty Roguelike".to_string()], + }); rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index d9855bf..ea0c60a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,8 +1,12 @@ -use super::{Rect, MAP_SIZE}; +use super::Rect; use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, SmallVec, RGB}; use specs::prelude::*; use std::cmp::{max, min}; +const MAPWIDTH: usize = 80; +const MAPHEIGHT: usize = 50; +const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; + #[derive(PartialEq, Copy, Clone)] pub enum TileType { Wall, @@ -58,14 +62,14 @@ impl Map { /// This gives a handful of random rooms and corridors joining them together pub fn new_map_rooms_and_corridors() -> Map { let mut map = Map { - tiles: vec![TileType::Wall; MAP_SIZE], + tiles: vec![TileType::Wall; MAPCOUNT], rooms: Vec::new(), width: 80, height: 50, - revealed_tiles: vec![false; MAP_SIZE], - visible_tiles: vec![false; MAP_SIZE], - blocked: vec![false; MAP_SIZE], - tile_content: vec![Vec::new(); MAP_SIZE], + revealed_tiles: vec![false; MAPCOUNT], + visible_tiles: vec![false; MAPCOUNT], + blocked: vec![false; MAPCOUNT], + tile_content: vec![Vec::new(); MAPCOUNT], }; const MAX_ROOMS: i32 = 30; diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index fd1dad9..dc696d0 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,5 +1,4 @@ -use crate::{CombatStats, Name, SufferDamage, WantsToMelee}; -use rltk::console; +use crate::{gamelog::GameLog, CombatStats, Name, SufferDamage, WantsToMelee}; use specs::prelude::*; pub struct MeleeCombatSystem {} @@ -7,6 +6,7 @@ pub struct MeleeCombatSystem {} impl<'a> System<'a> for MeleeCombatSystem { type SystemData = ( Entities<'a>, + WriteExpect<'a, GameLog>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, ReadStorage<'a, CombatStats>, @@ -14,7 +14,7 @@ impl<'a> System<'a> for MeleeCombatSystem { ); fn run(&mut self, data: Self::SystemData) { - let (entities, mut wants_melee, names, combat_stats, mut inflict_damage) = data; + let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data; for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() @@ -26,12 +26,12 @@ impl<'a> System<'a> for MeleeCombatSystem { let damage = i32::max(0, stats.power - target_stats.defense); if damage == 0 { - console::log(&format!( + log.entries.push(format!( "{} is unable to hurt {}", &name.name, &target_name.name )); } else { - console::log(&format!( + log.entries.push(format!( "{} hits {}, for {} hp", &name.name, &target_name.name, damage ));