//! The `render` module contains the structures for displaying //! the game, with each frame represented by a `Renderer` and //! frame specific information in `FrameInfo`. /// The (prefered) view distance of the game const VIEW_DISTANCE: i32 = 5; /// The baseline size of all ingame sprites and tile textures const BASE_TILE_SIZE: i32 = 16; use std::ops::Div; use dungeon::{Dungeon, Entity, FPos, Floor, MAP_SIZE, Pos, Tile}; use raylib::{ RaylibThread, camera::Camera2D, color::Color, math::Vector2, prelude::{ RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibMode2D, RaylibMode2DExt, RaylibTextureModeExt, }, texture::RenderTexture2D, }; use crate::assets::ImageData; /// The `FrameInfo` struct stores persistant information used thought a frame not /// accessable from `RaylibDraw` #[derive(Clone, Copy, Debug)] struct FrameInfo { /// The last calculated fps fps: u32, /// The render width of the framebuffer in pixels width: i32, /// The render height of the framebuffer in pixels height: i32, /// The tile size in pixels tile_size: i32, } impl FrameInfo { fn new(handle: &RaylibHandle) -> Self { let fps = handle.get_fps(); let width = handle.get_render_width(); let height = handle.get_render_height(); let tile_size = { let size = width.min(height); let dist = VIEW_DISTANCE * 2 + 1; let pixels = size.div(dist).max(BASE_TILE_SIZE); 1 << (i32::BITS - pixels.leading_zeros()) }; Self { fps, width, height, tile_size, } } } /// The `Renderer` struct is the persistant renderer /// for the duration for the application. #[derive(Debug)] pub struct Renderer { /// Set of sprites to be drawn #[expect(dead_code)] image: ImageData, /// Pre-rendered map texture that updates if the map changes /// Stores the hash of the map tiles to check this tiles_tex: RenderTexture2D, /// Hash of tiles used to draw on `tiles_tex` tiles_hash: u64, } impl Renderer { pub(crate) fn new( handle: &mut RaylibHandle, thread: &RaylibThread, image: ImageData, ) -> crate::Result { let tiles_tex = { let size = MAP_SIZE as u32 * BASE_TILE_SIZE as u32; handle.load_render_texture(thread, size, size)? }; Ok(Self { image, tiles_tex, tiles_hash: 0, }) } /// Invokes the renderer for the current frame pub(crate) fn invoke<'a>( &'a mut self, handle: &'a mut RaylibHandle, thread: &'a RaylibThread, ) -> FrameRendererImpl<'a> { let info = FrameInfo::new(handle); FrameRenderer { handle: handle.begin_drawing(thread), thread, info, renderer: self, } } } pub type FrameRendererImpl<'a> = FrameRenderer<'a, RaylibDrawHandle<'a>>; pub struct FrameRenderer<'a, T> where T: RaylibDraw, { /// The current draw handle for raylib handle: T, /// The raylib thread thread: &'a RaylibThread, /// Non drawing information for this current frame info: FrameInfo, /// Mutable reference to the main renderer (stores persistant data) renderer: &'a mut Renderer, } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibMode2DExt + RaylibTextureModeExt, { /// Draws an entire frame pub fn draw_frame(&mut self, dungeon: &Dungeon) { self.clear(); self.draw_dungeon(dungeon); self.draw_ui(dungeon); } /// Draws the dungeon, (tiles and entities) pub fn draw_dungeon(&mut self, dungeon: &Dungeon) { self.update_tilemap(&dungeon.floor); let camera = dungeon.camera(); let mut renderer = self.camera_renderer(camera); renderer.draw_tiles(); renderer.draw_entities(dungeon); } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibMode2DExt, { /// Returns a raylib camera for the given position #[must_use] fn camera_renderer<'b>( &'b mut self, cpos: FPos, ) -> FrameRenderer<'b, RaylibMode2D<'b, T>> { let width = self.info.width; let height = self.info.height; let camera = Camera2D { target: Vector2::from(cpos.xy()) .scale_by(self.info.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.info.tile_size) as f32 - (width as f32 / 2.0), (MAP_SIZE as i32 * self.info.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, }; let handle = self.handle.begin_mode2D(camera); FrameRenderer { handle, thread: self.thread, info: self.info, renderer: self.renderer, } } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibTextureModeExt, { /// Draw tiles on a provided texture fn update_tilemap(&mut self, floor: &Floor) { let hash = floor.hash(); if self.renderer.tiles_hash == hash { // Texture is up to date return; } self.renderer.tiles_hash = hash; let size = BASE_TILE_SIZE; let tex = &mut self.renderer.tiles_tex; let mut handle = self.handle.begin_texture_mode(self.thread, tex); for pos in Pos::values() { let (x, y) = pos.xy(); let color = tile_color(floor.get(pos)); handle.draw_rectangle(x as i32 * size, y as i32 * size, size, size, color); } } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw, { /// Clear the screen pub fn clear(&mut self) { self.handle.clear_background(Color::BLACK); } /// Draws player HP, inventory, and floor number pub fn draw_ui(&mut self, _dungeon: &Dungeon) { #[cfg(feature = "debug")] // Draw fps (debug only) self.draw_fps(); } /// Draws the entities on the map fn draw_entities(&mut self, dungeon: &Dungeon) { self.draw_entity(&dungeon.player.entity); } /// Draws an entity #[expect(clippy::cast_possible_truncation)] fn draw_entity(&mut self, entity: &Entity) { let size = self.info.tile_size; let x = (entity.fpos.x() * size as f32) as i32; let y = (entity.fpos.y() * size as f32) as i32; // TODO: per entity color self.handle.draw_rectangle(x, y, size, size, Color::GREEN); } /// Draw dungeon tiles fn draw_tiles(&mut self) { let tex = &self.renderer.tiles_tex; self.handle.draw_texture_ex( tex, Vector2::zero(), 0.0, (self.info.tile_size / BASE_TILE_SIZE) as f32, Color::WHITE, ); } /// Draw FPS counter fn draw_fps(&mut self) { let fps_str = format!("{}", self.info.fps); self.handle.draw_text(&fps_str, 10, 10, 30, Color::YELLOW); } } fn tile_color(tile: Tile) -> Color { // TODO: use textures instead of colors :) match tile { Tile::Wall => Color::BLUE, Tile::Air => Color::RED, Tile::Stairs => Color::GRAY, } } 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)) } }