render.rs (12535B)
1 use common::game::book::{BOOKS, BookId, Bookshelf, Slot}; 2 use lazy_static::lazy_static; 3 use macroquad::prelude::*; 4 5 use crate::{ClientState, input::get_bookshelf_mouse_address}; 6 7 lazy_static! { 8 pub(crate) static ref SPRITESHEET: Spritesheet = Spritesheet::load(); 9 } 10 11 pub const SCREEN_WIDTH_PX: f32 = 1280.; 12 pub const SCREEN_HEIGHT_PX: f32 = 720.; 13 14 #[derive(Clone, Copy, Debug, Default, PartialEq)] 15 pub(crate) struct Camera { 16 position: Vec2, 17 } 18 19 impl Camera { 20 pub(crate) fn position(&self) -> Vec2 { 21 self.position 22 } 23 } 24 25 #[derive(Debug)] 26 pub(crate) struct Spritesheet { 27 meeple: Sprite, 28 book: Sprite, 29 } 30 31 #[derive(Debug, Clone)] 32 pub(crate) struct Sprite { 33 texture: Texture2D, 34 size: Vec2, 35 } 36 37 impl Sprite { 38 fn render(&self, position: Vec2) { 39 draw_texture_ex( 40 &self.texture, 41 position.x, 42 position.y, 43 WHITE, 44 DrawTextureParams { 45 dest_size: Some(self.size), 46 ..Default::default() 47 }, 48 ); 49 } 50 } 51 52 impl From<Image> for Sprite { 53 fn from(value: Image) -> Self { 54 const TEXTURE_SCALE: f32 = 10.; 55 let texture = Texture2D::from_image(&value); 56 let size = Vec2::new( 57 texture.width() / TEXTURE_SCALE, 58 texture.height() / TEXTURE_SCALE, 59 ); 60 Sprite { texture, size } 61 } 62 } 63 64 impl Spritesheet { 65 pub(crate) fn load() -> Spritesheet { 66 Spritesheet { 67 meeple: Image::from_file_with_format( 68 include_bytes!("../assets/meeple.png"), 69 Some(ImageFormat::Png), 70 ) 71 .expect("Failed to load meeple.png") 72 .into(), 73 book: Image::from_file_with_format( 74 include_bytes!("../assets/book.png"), 75 Some(ImageFormat::Png), 76 ) 77 .expect("Failed to load book.png") 78 .into(), 79 } 80 } 81 } 82 83 impl ClientState { 84 pub(crate) fn draw_frame(&self) { 85 clear_background(MAGENTA); 86 87 if let Some(local_player_id) = self.local_player_id 88 && let Some(local_player) = self.interpolated_game.get_player(&local_player_id) 89 { 90 let mouse_slot = get_bookshelf_mouse_address(&self.camera, self.game.bookshelves()); 91 let mut mouse_book_popup = None; 92 // Render bookshelves 93 for (bookshelf_id, bookshelf) in self.interpolated_game.bookshelves() { 94 const BOOKSHELF_BACKGROUND: Color = BROWN; 95 const BOOKSHELF_FRAME: Color = DARKBROWN; 96 let bounds: Rect = bookshelf.rect().offset(-self.camera.position); 97 // Background 98 draw_rectangle(bounds.x, bounds.y, bounds.w, bounds.h, BOOKSHELF_BACKGROUND); 99 // Frame 100 draw_rectangle_lines( 101 bounds.x, 102 bounds.y, 103 bounds.w, 104 bounds.h, 105 // It seems that the thickness must be doubled to achieve the requested value 106 Bookshelf::FRAME_THICKNESS * 2., 107 BOOKSHELF_FRAME, 108 ); 109 // Shelves 110 for shelf_index in 0..bookshelf.shelf_count() { 111 let y = bounds.y + bounds.h 112 - shelf_index as f32 113 * (Bookshelf::BOOK_HEIGHT + Bookshelf::SHELF_THICKNESS) 114 - Bookshelf::FRAME_THICKNESS 115 - Bookshelf::SHELF_THICKNESS / 2.; 116 draw_line( 117 bounds.x, 118 y, 119 bounds.x + bounds.w, 120 y, 121 Bookshelf::SHELF_THICKNESS, 122 BOOKSHELF_FRAME, 123 ); 124 } 125 // Books 126 for (shelf_index, shelf) in bookshelf.iter().enumerate() { 127 let y = bounds.y + bounds.h 128 - shelf_index.saturating_add(1) as f32 129 * (Bookshelf::BOOK_HEIGHT + Bookshelf::SHELF_THICKNESS) 130 - Bookshelf::FRAME_THICKNESS; 131 for (slot_index, slot) in shelf.iter().enumerate() { 132 let x = bounds.x 133 + Bookshelf::FRAME_THICKNESS 134 + slot_index as f32 * Bookshelf::BOOK_WIDTH; 135 match slot { 136 Slot::Book { book_id } => { 137 draw_line(x, y, x, y + Bookshelf::BOOK_HEIGHT, 2., BLACK); 138 draw_rectangle( 139 x, 140 y, 141 Bookshelf::BOOK_WIDTH, 142 Bookshelf::BOOK_HEIGHT, 143 BLUE, 144 ); 145 draw_line( 146 x + Bookshelf::BOOK_WIDTH, 147 y, 148 x + Bookshelf::BOOK_WIDTH, 149 y + Bookshelf::BOOK_HEIGHT, 150 1., 151 BLACK, 152 ); 153 if let Some(ms) = mouse_slot 154 && ms.bookshelf_id() == *bookshelf_id 155 && ms.shelf_index() == shelf_index 156 && ms.position() == slot_index 157 { 158 draw_rectangle( 159 x, 160 y, 161 Bookshelf::BOOK_WIDTH, 162 Bookshelf::BOOK_HEIGHT, 163 GOLD.with_alpha(0.5), 164 ); 165 mouse_book_popup = Some((Vec2::new(x, y), book_id)); 166 } 167 } 168 Slot::Empty => { 169 if let Some(ms) = mouse_slot 170 && ms.bookshelf_id() == *bookshelf_id 171 && ms.shelf_index() == shelf_index 172 && ms.position() == slot_index 173 { 174 draw_rectangle( 175 x, 176 y, 177 Bookshelf::BOOK_WIDTH, 178 Bookshelf::BOOK_HEIGHT, 179 GOLD.with_alpha(0.5), 180 ); 181 } 182 } 183 } 184 } 185 } 186 } 187 if let Some((position, book_id)) = mouse_book_popup { 188 self.draw_book_metadata_popup(position, book_id); 189 } 190 191 // Render books on the floor 192 let floor_book_sprite = SPRITESHEET.book.clone(); 193 for floor_book in self.interpolated_game.floor_books().iter() { 194 let position_camera = floor_book.position() - self.camera.position; 195 floor_book_sprite.render(position_camera); 196 } 197 198 // Render players 199 let player_sprite = SPRITESHEET.meeple.clone(); 200 for player in self.interpolated_game.players().values() { 201 let position_camera = player.position() - self.camera.position; 202 player_sprite.render(position_camera); 203 let name_plate_dimensions = measure_text(player.name().clone(), None, 16, 1.); 204 draw_text( 205 player.name().clone(), 206 position_camera.x + player_sprite.size.x / 2. 207 - name_plate_dimensions.width / 2., 208 position_camera.y + player_sprite.size.y + 16., 209 16., 210 BLACK, 211 ); 212 } 213 214 // Render metadata popup for a floor book if the user mouses over it 215 if let Some(moused_floor_book) = 216 self.interpolated_game.floor_books().iter().find(|fb| { 217 fb.collider() 218 .rect() 219 .offset(fb.position()) 220 .offset(-self.camera.position) 221 .contains({ 222 let (x, y) = mouse_position(); 223 Vec2::new(x, y) 224 }) 225 }) 226 { 227 let popup_position = Vec2::new( 228 moused_floor_book.position().x - self.camera.position.x 229 + SPRITESHEET.book.size.x * 0.4, 230 moused_floor_book.position().y - self.camera.position.y - 5., 231 ); 232 self.draw_book_metadata_popup(popup_position, &moused_floor_book.id()); 233 } 234 235 // Render list of books held by the local player 236 for (index, book) in local_player.books().iter().enumerate() { 237 let book_string = if let Some(metadata) = BOOKS.get(book) { 238 metadata.to_string() 239 } else { 240 "Unknown book".to_string() 241 }; 242 let y = screen_height() - ((local_player.books().len() - index) as f32 * 16.) - 16.; 243 draw_text(book_string, 16., y, 16., BLACK); 244 if index == self.selected_inventory_book { 245 draw_circle(8., y - 4., 3., BLACK); 246 } 247 } 248 249 // Render debug view if enabled 250 if self.debug_view { 251 draw_rectangle(0., 0., 284., 56., BLACK); 252 draw_text("Library Tech Demo 2026-09-14", 4., 16., 16., WHITE); 253 draw_text(format!("fps {}", get_fps()), 4., 32., 16., WHITE); 254 draw_text(format!("id {}", local_player_id), 4., 48., 16., WHITE); 255 draw_text( 256 format!( 257 "pos ({:.1}, {:.1})", 258 local_player.position().x, 259 local_player.position().y 260 ), 261 60., 262 32., 263 16., 264 WHITE, 265 ); 266 } 267 } 268 } 269 pub(crate) fn follow_local_player(&mut self) { 270 let screen_center = Vec2::new(screen_width() / 2., screen_height() / 2.); 271 if let Some(local_player_id) = self.local_player_id 272 && let Some(local_player) = self.game.get_player(&local_player_id) 273 { 274 let player_sprite = &SPRITESHEET.meeple; 275 self.camera.position = 276 local_player.position() - screen_center + player_sprite.size / 2.; 277 } 278 } 279 fn draw_book_metadata_popup(&self, position: Vec2, book_id: &BookId) { 280 let text = if let Some(metadata) = BOOKS.get(book_id) { 281 metadata.to_string() 282 } else { 283 "Unknown book".to_string() 284 }; 285 let font_size = 24.; 286 let text_dimensions = measure_text(&text, None, font_size as u16, 1.); 287 let padding = 4.; 288 let Vec2 { x, y } = position; 289 let triangle_width = Bookshelf::BOOK_WIDTH * 2.; 290 let triangle_height = 10.; 291 let text_x = x - text_dimensions.width / 2.; 292 let text_y = 293 y - text_dimensions.height + text_dimensions.offset_y - triangle_height - padding; 294 let w = text_dimensions.width; 295 let h = text_dimensions.height; 296 draw_rectangle( 297 text_x - padding, 298 text_y - text_dimensions.offset_y - padding, 299 w + padding * 2., 300 h + padding * 2., 301 WHITE.with_alpha(0.5), 302 ); 303 draw_text(text, text_x, text_y, font_size, BLACK.with_alpha(0.5)); 304 draw_triangle( 305 Vec2::new( 306 x - triangle_width / 2. + Bookshelf::BOOK_WIDTH / 2., 307 y - triangle_height, 308 ), 309 Vec2::new(x + Bookshelf::BOOK_WIDTH / 2., y), 310 Vec2::new( 311 x + triangle_width / 2. + Bookshelf::BOOK_WIDTH / 2., 312 y - triangle_height, 313 ), 314 WHITE.with_alpha(0.5), 315 ); 316 } 317 }