summaryrefslogtreecommitdiff
path: root/graphics
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-11-07 13:44:26 -0500
committerFreya Murphy <freya@freyacat.org>2025-11-07 13:44:26 -0500
commitee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e (patch)
tree92454885e7c8f60f17cc99a95366ed6ab0609e44 /graphics
parentgraphics: update get_key_pressed to use try_borrow_mut instead of borrow_mut (diff)
downloadDungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.tar.gz
DungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.tar.bz2
DungeonCrawl-ee5060f88ad32a29a2dd28be7f9d5f3c2bb34b0e.zip
graphics: new UI!
Diffstat (limited to 'graphics')
-rw-r--r--graphics/src/render.rs380
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;