//! 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, Sub}; use dungeon::{Dungeon, Entity, Floor, MAP_SIZE, Player, Pos, Tile}; use raylib::{ RaylibThread, color::Color, math::{Rectangle, 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); FrameRenderer { handle: draw_handle, thread, renderer: self, } } } /// 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, } 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 the current render width fn render_width(&self) -> i32 { self.handle.get_render_width() } /// Returns the current render height fn render_height(&self) -> i32 { self.handle.get_render_height() } /// Returns the wanted map tile size fn tile_size(&self) -> i32 { // the pixl size we will base everything on let size = self.render_width().max(self.render_height()); // min visible tiles let dist = VIEW_DISTANCE * 2 + 1; // get crude number of pixles // TODO: force scaling levels size.div(dist).max(16) } /// Clear the screen pub 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<()> { // Clear the background to black self.clear(); // Draw the dungeon self.draw_tiles(dungeon)?; self.draw_player(&dungeon.player); // Draw the ui self.draw_ui(dungeon); 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(); } /// Draw the player sprite pub fn draw_player(&mut self, player: &Player) { self.draw_entity(&player.entity); } /// Draws an entity #[expect(clippy::unused_self)] pub fn draw_entity(&mut self, _entity: &Entity) { // TODO: } /// 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 let size = self.tile_size() as f32; let dist = VIEW_DISTANCE as f32; let camera = Vector2::from(dungeon.camera().xy()); let source_rec = Rectangle::new( camera.x.sub(dist).max(0.0) * size, camera.y.sub(dist).max(0.0) * size, size * (dist * 2.0 + 1.0), size * (dist * 2.0 + 1.0), ); self.handle .draw_texture_rec(&tex, source_rec, Vector2::zero(), 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, } }