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}, text::Font, 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_TEXTURE_SIZE: u16 = 16; /// The height of the wall (offset between tile layers) const WALL_HEIGHT: u16 = 7; /// The (prefered) view distance of the game const VIEW_DISTANCE: u16 = 5; /// The minimum width/height we will render to const MIN_RENDER_SIZE: u16 = BASE_TEXTURE_SIZE * (VIEW_DISTANCE + 2 + 2); // 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); // 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_TEXTURE_SIZE, BASE_TEXTURE_SIZE, }; #[derive(Debug)] struct Textures { // Tilemap atlas: Texture2D, // Entity player: Texture2D, // Misc error: Texture2D, // Fonts nes_font: Font, } impl Textures { fn new(handle: &mut RaylibHandle, thread: &RaylibThread) -> crate::Result { let atlas = handle.load_texture(thread, "assets/atlas.bmp")?; let player = handle.load_texture(thread, "assets/player.bmp")?; let error = handle.load_texture(thread, "assets/error.bmp")?; let nes_font = handle.load_font_ex( thread, "assets/nintendo-nes-font.otf", BASE_TEXTURE_SIZE.into(), None, )?; Ok(Self { atlas, player, error, nes_font, }) } 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, /// Tilemap texture used for the minimap tilemap_mm: 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_TEXTURE_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)?; let tilemap_mm = handle.load_render_texture(thread, MAP_SIZE as u32, MAP_SIZE as u32)?; Ok(Self { textures, tilemap_fg, tilemap_bg, tilemap_mm, 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).max(MIN_RENDER_SIZE); self.height = downcast!(handle.get_render_height(), u16).max(MIN_RENDER_SIZE); // 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_TEXTURE_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/2}, 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); self.update_mm_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_TEXTURE_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_TEXTURE_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); } } /// Draws the foregound tile map fn update_mm_tilemap(&mut self, r: &mut R, t: &RaylibThread, floor: &Floor) where R: RaylibDraw + RaylibTextureModeExt, { let mut rt = r.begin_texture_mode(t, &mut self.tilemap_mm); rt.clear_background(Color::DARKGRAY); for pos in Pos::values() { let (x, y) = pos.xy(); let tile = floor.get(pos); let color = match tile { Tile::Wall => Color::DARKGRAY, Tile::Air => Color::GRAY, Tile::Stairs => Color::WHITE, }; rt.draw_pixel(x.into(), y.into(), color); } } /// Draw dungeon tiles (foreground layer) fn draw_fg_tilemap(&mut self, r: &mut R) where R: RaylibDraw, { r.draw_tilemap( &self.tilemap_fg, 0, 0, self.tile_size / BASE_TEXTURE_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, 0, 0, self.tile_size / BASE_TEXTURE_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 (fx, fy) = entity.fpos.xy(); let texture = match entity.kind { EntityKind::Player => &self.textures.player, _ => &self.textures.error, }; let (x, y) = match entity.dir { Direction::North => (0, 0), Direction::South => (0, 1), Direction::East => (1, 0), Direction::West => (1, 1), }; let source_rec = rect! { x * BASE_TEXTURE_SIZE, y * BASE_TEXTURE_SIZE, BASE_TEXTURE_SIZE, BASE_TEXTURE_SIZE, }; let dest_rec = rect! { fx * size - size/2.0, fy * size - size/2.0, size, size, }; r.draw_texture_pro( texture, source_rec, dest_rec, Vector2::zero(), 0.0, Color::WHITE, ); } /// 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 minimap_y = padding; let size = self.get_ui_height() - padding * 2; r.draw_tilemap(&self.tilemap_mm, minimap_x, minimap_y, size / MAP_SIZE, 0); // Draw minimap entity's let dot_size = (size / MAP_SIZE).max(1); let mut draw_dot = |pos: Pos, color| { let (x, y) = pos.xy(); let offset_x = x * (size / MAP_SIZE); let offset_y = y * (size / MAP_SIZE); r.draw_rectangle_ext( minimap_x + offset_x, minimap_y + offset_y, dot_size, dot_size, color, ); }; // Draw enemy dots for enemy in &dungeon.enemies { draw_dot(enemy.entity.pos, Color::RED); } // Draw player dot draw_dot(dungeon.player.entity.pos, 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; self.draw_text(r, str, x, char_y, 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, ); self.draw_text( r, &format!("x{health:02}"), text_x, heart_y + padding, 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, ); self.draw_text( r, &format!("x{damage:02}"), text_x, damage_y + padding, 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(&fps_str, 10, 10, 30, Color::YELLOW); } /// Draw text helper function fn draw_text(&self, r: &mut R, text: &str, x: u16, y: u16, color: Color) where R: RaylibDraw, { let font = &self.textures.nes_font; let font_size = self.get_ui_font_size(); r.draw_text_ex(font, text, vec2! {x, y}, font_size.into(), 0.0, color); } } 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 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_TEXTURE_SIZE, ay * BASE_TEXTURE_SIZE, BASE_TEXTURE_SIZE, BASE_TEXTURE_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, x: u16, y: u16, scale: u16, offset: u16, ) { 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! { x, y.cast_signed() - (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_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 {}