use std::{ f32, hash::{DefaultHasher, Hash, Hasher}, io::Write, time::Duration, }; use dungeon::{ Dungeon, entity::{Entity, EntityKind, Item, PLAYER_INVENTORY_SIZE, Player}, map::{Floor, MAP_SIZE, Tile}, pos::{Direction, Pos}, }; use raylib::{ RaylibHandle, RaylibThread, camera::Camera2D, color::Color, math::{Rectangle, Vector2}, prelude::{RaylibDraw, RaylibMode2DExt, RaylibScissorModeExt, RaylibTextureModeExt}, texture::{RaylibTexture2D, RenderTexture2D, Texture2D}, }; use crate::timer::Timer; 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, } }; } macro_rules! draw_text { ($self:ident, $r:expr, $x:expr, $y:expr, $($arg:tt)*) => {{ let mut buffer = [0u8; MAX_TEXT_LEN]; let _ = writeln!(&mut buffer[..], $($arg)*); $self.draw_text($r, &buffer, $x, $y); }}; } macro_rules! load_texture { ($handle:expr, $thread:expr, $filepath:expr) => { if cfg!(any(feature = "static", target_arch = "wasm32")) { let bytes = include_bytes!(concat!("../../", $filepath)); let image = ::raylib::texture::Image::load_image_from_mem(".bmp", bytes)?; $handle.load_texture_from_image($thread, &image)? } else { $handle.load_texture($thread, $filepath)? } }; } /// The baseline size of all ingame sprites and tile textures const 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 = 9; /// The size of padding in the UI const UI_PADDING: u16 = TEXTURE_SIZE / 2; /// The size of the font const FONT_SIZE: u16 = TEXTURE_SIZE; /// The height of the UI const UI_HEIGHT: u16 = (UI_PADDING + FONT_SIZE) * 3; /// The size of a tile drawn to the screen const TILE_SIZE: u16 = TEXTURE_SIZE * 2; /// The render height of the screen pub const RENDER_HEIGHT: u16 = UI_HEIGHT + (TILE_SIZE * VIEW_DISTANCE); /// The render width of the screen pub const RENDER_WIDTH: u16 = RENDER_HEIGHT * 4 / 3; // Tile atlas.bmp textures const ATLAS_WALL_SIDE: (u16, u16) = (0, 0); const ATLAS_FLOOR: (u16, u16) = (1, 0); const ATLAS_STAIR: (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); // Floor ext accent textures const ATLAS_FLOOR_EXT1: (u16, u16) = (0, 2); const ATLAS_FLOOR_EXT2: (u16, u16) = (1, 2); const ATLAS_FLOOR_EXT3: (u16, u16) = (2, 2); const ATLAS_FLOOR_EXT4: (u16, u16) = (3, 2); // 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, TEXTURE_SIZE, TEXTURE_SIZE, }; /* Precomputed UI text rows */ const UI_ROW1: u16 = UI_PADDING; const UI_ROW2: u16 = UI_ROW1 + FONT_SIZE + UI_PADDING; const UI_ROW3: u16 = UI_ROW2 + FONT_SIZE + UI_PADDING; /* Precomputed UI text columns */ const UI_COL1: u16 = UI_PADDING; const UI_COL2: u16 = UI_COL1 + FONT_SIZE * 10; /// The maxmimum length any text string can be const MAX_TEXT_LEN: usize = 16; struct Textures { // Tilemap atlas: Texture2D, // Entity player: Texture2D, zombie: Texture2D, // Misc error: Texture2D, // Fonts font: Texture2D, } impl Textures { fn new(handle: &mut RaylibHandle, thread: &RaylibThread) -> crate::Result { let atlas = load_texture!(handle, thread, "assets/atlas.bmp"); let player = load_texture!(handle, thread, "assets/player.bmp"); let zombie = load_texture!(handle, thread, "assets/zombie.bmp"); let error = load_texture!(handle, thread, "assets/error.bmp"); let font = load_texture!(handle, thread, "assets/font.bmp"); Ok(Self { atlas, player, zombie, error, font, }) } const fn item_texture(&self, _item: Item) -> &Texture2D { // TODO: make item textures &self.error } } 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, /// Framebuffer to render the whole (unscaled) game to framebuffer: Option, /// Show debug UI debug: bool, /// Frame timer timer: Timer, } 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 * 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)?; let framebuffer = handle.load_render_texture( thread, RENDER_WIDTH as u32, RENDER_HEIGHT as u32, )?; Ok(Self { textures, tilemap_fg, tilemap_bg, tilemap_mm, tiles_hash: None, framebuffer: Some(framebuffer), debug: false, timer: Timer::new(), }) } pub const fn toggle_debug(&mut self) { self.debug = !self.debug; } pub const fn delta_time(&self) -> Duration { self.timer.delta_time() } pub fn draw_frame( &mut self, handle: &mut RaylibHandle, t: &RaylibThread, dungeon: &Dungeon, ) { let render_width = handle.get_render_width() as f32; let render_height = handle.get_render_height() as f32; // Start the frame let mut r = handle.begin_drawing(t); // Update render info before drawing self.timer.update(); // Update cached tilemaps self.update_tilemaps(&mut r, t, &dungeon.floor); // Draw the frame to the framebuffer if let Some(mut fb) = self.framebuffer.take() { // Draw the dungeon and UI { let mut rt = r.begin_texture_mode(t, &mut fb); rt.clear_background(Color::BLACK); self.draw_dungeon(&mut rt, dungeon); self.draw_ui(&mut rt, dungeon); } // Draw and scale the fb to the screen r.clear_background(Color::BLACK); let source_rec = rect! { 0, 0, RENDER_WIDTH, -RENDER_HEIGHT.cast_signed(), }; let scale = (render_width / RENDER_WIDTH as f32) .min(render_height / RENDER_HEIGHT as f32); let dest_rec = rect! { (render_width - (RENDER_WIDTH as f32 * scale))/2.0, (render_height - (RENDER_HEIGHT as f32 * scale))/2.0, RENDER_WIDTH as f32 * scale, RENDER_HEIGHT as f32 * scale, }; r.draw_texture_pro(&fb, source_rec, dest_rec, Vector2::ZERO, 0.0, Color::WHITE); // Restore the fb self.framebuffer = Some(fb); } } /// Draws the game dungeon fn draw_dungeon(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw + RaylibMode2DExt, { let camera = dungeon.render_camera(); let mut rc = r.begin_mode2D(camera); self.draw_bg_tilemap(&mut rc); if self.debug { rc.draw_pathing_deug(dungeon); } 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 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() * TEXTURE_SIZE, pos.y() * TEXTURE_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, TEXTURE_SIZE, 0.0, Color::WHITE, ); // 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, TEXTURE_SIZE, 0.0, Color::WHITE, ); } if !is_wall(Direction::East) { rt.draw_atlas( &self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, TEXTURE_SIZE, 90.0, Color::WHITE, ); } if !is_wall(Direction::South) { rt.draw_atlas( &self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, TEXTURE_SIZE, 180.0, Color::WHITE, ); } if !is_wall(Direction::West) { rt.draw_atlas( &self.textures.atlas, ATLAS_WALL_EDGE, xs, ys, TEXTURE_SIZE, 270.0, Color::WHITE, ); } } } /// Draws the foregound tile map fn update_bg_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_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 if y + 1 == MAP_SIZE => ATLAS_WALL_TOP, Tile::Wall => ATLAS_WALL_SIDE, Tile::Room | Tile::Hallway => ATLAS_FLOOR, Tile::Stairs => ATLAS_STAIR, //_ => ATLAS_ERROR, }; rt.draw_atlas( &self.textures.atlas, idx, x * TEXTURE_SIZE, y * TEXTURE_SIZE, TEXTURE_SIZE, 0.0, Color::WHITE, ); if idx == ATLAS_FLOOR { // add possible extentions let mut hasher = DefaultHasher::new(); pos.hash(&mut hasher); let idx_ext = match hasher.finish() % 20 { 0 => Some(ATLAS_FLOOR_EXT1), 5 => Some(ATLAS_FLOOR_EXT2), 10 => Some(ATLAS_FLOOR_EXT3), 15 => Some(ATLAS_FLOOR_EXT4), _ => None, }; if let Some(idx) = idx_ext { rt.draw_atlas( &self.textures.atlas, idx, x * TEXTURE_SIZE, y * TEXTURE_SIZE, TEXTURE_SIZE, 0.0, Color::WHITE, ); } } } } /// 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::Room => Color::GRAY, Tile::Hallway => Color::GRAY, Tile::Stairs => Color::WHITE, }; rt.draw_pixel(x.into(), y.into(), color); } } /// Draw dungeon tiles (foreground layer) fn draw_fg_tilemap(&self, r: &mut R) where R: RaylibDraw, { r.draw_tilemap( &self.tilemap_fg, 0u16, 0u16, TILE_SIZE / TEXTURE_SIZE, WALL_HEIGHT, ); } /// Draw dungeon tiles (background layer) fn draw_bg_tilemap(&self, r: &mut R) where R: RaylibDraw, { r.draw_tilemap(&self.tilemap_bg, 0u16, 0u16, TILE_SIZE / TEXTURE_SIZE, 0); } /// Draws the entities on the map fn draw_entities(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { for enemy in &dungeon.entities { self.draw_entity(r, enemy); } self.draw_entity(r, &dungeon.player.entity); } /// Draws an entity fn draw_entity(&self, r: &mut R, entity: &Entity) where R: RaylibDraw, { let size = TILE_SIZE as f32; let (fx, fy) = entity.fpos.xy(); let texture = match entity.kind { EntityKind::Player => &self.textures.player, EntityKind::Zombie(_) => &self.textures.zombie, _ => &self.textures.error, }; let (mut ax, ay) = match entity.dir { Direction::North => (0, 0), Direction::South => (0, 1), Direction::East => (1, 0), Direction::West => (1, 1), }; if (self.timer.since_start().as_millis() / 250).is_multiple_of(2) { // entity animtion frame every 250 millis ax += 2; } let dest_rec = rect! { fx * size, fy * size, size, size, }; r.draw_atlas( texture, (ax, ay), dest_rec.x, dest_rec.y - WALL_HEIGHT as f32, size, 0.0, Color::WHITE, ); if self.debug { r.draw_rectangle_lines_ex(dest_rec, 1.0, Color::YELLOW); } } /// 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, RENDER_WIDTH, UI_HEIGHT, Color::BLACK); if self.debug { self.draw_debug_ui(r, dungeon); } else { // Draw core ui components self.draw_minimap(r, dungeon); self.draw_inventory(r, &dungeon.player); self.draw_stats(r, &dungeon.player); } if let Some(buf) = dungeon.msg.current() { self.draw_msg(r, buf); } } /// Draw in game minimap fn draw_minimap(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw + RaylibScissorModeExt, { const Y: u16 = UI_PADDING; // Draw MAP vert text const TEXT_X: u16 = UI_PADDING; self.draw_text_vertical(r, b"MAP", TEXT_X, Y); // Base coords for the minimap const MINIMAP_X: u16 = TEXT_X + FONT_SIZE + UI_PADDING; const MAX_MINIMAP_SIZE: u16 = FONT_SIZE * 3; const EXTRA_PIXELS: u16 = MAP_SIZE.saturating_sub(MAX_MINIMAP_SIZE) + 1; // Get the real coords of the minimap (offset) let offset_x = (dungeon.player.entity.pos.x() / (MAP_SIZE / EXTRA_PIXELS)) .saturating_sub(MAX_MINIMAP_SIZE / 2); let offset_y = (dungeon.player.entity.pos.y() / (MAP_SIZE / EXTRA_PIXELS)) .saturating_sub(MAX_MINIMAP_SIZE / 2); let minimap_x = MINIMAP_X.cast_signed() - offset_x.cast_signed(); let minimap_y = Y.cast_signed() - offset_y.cast_signed(); // Draw in scissor mode incase the map size is too big { let mut rs = r.begin_scissor_mode( MINIMAP_X.into(), Y.into(), MAX_MINIMAP_SIZE.into(), MAX_MINIMAP_SIZE.into(), ); rs.draw_tilemap(&self.tilemap_mm, minimap_x, minimap_y, 1, 0); // Draw minimap entity's let mut draw_dot = |pos: Pos, color| { let (x, y) = pos.xy(); rs.draw_pixel( (minimap_x + x.cast_signed()).into(), (minimap_y + y.cast_signed()).into(), color, ); }; // Draw entity dots for entity in &dungeon.entities { use EntityKind as K; let color = match entity.kind { K::Player => Color::LIME, K::Zombie(_) => Color::RED, K::Item(_) => Color::TURQUOISE, }; draw_dot(entity.pos, color); } // Draw player dot draw_dot(dungeon.player.entity.pos, Color::LIME); } } /// 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, { const TEXT_LEN: u16 = FONT_SIZE + UI_PADDING; const SLOT_LEN: u16 = UI_HEIGHT - UI_PADDING * 2; const SLOTS_LEN: u16 = SLOT_LEN * PLAYER_INVENTORY_SIZE; const FULL_LEN: u16 = TEXT_LEN + SLOTS_LEN; const TEXT_X: u16 = RENDER_WIDTH / 2 - FULL_LEN / 2; const SLOTS_X: u16 = TEXT_X + TEXT_LEN; // Draw text self.draw_text_vertical(r, b"INV", TEXT_X, UI_PADDING); // 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 * idx; // Draw slot container let tint = if (idx as usize) == player.active_inv_slot { Color::YELLOW } else { Color::WHITE }; r.draw_atlas( &self.textures.atlas, ATLAS_INV_CONTAINER, slot_x, UI_PADDING, SLOT_LEN, 0.0, tint, ); if let Some(item) = player.inventory.get(idx as usize) { let tex = self.textures.item_texture(*item); const ITEM_PADDDING: u16 = UI_PADDING * 3; let dest_rec = rect! { slot_x + ITEM_PADDDING/2, UI_PADDING + ITEM_PADDDING/2, SLOT_LEN - ITEM_PADDDING, SLOT_LEN - ITEM_PADDDING, }; 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; let damage = player.weapon.attack_dmg(); const TEXT_WIDTH: u16 = FONT_SIZE * 3 + UI_PADDING; const ICON_WIDTH: u16 = FONT_SIZE; const ICON_X: u16 = RENDER_WIDTH - TEXT_WIDTH - ICON_WIDTH; const TEXT_X: u16 = RENDER_WIDTH - TEXT_WIDTH; // draw health const HEART_Y: u16 = UI_PADDING * 2; r.draw_atlas( &self.textures.atlas, ATLAS_HEART_ICON, ICON_X, HEART_Y, ICON_WIDTH, 0.0, Color::WHITE, ); draw_text!(self, r, TEXT_X, HEART_Y, "x{health:02}"); // draw damage const DAMAGE_Y: u16 = HEART_Y + FONT_SIZE + UI_PADDING; r.draw_atlas( &self.textures.atlas, ATLAS_DAMAGE_ICON, ICON_X, DAMAGE_Y, ICON_WIDTH, 0.0, Color::WHITE, ); draw_text!(self, r, TEXT_X, DAMAGE_Y, "x{damage:02}"); } /// Draws debug information ontop of other UI elements fn draw_debug_ui(&self, r: &mut R, dungeon: &Dungeon) where R: RaylibDraw, { let player = &dungeon.player; // Draw FPS draw_text!(self, r, UI_COL1, UI_ROW1, "FPS {}", self.timer.fps()); // Draw Player position let (x, y) = &player.entity.pos.xy(); draw_text!(self, r, UI_COL1, UI_ROW2, "POS {x:02},{y:02}"); // Draw Player direction let dir = &player.entity.dir; draw_text!(self, r, UI_COL1, UI_ROW3, "DIR {dir}"); // Draw Player Seed let seed = &dungeon.seed(); draw_text!(self, r, UI_COL2, UI_ROW1, "{seed:016X}"); // Draw Dungeon Hash let hash = dungeon.floor.hash(); draw_text!(self, r, UI_COL2, UI_ROW2, "{hash:016X}"); // Draw current frame number let frame = self.timer.frame(); draw_text!(self, r, UI_COL2, UI_ROW3, "FRAME {frame}"); } /// Draw msg box fn draw_msg(&self, r: &mut R, msg: &[u8]) where R: RaylibDraw, { const MAX_LINES: u16 = 5; const MSG_LEN: u16 = 20; const HEIGHT: u16 = MAX_LINES * FONT_SIZE + UI_PADDING * 2; const WIDTH: u16 = MSG_LEN * FONT_SIZE + UI_PADDING * 2; const X: u16 = RENDER_WIDTH / 2 - WIDTH / 2; const Y: u16 = RENDER_HEIGHT - HEIGHT - UI_PADDING; // draw background r.draw_rectangle( X.into(), Y.into(), WIDTH.into(), HEIGHT.into(), Color::BLACK, ); r.draw_rectangle_lines( X.into(), Y.into(), WIDTH.into(), HEIGHT.into(), Color::WHITE, ); let mut x = 0; let mut y = 0; let words = msg.split(u8::is_ascii_whitespace); for word in words { let left = MSG_LEN.saturating_sub(x); if word.len() > left as usize { y += 1; x = 0; } for char in word { self.draw_char( r, *char, X + UI_PADDING + x * FONT_SIZE, Y + UI_PADDING + y * FONT_SIZE, ); x += 1; } x += 1; } } /// Draws vertical text fn draw_text_vertical(&self, r: &mut R, text: &[u8], x: u16, y_start: u16) where R: RaylibDraw, { const SPACING: u16 = FONT_SIZE + UI_PADDING / 2; for (idx, char) in text.iter().enumerate() { if *char == b'\n' { break; } let y = y_start + downcast!(idx, u16) * SPACING; self.draw_char(r, *char, x, y); } } /// Draw text helper function fn draw_text(&self, r: &mut R, text: &[u8], x_start: u16, y: u16) where R: RaylibDraw, { for (idx, char) in text.iter().enumerate() { if *char == b'\n' { break; } let x = x_start + downcast!(idx, u16) * FONT_SIZE; self.draw_char(r, *char, x, y); } } /// Draws a char to the screen fn draw_char(&self, r: &mut R, char: u8, x: u16, y: u16) where R: RaylibDraw, { if !(32..=127).contains(&char) { return; } let ax = char % 16; let ay = (char - 32) / 16; r.draw_atlas( &self.textures.font, (ax.into(), ay.into()), x, y, FONT_SIZE, 0.0, Color::WHITE, ); } } trait DungeonExt { fn render_camera(&self) -> Camera2D; } impl DungeonExt for Dungeon { /// Returns the Raylib Camera setup with needed 2D position/transforms fn render_camera(&self) -> Camera2D { /// The offset to apply const OFFSET: Vector2 = vec2! { RENDER_WIDTH/2 - TILE_SIZE/2, RENDER_HEIGHT/2 - TILE_SIZE/2 + UI_HEIGHT/2, }; /// The minimum position the camera is allowed to go const CAMERA_MIN: Vector2 = vec2! { RENDER_WIDTH/2 - TILE_SIZE/2, RENDER_HEIGHT/2 - TILE_SIZE/2 - UI_HEIGHT/2, }; /// The maximum position the camera is allowed to go const CAMERA_MAX: Vector2 = vec2! { MAP_SIZE * TILE_SIZE - RENDER_WIDTH/2 - TILE_SIZE/2, MAP_SIZE * TILE_SIZE - RENDER_HEIGHT/2 - TILE_SIZE/2 + UI_HEIGHT/2, }; let pos = self.camera(); Camera2D { target: Vector2::from(pos.xy()) .scale_by(TILE_SIZE.into()) .max(CAMERA_MIN) .min(CAMERA_MAX), offset: OFFSET, rotation: 0.0, zoom: 1.0, } } } trait Vector2Ext { fn scale_by(self, amt: f32) -> Self; } impl Vector2Ext for Vector2 { fn scale_by(self, amt: f32) -> Self { Self { x: self.x * amt, y: self.y * amt, } } } trait RaylibDrawExt where Self: RaylibDraw, { /// Draw an atlas texture index #[inline] #[expect(clippy::too_many_arguments)] fn draw_atlas( &mut self, tex: &Texture2D, (ax, ay): (u16, u16), x: impl Into, y: impl Into, size: impl Into, rotation: impl Into, tint: Color, ) { let size_into = size.into(); let source_rec = rect! { ax * TEXTURE_SIZE, ay * TEXTURE_SIZE, TEXTURE_SIZE, TEXTURE_SIZE, }; let dest_rec = rect! { x.into() + size_into/2.0, y.into() + size_into/2.0, 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(), tint); } /// Draw dungeon tiles helper function fn draw_tilemap( &mut self, tex: &RenderTexture2D, x: impl Into, y: impl Into, 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.into(), y.into() - (offset * scale) as i32, 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); } fn draw_pathing_deug(&mut self, dungeon: &Dungeon) { for enemy in &dungeon.entities { let Some(ai) = enemy.get_ai() else { continue; }; let Some(moves) = ai.moves() else { continue; }; for pos in moves { let (x, y) = pos.xy(); let color = Color::RED.alpha(0.5); self.draw_rectangle_ext( x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE, color, ); } } } } impl RaylibDrawExt for T where T: RaylibDraw {}