library

Online co-op book organization video game
git clone https://git.bradleygannon.com/library.git
Log | Files | Refs

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 }