//! 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 = 10; use std::ops::Div; use dungeon::{Dungeon, Entity, Floor, MAP_SIZE, Pos, Tile}; use raylib::{ RaylibThread, camera::Camera2D, color::Color, ffi, math::Vector2, prelude::{RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibTextureModeExt}, texture::RenderTexture2D, }; use crate::assets::ImageData; /// The `Renderer` struct is the persistant renderer /// for the duration for the application. #[derive(Debug)] pub struct Renderer { /// Set of sprites to be drawn image: ImageData, /// Pre-rendered map texture that updates if the map changes /// Stores the hash of the map tiles to check this tiles: Option<(u64, RenderTexture2D)>, } impl Renderer { pub(crate) fn new(image: ImageData) -> Self { Self { image, tiles: None } } /// Invokes the renderer for the current frame pub(crate) fn invoke<'a>( &'a mut self, handle: &'a mut RaylibHandle, thread: &'a RaylibThread, ) -> FrameRenderer<'a> { let draw_handle = handle.begin_drawing(thread); // calculate the scaling factor let width = draw_handle.get_render_width(); let height = draw_handle.get_render_height(); let tile_size = { let size = width.max(height); let dist = VIEW_DISTANCE * 2 + 1; // TODO: force by 16 scaling levels size.div(dist).max(16) }; FrameRenderer { handle: draw_handle, thread, renderer: self, tile_size, } } } /// A `FrameRenderer` is a renderer for a single /// frame of the game. It is created per frame. pub struct FrameRenderer<'a> { /// The current draw handle for raylib handle: RaylibDrawHandle<'a>, /// The raylib thread thread: &'a RaylibThread, /// Mutable reference to the main renderer (stores persistant data) renderer: &'a mut Renderer, /// The tile size for this frame tile_size: i32, } impl<'a> FrameRenderer<'a> { /// Returns last computed fps fn fps(&self) -> u32 { self.handle.get_fps() } /// Returns image data #[expect(dead_code)] fn image(&self) -> &ImageData { &self.renderer.image } /// Returns a raylib camera for the given position #[must_use] fn camera(&self, dungeon: &Dungeon) -> Camera2D { let cpos = dungeon.camera(); let width = self.handle.get_render_width(); let height = self.handle.get_render_height(); Camera2D { target: Vector2::from(cpos.xy()).scale_by(self.tile_size as f32), offset: Vector2::new(width as f32 / 2.0, height as f32 / 2.0), rotation: 0.0, zoom: 1.0, } } /// Clear the screen fn clear(&mut self) { self.handle.clear_background(Color::BLACK); } /// Draws an entire frame /// /// # Examples /// ```no_run /// use dungeon::Dungeon; /// use graphics::Window; /// let mut window = Window::new(800, 600, "Dungeon Crawl").unwrap(); /// let mut renderer = window.renderer(); /// let dungeon = Dungeon::new(); /// renderer.draw_frame(&dungeon); /// ``` pub fn draw_frame(&mut self, dungeon: &Dungeon) -> crate::Result<()> { self.clear(); self.draw_dungeon(dungeon)?; self.draw_ui(dungeon); Ok(()) } /// Draws the dungeon, (tiles and entities) pub fn draw_dungeon(&mut self, dungeon: &Dungeon) -> crate::Result<()> { let camera = self.camera(dungeon); unsafe { ffi::BeginMode2D(camera.into()); } self.draw_tiles(dungeon)?; self.draw_entities(dungeon); unsafe { ffi::EndMode2D(); } Ok(()) } /// 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 pub fn draw_entities(&mut self, dungeon: &Dungeon) { self.draw_entity(&dungeon.player.entity); } /// Draws an entity #[expect(clippy::cast_possible_truncation)] pub fn draw_entity(&mut self, entity: &Entity) { let size = self.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 pub fn draw_tiles(&mut self, dungeon: &Dungeon) -> crate::Result<()> { let hash = dungeon.floor.hash(); let tex = match self.renderer.tiles.take() { Some((h, tex)) if hash == h => tex, _ => self.draw_tiles_to_tex(&dungeon.floor)?, }; // caculate the starting postion on the texture to draw from self.handle.draw_texture(&tex, 0, 0, Color::WHITE); // save the texture self.renderer.tiles.replace((hash, tex)); Ok(()) } /// Draw tiles on a provided texture fn draw_tiles_to_tex(&mut self, floor: &Floor) -> crate::Result { let size = self.tile_size; let pixels = (MAP_SIZE as i32) * size; let mut tex = self.handle .load_render_texture(self.thread, pixels as u32, pixels as u32)?; // draw the tiles to the texture { let mut handle = self.handle.begin_texture_mode(self.thread, &mut tex); handle.clear_background(Color::BLACK); 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); } } Ok(tex) } /// Draw FPS counter pub fn draw_fps(&mut self) { let fps_str = format!("{}", self.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, } }