use std::ops::Div; use dungeon::{ Direction, Dungeon, Entity, EntityKind, Floor, Item, MAP_SIZE, PLAYER_INVENTORY_SIZE, Player, Pos, Tile, }; use raylib::{ RaylibHandle, RaylibThread, camera::Camera2D, color::Color, math::{Rectangle, Vector2}, prelude::{RaylibDraw, RaylibMode2DExt, RaylibTextureModeExt}, texture::{RaylibTexture2D, RenderTexture2D, Texture2D}, }; macro_rules! downcast { ($usize:expr, $type:ty) => { <$type>::try_from($usize).unwrap_or(<$type>::MAX) }; } macro_rules! rect { {$x:expr, $y:expr, $w:expr, $h:expr $(,)?} => { ::raylib::math::Rectangle { x: ($x) as f32, y: ($y) as f32, width: ($w) as f32, height: ($h) as f32, } }; } macro_rules! vec2 { {$x:expr, $y:expr $(,)?} => { ::raylib::math::Vector2 { x: ($x) as f32, y: ($y) as f32, } }; } /// The baseline size of all ingame sprites and tile textures const BASE_TILE_SIZE: u16 = 32; /// The height of the wall (offset between tile layers) const WALL_HEIGHT: u16 = 13; /// The (prefered) view distance of the game const VIEW_DISTANCE: u16 = 5; // Tile atlas.bmp textures const ATLAS_WALL_SIDE: (u16, u16) = (0, 0); const ATLAS_FLOOR_FULL: (u16, u16) = (1, 0); const ATLAS_FLOOR_EMPTY: (u16, u16) = (2, 0); const ATLAS_WALL_TOP: (u16, u16) = (3, 0); const ATLAS_WALL_EDGE: (u16, u16) = (0, 1); // Entity atlas.bmp textures const ATLAS_PLAYER: (u16, u16) = (0, 2); // UI atlas.bmp textures const ATLAS_INV_CONTAINER: (u16, u16) = (1, 1); const ATLAS_HEART_ICON: (u16, u16) = (2, 1); const ATLAS_DAMAGE_ICON: (u16, u16) = (3, 1); // Misc atlas.bmp textures const ATLAS_ERROR: (u16, u16) = (3, 3); /// Full source rec for any texture const FULL_SOURCE_REC: Rectangle = rect! { 0.0, 0.0, BASE_TILE_SIZE, BASE_TILE_SIZE, }; #[derive(Debug)] struct Textures { // Tilemap atlas: Texture2D, // Misc error: Texture2D, } impl Textures { fn new(handle: &mut RaylibHandle, thread: &RaylibThread) -> crate::Result { let atlas = handle.load_texture(thread, "assets/atlas.bmp")?; let error = handle.load_texture(thread, "assets/error.bmp")?; Ok(Self { atlas, error }) } fn item_texture(&self, _item: &Item) -> &Texture2D { // TODO: make item textures &self.error } } #[derive(Debug)] pub struct Renderer { /* Persistant Render Data */ /// All loaded image resources/textures textures: Textures, /// Top layer of the tilemap (walls) tilemap_fg: RenderTexture2D, /// Bottom layer of the tilemap (floor and half-height walls) tilemap_bg: RenderTexture2D, /// Last computed hash of the tilemap (floor) tiles_hash: Option, /* Per Frame Caculated Data */ /// Current tile size we are rendering tile_size: u16, /// Last known FPS fps: u32, /// Render width of the current frame width: u16, /// Render height of the current frame height: u16, } impl Renderer { pub fn new(handle: &mut RaylibHandle, thread: &RaylibThread) -> crate::Result { // Load resources let textures = Textures::new(handle, thread)?; // Load render textures let tex_size = (MAP_SIZE * BASE_TILE_SIZE) as u32; let tilemap_fg = handle.load_render_texture(thread, tex_size, tex_size)?; let tilemap_bg = handle.load_render_texture(thread, tex_size, tex_size)?; Ok(Self { textures, tilemap_fg, tilemap_bg, tiles_hash: None, tile_size: 0, fps: 0, width: 0, height: 0, }) } pub fn draw_frame( &mut self, handle: &mut RaylibHandle, t: &RaylibThread, dungeon: &Dungeon, ) { self.update_per_frame_data(handle); let mut r = handle.begin_drawing(t); r.clear_background(Color::BLACK); self.draw_dungeon(&mut r, t, dungeon); self.draw_ui(&mut r, dungeon); } /// Update frame metadata while we still have access to the /// `RaylibHandle`. These fields are inaccessable once it's /// turned into a `RaylibDrawHandle`. fn update_per_frame_data(&mut self, handle: &RaylibHandle) { // Get last known fps self.fps = handle.get_fps(); // Get size of framebuffer self.width = downcast!(handle.get_render_width(), u16); self.height = downcast!(handle.get_render_height(), u16); // Get size (in pixels) to draw each tile self.tile_size = { let size = self.width.min(self.height); let dist = VIEW_DISTANCE * 2 + 1; let pixels = size.div(dist).max(BASE_TILE_SIZE); 1 << (u16::BITS - pixels.leading_zeros()) }; } /// Returns the Raylib Camera setup with needed 2D position/transforms fn render_camera(&self, dungeon: &Dungeon) -> Camera2D { let cpos = dungeon.camera(); let width = self.width; let height = self.height; let ui_height = self.get_ui_height(); Camera2D { target: Vector2::from(cpos.xy()) .scale_by(self.tile_size.into()) .max(vec2! {width/2, height/2}) .min(vec2! { (MAP_SIZE * self.tile_size) - (width/2), (MAP_SIZE * self.tile_size) - (height/2), }), offset: vec2! {width/2, height/2 + ui_height}, rotation: 0.0, zoom: 1.0, } } /// Draws the game dungeon fn draw_dungeon(&mut self, r: &mut R, t: &RaylibThread, dungeon: &Dungeon) where R: RaylibDraw + RaylibMode2DExt + RaylibTextureModeExt, { self.update_tilemaps(r, t, &dungeon.floor); let camera = self.render_camera(dungeon); let mut rc = r.begin_mode2D(camera); self.draw_bg_tilemap(&mut rc); self.draw_entities(&mut rc, dungeon); self.draw_fg_tilemap(&mut rc); } /// Updates all tilemaps fn update_tilemaps(&mut self, r: &mut R, t: &RaylibThread, floor: &Floor) where R: RaylibDraw + RaylibTextureModeExt, { let current_hash = floor.hash(); if let Some(old_hash) = self.tiles_hash && old_hash == current_hash { // Textures are up to date return; }; self.tiles_hash = Some(current_hash); self.update_fg_tilemap(r, t, floor); self.update_bg_tilemap(r, t, floor); } /// Draws the foregound tile map fn update_fg_tilemap(&mut self, r: &mut R, t: &RaylibThread, floor: &Floor) where R: RaylibDraw + RaylibTextureModeExt, { let size = BASE_TILE_SIZE; let mut rt = r.begin_texture_mode(t, &mut self.tilemap_fg); rt.clear_background(Color::BLANK); for pos in Pos::values() { let (xs, ys) = (pos.x() * size, pos.y() * size); // fg layer only draws a top walls if floor.get(pos) != Tile::Wall { continue; }; // draw base wall top texture rt.draw_atlas(&self.textures.atlas, ATLAS_WALL_TOP, xs, ys, size, 0.0); // draw top wall borders let is_wall = |dir| pos.step(dir).map_or(Tile::Wall, |p| floor.get(p)).is_wall(); if !is_wall(Direction::North) { rt.draw_atlas(&self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, size, 0.0); } if !is_wall(Direction::East) { rt.draw_atlas(&self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, size, 90.0); } if !is_wall(Direction::South) { rt.draw_atlas(&self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, size, 180.0); } if !is_wall(Direction::West) { rt.draw_atlas(&self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, size, 270.0); } } } /// Draws the foregound tile map fn update_bg_tilemap(&mut self, r: &mut R, t: &RaylibThread, floor: &Floor) where R: RaylibDraw + RaylibTextureModeExt, { let size = BASE_TILE_SIZE; let mut rt = r.begin_texture_mode(t, &mut self.tilemap_bg); rt.clear_background(Color::BLACK); for pos in Pos::values() { let (x, y) = pos.xy(); let tile = floor.get(pos); let idx = match tile { Tile::Wall => ATLAS_WALL_SIDE, Tile::Air if (x + y) % 2 == 0 => ATLAS_FLOOR_FULL, Tile::Air if (x + y) % 2 == 1 => ATLAS_FLOOR_EMPTY, _ => ATLAS_ERROR, }; rt.draw_atlas(&self.textures.atlas, idx, x * size, y * size, size, 0.0); } } /// Draw dungeon tiles (foreground layer) fn draw_fg_tilemap(&mut self, r: &mut R) where R: RaylibDraw, { r.draw_tilemap(&self.tilemap_fg, self.tile_size, WALL_HEIGHT); } /// Draw dungeon tiles (background layer) fn draw_bg_tilemap(&mut self, r: &mut R) where R: RaylibDraw, { r.draw_tilemap(&self.tilemap_bg, self.tile_size, 0); } /// Draws the entities on the map fn draw_entities(&mut self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { self.draw_entity(r, &dungeon.player.entity); for enemy in &dungeon.enemies { self.draw_entity(r, &enemy.entity); } } /// Draws an entity fn draw_entity(&self, r: &mut R, entity: &Entity) where R: RaylibDraw, { let size = self.tile_size as f32; let x = entity.fpos.x(); let y = entity.fpos.y(); let tex = match entity.kind { EntityKind::Player => ATLAS_PLAYER, _ => ATLAS_ERROR, }; 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) -> u16 { self.tile_size } /// Returns the padding used between ui elements fn get_ui_padding(&self) -> u16 { self.get_ui_height() / 10 } /// Returns the font size for the UI fn get_ui_font_size(&self) -> u16 { self.get_ui_height() / 4 } /// Draws player HP, inventory, and floor number fn draw_ui(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { // Draw UI base rect r.draw_rectangle_ext(0, 0, self.width, self.get_ui_height(), Color::BLACK); // Draw core ui components 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 in game minimap fn draw_minimap(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { let padding = self.get_ui_padding(); let y = padding; // Draw MAP vert text let text_x = padding; self.draw_vertical_text(r, text_x, y, "MAP", 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; let group_size = (MAP_SIZE / size).max(1); let groups = MAP_SIZE / group_size; let dot_size = size / groups; let mut draw_dot = |x_idx: u16, y_idx: u16, color| { r.draw_rectangle_ext( minimap_x + x_idx * dot_size, padding + y_idx * dot_size, dot_size, dot_size, color, ); }; // 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(&self, r: &mut R, x: u16, y: u16, 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, u16); let byte_len = char.len_utf8(); let str = &text[byte_off..byte_off + byte_len]; let char_y = y + idx * spacing; r.draw_text_ext(str, x, char_y, font_size, color); byte_off += byte_len; } } /// Draws the player's inventory /// NOTE: Nothing is drawn if the inventory is empty fn draw_inventory(&self, r: &mut R, player: &Player) where R: RaylibDraw, { let slots = downcast!(PLAYER_INVENTORY_SIZE, u16); let padding = self.get_ui_padding(); let font_size = self.get_ui_font_size(); let ui_height = self.get_ui_height(); 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; let start_x = self.width / 2 - full_len / 2; let text_x = start_x; let slots_x = text_x + text_len; // Draw text self.draw_vertical_text(r, text_x, padding, "INV", Color::WHITE); // 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, u16); let size = ui_height - padding * 2; // Draw slot container r.draw_inv_atlas( &self.textures.atlas, ATLAS_INV_CONTAINER, slot_x, padding, size, 0.0, ); if let Some(item) = player.inventory.get(idx) { let tex = self.textures.item_texture(item); let item_padding = padding * 3; let dest_rec = rect! { slot_x + item_padding / 2, padding+ item_padding / 2, size - item_padding, 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(&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, heart_y, icon_width, 0.0, ); r.draw_text_ext( &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, damage_y, icon_width, 0.0, ); r.draw_text_ext( &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(&self, r: &mut R) where R: RaylibDraw, { self.draw_fps(r); } /// Draw FPS counter (debug) #[cfg(feature = "debug")] fn draw_fps(&self, r: &mut R) where R: RaylibDraw, { let fps_str = format!("{}", self.fps); r.draw_text_ext(&fps_str, 10, 10, 30, Color::YELLOW); } } trait Vector2Ext { fn min(self, other: Self) -> Self; fn max(self, other: Self) -> Self; } impl Vector2Ext for Vector2 { fn min(self, other: Self) -> Self { Self::new(self.x.min(other.x), self.y.min(other.y)) } fn max(self, other: Self) -> Self { Self::new(self.x.max(other.x), self.y.max(other.y)) } } 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, { /// Draw an atlas texture index fn draw_atlas( &mut self, tex: &Texture2D, (ax, ay): (u16, u16), x: impl Into, y: impl Into, size: impl Into, rotation: impl Into, ) { let size_into = size.into(); let source_rec = rect! { ax * BASE_TILE_SIZE, ay * BASE_TILE_SIZE, BASE_TILE_SIZE, BASE_TILE_SIZE, }; let dest_rec = rect! { x.into(), y.into(), size_into, size_into, }; let origin = vec2! { dest_rec.width / 2.0, dest_rec.height / 2.0, }; self.draw_texture_pro( tex, source_rec, dest_rec, origin, rotation.into(), Color::WHITE, ); } /// Draw in INV element from an atlas fn draw_inv_atlas( &mut self, tex: &Texture2D, (ax, ay): (u16, u16), x: impl Into, y: impl Into, size: impl Into, rotation: impl Into, ) { let size_into = size.into(); self.draw_atlas( tex, (ax, ay), x.into() + size_into / 2.0, y.into() + size_into / 2.0, size_into, rotation, ); } /// Draw dungeon tiles helper function fn draw_tilemap(&mut self, tex: &RenderTexture2D, size: u16, offset: u16) { let scale = size / BASE_TILE_SIZE; let width = downcast!(tex.width(), u16); let height = downcast!(tex.height(), u16); let source_rec = rect! { 0, 0, width, -height.cast_signed(), }; let dest_rec = rect! { 0, -(offset * scale).cast_signed(), width * scale, height * scale, }; let origin = Vector2::zero(); self.draw_texture_pro(tex, source_rec, dest_rec, origin, 0.0, Color::WHITE); } fn draw_text_ext( &mut self, text: &str, x: impl Into, y: impl Into, font_size: impl Into, color: Color, ) { self.draw_text(text, x.into(), y.into(), font_size.into(), color); } fn draw_rectangle_ext( &mut self, x: impl Into, y: impl Into, width: impl Into, height: impl Into, color: Color, ) { self.draw_rectangle(x.into(), y.into(), width.into(), height.into(), color); } } impl RaylibDrawExt for T where T: RaylibDraw {}