use std::ops::Div; use dungeon::{ Direction, Dungeon, Entity, EntityKind, FPos, Floor, Item, MAP_SIZE, PLAYER_FULL_HEALTH, Player, Pos, Tile, }; use raylib::{ RaylibHandle, RaylibThread, camera::Camera2D, color::Color, math::{Rectangle, Vector2}, prelude::{RaylibDraw, RaylibMode2DExt, RaylibTextureModeExt}, texture::{RaylibTexture2D, RenderTexture2D, Texture2D}, }; /// The baseline size of all ingame sprites and tile textures const BASE_TILE_SIZE: i32 = 32; /// The height of the wall (offset between tile layers) const WALL_HEIGHT: i32 = 13; /// The (prefered) view distance of the game const VIEW_DISTANCE: i32 = 5; #[derive(Debug)] 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 { 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, }) } fn item_texture(&self, _item: &Item) -> &Texture2D { // TODO: make item textures &self.error } } // Tile atlas.bmp textures const ATLAS_WALL_SIDE: (i32, i32) = (0, 0); const ATLAS_FLOOR_FULL: (i32, i32) = (1, 0); const ATLAS_FLOOR_EMPTY: (i32, i32) = (2, 0); const ATLAS_WALL_TOP: (i32, i32) = (3, 0); const ATLAS_WALL_EDGE: (i32, i32) = (0, 1); // Entity atlas.bmp textures 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); // Misc atlas.bmp textures const ATLAS_ERROR: (i32, i32) = (3, 3); /// Full source rec for any texture const FULL_SOURCE_REC: Rectangle = Rectangle { x: 0.0, y: 0.0, width: BASE_TILE_SIZE as f32, height: BASE_TILE_SIZE as f32, }; #[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: i32, /// Last known FPS fps: u32, /// Render width of the current frame width: i32, /// Render height of the current frame height: i32, } 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 as u32 * 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 (in pixels) to draw each tile self.width = handle.get_render_width(); self.height = handle.get_render_height(); 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 << (i32::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; Camera2D { target: Vector2::from(cpos.xy()) .scale_by(self.tile_size as f32) .max(Vector2::new(width as f32 / 2.0, height as f32 / 2.0)) .min(Vector2::new( (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), 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 as f32; let mut rt = r.begin_texture_mode(t, &mut self.tilemap_fg); rt.clear_background(Color::BLANK); for pos in Pos::values() { let (fx, fy) = FPos::from(pos).xy(); let (xs, ys) = (fx * size, fy * 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 as f32; 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 as f32 * size, y as f32 * size, size, 0.0, ); } } /// Draw dungeon tiles (foreground layer) fn draw_fg_tilemap(&mut self, r: &mut R) where R: RaylibDraw, { let size = self.tile_size as f32; let offset = WALL_HEIGHT as f32; r.draw_tilemap(&self.tilemap_fg, size, offset); } /// Draw dungeon tiles (background layer) fn draw_bg_tilemap(&mut self, r: &mut R) where R: RaylibDraw, { let size = self.tile_size as f32; let offset = 0.0; r.draw_tilemap(&self.tilemap_bg, size, offset); } /// 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); } /// Draws player HP, inventory, and floor number fn draw_ui(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { // Draw core ui components self.draw_health(r, &dungeon.player); self.draw_inventory(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(&self, r: &mut R, player: &Player) 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 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; }; 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, ); x += size; } } /// 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 len = i32::try_from(player.inventory.len()).unwrap_or(0); // size of the inv blocks let size = self.tile_size.div(2).max(BASE_TILE_SIZE); let half_size = size as f32 / 2.0; // position of the inv blocks let y = self.height - size; let mut x = self.width / 2 - (size * len / 2); // size of font for number index let font_size = size / 3; let font_offset = font_size * 2; 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( &self.textures.atlas, ATLAS_INV_TOP_LAYER, x as f32 + half_size, y as f32 + half_size, size as f32, 0.0, ); r.draw_text( &format!("{}", idx + 1), x + font_offset + font_offset / 5, y + font_offset, font_size, Color::WHITE, ); x += size; } } /// Draws debug information ontop of other UI elements /// Called by default if `debug` featue is enabled fn draw_debug_ui(&self, r: &mut R) where R: RaylibDraw, { self.draw_fps(r); } /// Draw FPS counter (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); } } 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): (i32, i32), x: f32, y: f32, size: f32, rotation: f32, ) { let source_rec = Rectangle { x: (ax * BASE_TILE_SIZE) as f32, y: (ay * BASE_TILE_SIZE) as f32, width: BASE_TILE_SIZE as f32, height: BASE_TILE_SIZE as f32, }; let dest_rec = Rectangle { x, y, width: size, height: size, }; let origin = Vector2 { x: dest_rec.width / 2.0, y: dest_rec.height / 2.0, }; self.draw_texture_pro(tex, source_rec, dest_rec, origin, rotation, Color::WHITE); } /// Draw dungeon tiles helper function fn draw_tilemap(&mut self, tex: &RenderTexture2D, size: f32, offset: f32) { let scale = size / BASE_TILE_SIZE as f32; let width = tex.width() as f32; let height = tex.height() as f32; let source_rec = Rectangle { x: 0.0, y: 0.0, width, height: -height, }; let dest_rec = Rectangle { x: 0.0, y: -(offset * scale), width: width * scale, height: height * scale, }; let origin = Vector2::zero(); self.draw_texture_pro(tex, source_rec, dest_rec, origin, 0.0, Color::WHITE); } } impl RaylibDrawExt for T where T: RaylibDraw {}