diff options
| author | Freya Murphy <freya@freyacat.org> | 2025-11-07 13:44:26 -0500 |
|---|---|---|
| committer | Freya Murphy <freya@freyacat.org> | 2025-11-07 13:44:26 -0500 |
| commit | ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e (patch) | |
| tree | 92454885e7c8f60f17cc99a95366ed6ab0609e44 /graphics/src | |
| parent | graphics: update get_key_pressed to use try_borrow_mut instead of borrow_mut (diff) | |
| download | DungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.tar.gz DungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.tar.bz2 DungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.zip | |
graphics: new UI!
Diffstat (limited to 'graphics/src')
| -rw-r--r-- | graphics/src/render.rs | 380 |
1 files changed, 276 insertions, 104 deletions
diff --git a/graphics/src/render.rs b/graphics/src/render.rs index 86ed6e6..87716de 100644 --- a/graphics/src/render.rs +++ b/graphics/src/render.rs @@ -2,7 +2,7 @@ use std::ops::Div; use dungeon::{ Direction, Dungeon, Entity, EntityKind, FPos, Floor, Item, MAP_SIZE, - PLAYER_FULL_HEALTH, Player, Pos, Tile, + PLAYER_INVENTORY_SIZE, Player, Pos, Tile, }; use raylib::{ RaylibHandle, RaylibThread, @@ -13,6 +13,12 @@ use raylib::{ texture::{RaylibTexture2D, RenderTexture2D, Texture2D}, }; +macro_rules! downcast { + ($usize:expr, $type:ty) => { + <$type>::try_from($usize).unwrap_or(<$type>::MAX) + }; +} + /// The baseline size of all ingame sprites and tile textures const BASE_TILE_SIZE: i32 = 32; @@ -26,28 +32,15 @@ const VIEW_DISTANCE: i32 = 5; struct Textures { // Tilemap atlas: Texture2D, - // UI - heart_full: Texture2D, - heart_half: Texture2D, - heart_empty: Texture2D, // Misc error: Texture2D, } impl Textures { fn new(handle: &mut RaylibHandle, thread: &RaylibThread) -> crate::Result<Self> { let atlas = handle.load_texture(thread, "assets/atlas.bmp")?; - let heart_full = handle.load_texture(thread, "assets/heart_full.bmp")?; - let heart_half = handle.load_texture(thread, "assets/heart_half.bmp")?; - let heart_empty = handle.load_texture(thread, "assets/heart_empty.bmp")?; let error = handle.load_texture(thread, "assets/error.bmp")?; - Ok(Self { - atlas, - heart_full, - heart_half, - heart_empty, - error, - }) + Ok(Self { atlas, error }) } fn item_texture(&self, _item: &Item) -> &Texture2D { @@ -67,8 +60,9 @@ const ATLAS_WALL_EDGE: (i32, i32) = (0, 1); const ATLAS_PLAYER: (i32, i32) = (0, 2); // UI atlas.bmp textures -const ATLAS_INV_TOP_LAYER: (i32, i32) = (1, 2); -const ATLAS_INV_BOTTOM_LAYER: (i32, i32) = (2, 2); +const ATLAS_INV_CONTAINER: (i32, i32) = (1, 1); +const ATLAS_HEART_ICON: (i32, i32) = (2, 1); +const ATLAS_DAMAGE_ICON: (i32, i32) = (3, 1); // Misc atlas.bmp textures const ATLAS_ERROR: (i32, i32) = (3, 3); @@ -142,9 +136,10 @@ impl Renderer { fn update_per_frame_data(&mut self, handle: &RaylibHandle) { // Get last known fps self.fps = handle.get_fps(); - // Get size (in pixels) to draw each tile + // Get size of framebuffer self.width = handle.get_render_width(); self.height = handle.get_render_height(); + // Get size (in pixels) to draw each tile self.tile_size = { let size = self.width.min(self.height); let dist = VIEW_DISTANCE * 2 + 1; @@ -158,6 +153,7 @@ impl Renderer { let cpos = dungeon.camera(); let width = self.width; let height = self.height; + let ui_height = self.get_ui_height() as f32; Camera2D { target: Vector2::from(cpos.xy()) .scale_by(self.tile_size as f32) @@ -166,7 +162,7 @@ impl Renderer { (MAP_SIZE as i32 * self.tile_size) as f32 - (width as f32 / 2.0), (MAP_SIZE as i32 * self.tile_size) as f32 - (height as f32 / 2.0), )), - offset: Vector2::new(width as f32 / 2.0, height as f32 / 2.0), + offset: Vector2::new(width as f32 / 2.0, height as f32 / 2.0 + ui_height), rotation: 0.0, zoom: 1.0, } @@ -317,63 +313,117 @@ impl Renderer { r.draw_atlas(&self.textures.atlas, tex, x * size, y * size, size, 0.0); } + /// Returns the known UI height for this frame + fn get_ui_height(&self) -> i32 { + self.tile_size + } + + /// Returns the padding used between ui elements + fn get_ui_padding(&self) -> i32 { + self.get_ui_height() / 10 + } + + /// Returns the font size for the UI + fn get_ui_font_size(&self) -> i32 { + self.get_ui_height() / 4 + } + /// Draws player HP, inventory, and floor number fn draw_ui<R>(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { + // Draw UI base rect + r.draw_rectangle(0, 0, self.width, self.get_ui_height(), Color::BLACK); + // Draw core ui components - self.draw_health(r, &dungeon.player); + self.draw_minimap(r, dungeon); self.draw_inventory(r, &dungeon.player); + self.draw_stats(r, &dungeon.player); // Draw debug info #[cfg(feature = "debug")] self.draw_debug_ui(r); } - /// Draw health meter in the top left of the screen - fn draw_health<R>(&self, r: &mut R, player: &Player) + /// Draw in game minimap + fn draw_minimap<R>(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { - let health = player.entity.health.unwrap_or(0); - let hearts = PLAYER_FULL_HEALTH.div_ceil(2); - let mut full_hearts = health / 2; - let mut half_hearts = health % 2; - let mut empty_hearts = hearts.saturating_sub(full_hearts + half_hearts); + let padding = self.get_ui_padding(); + let y = padding; - let size = self.tile_size.div(2).max(BASE_TILE_SIZE); - let y = BASE_TILE_SIZE; - let mut x = BASE_TILE_SIZE; - loop { - let tex = if full_hearts > 0 { - full_hearts -= 1; - &self.textures.heart_full - } else if half_hearts > 0 { - half_hearts -= 1; - &self.textures.heart_half - } else if empty_hearts > 0 { - empty_hearts -= 1; - &self.textures.heart_empty - } else { - break; - }; + // Draw MAP vert text + let text_x = padding; + self.draw_vertical_text(r, text_x, y, "MAP", Color::WHITE); - let dest_rec = Rectangle { - x: x as f32, - y: y as f32, - width: size as f32, - height: size as f32, - }; - r.draw_texture_pro( - tex, - FULL_SOURCE_REC, - dest_rec, - Vector2::zero(), - 0.0, - Color::WHITE, + // Draw minimap in top left of UI bar + let minimap_x = text_x + self.get_ui_font_size() + padding; + let size = self.get_ui_height() - padding * 2; + r.draw_rectangle(minimap_x, y, size, size, Color::DARKGRAY); + + let size_u16 = downcast!(size, u16); + let group_size = (MAP_SIZE / size_u16).max(1); + let groups = MAP_SIZE / group_size; + let dot_size = size_u16 / groups; + + let mut draw_dot = |x_idx: u16, y_idx: u16, color| { + r.draw_rectangle( + minimap_x + (x_idx * dot_size) as i32, + padding + (y_idx * dot_size) as i32, + dot_size as i32, + dot_size as i32, + color, ); - x += size; + }; + + // TODO: fog of war! + for x_idx in 0..groups { + for y_idx in 0..groups { + let x = x_idx * group_size; + let y = y_idx * group_size; + let Some(pos) = Pos::new(x, y) else { continue }; + + // Get the color of the tile + let color = dungeon.floor.minimap_color(pos, group_size); + draw_dot(x_idx, y_idx, color); + } + } + + // Draw enemy dots + for enemy in &dungeon.enemies { + let (x, y) = enemy.entity.pos.xy(); + let x_idx = x / group_size; + let y_idx = y / group_size; + draw_dot(x_idx, y_idx, Color::RED); + } + + // Draw player dot + { + let (x, y) = dungeon.player.entity.pos.xy(); + let x_idx = x / group_size; + let y_idx = y / group_size; + draw_dot(x_idx, y_idx, Color::LIME); + } + } + + /// Draws vertical text + fn draw_vertical_text<R>(&self, r: &mut R, x: i32, y: i32, text: &str, color: Color) + where + R: RaylibDraw, + { + let font_size = self.get_ui_font_size(); + let padding = self.get_ui_padding() / 2; + let spacing = font_size + padding; + let mut byte_off = 0; + for (idx_usize, char) in text.chars().enumerate() { + let idx = downcast!(idx_usize, i32); + let byte_len = char.len_utf8(); + let str = &text[byte_off..byte_off + byte_len]; + let char_y = y + idx * spacing; + r.draw_text(str, x, char_y, font_size, color); + byte_off += byte_len; } } @@ -383,65 +433,122 @@ impl Renderer { where R: RaylibDraw, { - let len = i32::try_from(player.inventory.len()).unwrap_or(0); + let slots = downcast!(PLAYER_INVENTORY_SIZE, i32); + let padding = self.get_ui_padding(); + let font_size = self.get_ui_font_size(); + let ui_height = self.get_ui_height(); - // size of the inv blocks - let size = self.tile_size.div(2).max(BASE_TILE_SIZE); - let half_size = size as f32 / 2.0; + let text_len = font_size + padding; + let slot_len = ui_height - padding * 2; + let slots_len = slot_len * slots; + let full_len = text_len + slots_len; - // position of the inv blocks - let y = self.height - size; - let mut x = self.width / 2 - (size * len / 2); + let start_x = self.width / 2 - full_len / 2; + let text_x = start_x; + let slots_x = text_x + text_len; - // size of font for number index - let font_size = size / 3; - let font_offset = font_size * 2; + // Draw text + self.draw_vertical_text(r, text_x, padding, "INV", Color::WHITE); - for (idx, item) in player.inventory.iter().enumerate() { - let dest_rec = Rectangle { - x: x as f32, - y: y as f32, - width: size as f32, - height: size as f32, - }; - r.draw_atlas( - &self.textures.atlas, - ATLAS_INV_BOTTOM_LAYER, - x as f32 + half_size, - y as f32 + half_size, - size as f32, - 0.0, - ); - let tex = self.textures.item_texture(item); - r.draw_texture_pro( - tex, - FULL_SOURCE_REC, - dest_rec, - Vector2::zero(), - 0.0, - Color::WHITE, - ); - r.draw_atlas( + // Draw slots + for idx in 0..PLAYER_INVENTORY_SIZE { + if idx >= PLAYER_INVENTORY_SIZE { + // This should never happen! + // Maybe use a different type?? + break; + } + + let slot_x = slots_x + slot_len * downcast!(idx, i32); + let size = (ui_height - padding * 2) as f32; + + // Draw slot container + r.draw_inv_atlas( &self.textures.atlas, - ATLAS_INV_TOP_LAYER, - x as f32 + half_size, - y as f32 + half_size, - size as f32, + ATLAS_INV_CONTAINER, + slot_x as f32, + padding as f32, + size, 0.0, ); - r.draw_text( - &format!("{}", idx + 1), - x + font_offset + font_offset / 5, - y + font_offset, - font_size, - Color::WHITE, - ); - x += size; + + if let Some(item) = player.inventory.get(idx) { + let tex = self.textures.item_texture(item); + let item_padding = padding as f32 * 3.0; + let dest_rec = Rectangle { + x: slot_x as f32 + item_padding / 2.0, + y: padding as f32 + item_padding / 2.0, + width: size - item_padding, + height: size - item_padding, + }; + r.draw_texture_pro( + tex, + FULL_SOURCE_REC, + dest_rec, + Vector2::zero(), + 0.0, + Color::WHITE, + ); + } } } + /// Draw player health & equpped weapon damage + fn draw_stats<R>(&self, r: &mut R, player: &Player) + where + R: RaylibDraw, + { + let health = player.entity.health.unwrap_or(0); + let damage = 0; // TODO: calc damage + + let padding = self.get_ui_padding(); + let font_size = self.get_ui_font_size(); + + let text_width = font_size * 3 + padding; + let icon_width = font_size + padding; + let stats_x = self.width - text_width - icon_width; + let icon_x = stats_x; + let text_x = icon_x + icon_width; + + // draw health + let heart_y = padding; + r.draw_inv_atlas( + &self.textures.atlas, + ATLAS_HEART_ICON, + icon_x as f32, + heart_y as f32, + icon_width as f32, + 0.0, + ); + r.draw_text( + &format!("x{health:02}"), + text_x, + heart_y + padding, + font_size, + Color::WHITE, + ); + + // draw damage + let damage_y = heart_y + font_size + padding; + r.draw_inv_atlas( + &self.textures.atlas, + ATLAS_DAMAGE_ICON, + icon_x as f32, + damage_y as f32, + icon_width as f32, + 0.0, + ); + r.draw_text( + &format!("x{damage:02}"), + text_x, + damage_y + padding, + font_size, + Color::WHITE, + ); + } + /// Draws debug information ontop of other UI elements /// Called by default if `debug` featue is enabled + #[cfg(feature = "debug")] fn draw_debug_ui<R>(&self, r: &mut R) where R: RaylibDraw, @@ -450,6 +557,7 @@ impl Renderer { } /// Draw FPS counter (debug) + #[cfg(feature = "debug")] fn draw_fps<R>(&self, r: &mut R) where R: RaylibDraw, @@ -473,6 +581,50 @@ impl Vector2Ext for Vector2 { } } +trait TileExt { + fn minimap_score(&self) -> u8; +} +impl TileExt for Tile { + fn minimap_score(&self) -> u8 { + match self { + // Walls + Self::Wall => 0, + // Empty + Self::Air => 1, + // Stairs + Self::Stairs => 2, + } + } +} + +trait FloorExt { + fn minimap_color(&self, pos: Pos, group_size: u16) -> Color; +} +impl FloorExt for Floor { + fn minimap_color(&self, pos: Pos, group_size: u16) -> Color { + let mut score = 0; + + let (x, y_start) = pos.xy(); + for y in y_start..y_start + group_size { + let idx = (x + y * MAP_SIZE) as usize; + let tiles = &self.tiles()[idx..idx + group_size as usize]; + let new_score = tiles.iter().map(TileExt::minimap_score).max().unwrap_or(0); + score = score.max(new_score); + } + + match score { + // Walls + 0 => Color::DARKGRAY, + // Empty + 1 => Color::GRAY, + // Stairs + 2 => Color::WHITE, + // UNKNOWN (ERROR) + _ => Color::PINK, + } + } +} + trait RaylibDrawExt where Self: RaylibDraw, @@ -506,6 +658,26 @@ where self.draw_texture_pro(tex, source_rec, dest_rec, origin, rotation, Color::WHITE); } + /// Draw in INV element from an atlas + fn draw_inv_atlas( + &mut self, + tex: &Texture2D, + (ax, ay): (i32, i32), + x: f32, + y: f32, + size: f32, + rotation: f32, + ) { + self.draw_atlas( + tex, + (ax, ay), + x + size / 2.0, + y + size / 2.0, + size, + rotation, + ); + } + /// Draw dungeon tiles helper function fn draw_tilemap(&mut self, tex: &RenderTexture2D, size: f32, offset: f32) { let scale = size / BASE_TILE_SIZE as f32; |