library

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

commit 6f3c9c80e418f35e28e1e64b73ae515131c8854b
parent 2e3361408c34945f3a8b0a3be9854f7716f87d52
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Mon, 14 Sep 2026 21:44:09 -0400

Draw metadata popups on top and limit player action radius

Diffstat:
Mclient/src/input.rs | 38++++++++++++++++++++++++++------------
Mclient/src/render.rs | 81++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mclient/src/run.rs | 10+++++++---
Mcommon/src/game/state.rs | 16++++++++--------
4 files changed, 84 insertions(+), 61 deletions(-)

diff --git a/client/src/input.rs b/client/src/input.rs @@ -36,23 +36,26 @@ pub(crate) fn handle_movement_input(local_player: &Player, dt: &Duration) -> Opt pub(crate) fn handle_pick_up_book_input( local_player: &Player, + camera: &Camera, floor_books: &[FloorBook], ) -> Option<ActionKind> { - if is_key_pressed(KeyCode::Space) { - let local_player_collider = local_player - .collider() - .rect() - .offset(local_player.position()); - if let Some(floor_book) = floor_books.iter().find(|fb| { + if is_mouse_button_pressed(MouseButton::Left) + && let Some(moused_floor_book) = floor_books.iter().find(|fb| { fb.collider() .rect() .offset(fb.position()) - .overlaps(&local_player_collider) - }) { - return Some(ActionKind::PickUpBook { - book_id: floor_book.id(), - }); - } + .offset(-camera.position()) + .contains({ + let (x, y) = mouse_position(); + Vec2::new(x, y) + }) + && fb.position().distance(local_player.position()) + <= GameState::MAX_PLAYER_ACTION_RADIUS + }) + { + return Some(ActionKind::PickUpBook { + book_id: moused_floor_book.id(), + }); } None } @@ -103,6 +106,11 @@ pub(crate) fn handle_put_book_on_shelf_input( && let Some(book_id) = local_player.books().get(selected_book_index) && let Some(shelf) = bookshelves.get(&slot_address.bookshelf_id()) && let Some(Slot::Empty) = shelf.get_slot(&slot_address) + // TODO Base this on the position of the slot, not the click + && local_player.position().distance({ + let (x, y) = mouse_position(); + Vec2::new(x, y) + camera.position() + }) <= GameState::MAX_PLAYER_ACTION_RADIUS { return Some(ActionKind::PutBookOnShelf { book_id: *book_id, @@ -115,11 +123,17 @@ pub(crate) fn handle_put_book_on_shelf_input( pub(crate) fn handle_take_book_off_shelf_input( bookshelves: &HashMap<BookshelfId, Bookshelf>, camera: &Camera, + local_player: &Player, ) -> Option<ActionKind> { if is_mouse_button_pressed(MouseButton::Left) && let Some(slot_address) = get_bookshelf_mouse_address(camera, bookshelves) && let Some(shelf) = bookshelves.get(&slot_address.bookshelf_id()) && let Some(Slot::Book { .. }) = shelf.get_slot(&slot_address) + // TODO Base this on the position of the slot, not the click + && local_player.position().distance({ + let (x, y) = mouse_position(); + Vec2::new(x, y) + camera.position() + }) <= GameState::MAX_PLAYER_ACTION_RADIUS { return Some(ActionKind::TakeBookOffShelf { slot_address }); } diff --git a/client/src/render.rs b/client/src/render.rs @@ -88,6 +88,7 @@ impl ClientState { && let Some(local_player) = self.interpolated_game.get_player(&local_player_id) { let mouse_slot = get_bookshelf_mouse_address(&self.camera, self.game.bookshelves()); + let mut mouse_book_popup = None; // Render bookshelves for (bookshelf_id, bookshelf) in self.interpolated_game.bookshelves() { const BOOKSHELF_BACKGROUND: Color = BROWN; @@ -161,13 +162,7 @@ impl ClientState { Bookshelf::BOOK_HEIGHT, GOLD.with_alpha(0.5), ); - self.draw_book_metadata_popup( - Vec2::new( - x - Bookshelf::BOOK_WIDTH / 2. + self.camera.position.x, - y + self.camera.position.y, - ), - book_id, - ); + mouse_book_popup = Some((Vec2::new(x, y), book_id)); } } Slot::Empty => { @@ -189,6 +184,9 @@ impl ClientState { } } } + if let Some((position, book_id)) = mouse_book_popup { + self.draw_book_metadata_popup(position, book_id); + } // Render books on the floor let floor_book_sprite = SPRITESHEET.book.clone(); @@ -213,28 +211,25 @@ impl ClientState { ); } - // Render book metadata popup if local player is near a floor book - if let Some(nearby_floor_book) = - self.interpolated_game - .floor_books() - .iter() - .find(|floor_book| { - floor_book - .collider() - .rect() - .offset(floor_book.position()) - .overlaps( - &local_player - .collider() - .rect() - .offset(local_player.position()), - ) - }) + // Render metadata popup for a floor book if the user mouses over it + if let Some(moused_floor_book) = + self.interpolated_game.floor_books().iter().find(|fb| { + fb.collider() + .rect() + .offset(fb.position()) + .offset(-self.camera.position) + .contains({ + let (x, y) = mouse_position(); + Vec2::new(x, y) + }) + }) { - self.draw_book_metadata_popup( - nearby_floor_book.position(), - &nearby_floor_book.id(), + let popup_position = Vec2::new( + moused_floor_book.position().x - self.camera.position.x + + SPRITESHEET.book.size.x * 0.4, + moused_floor_book.position().y - self.camera.position.y - 5., ); + self.draw_book_metadata_popup(popup_position, &moused_floor_book.id()); } // Render list of books held by the local player @@ -254,7 +249,7 @@ impl ClientState { // Render debug view if enabled if self.debug_view { draw_rectangle(0., 0., 284., 56., BLACK); - draw_text("Library Tech Demo 2026-09-09", 4., 16., 16., WHITE); + draw_text("Library Tech Demo 2026-09-14", 4., 16., 16., WHITE); draw_text(format!("fps {}", get_fps()), 4., 32., 16., WHITE); draw_text(format!("id {}", local_player_id), 4., 48., 16., WHITE); draw_text( @@ -290,22 +285,32 @@ impl ClientState { let font_size = 24.; let text_dimensions = measure_text(&text, None, font_size as u16, 1.); let padding = 4.; - let x = position.x - self.camera.position.x; - let y = position.y - self.camera.position.y - 16.; + let Vec2 { x, y } = position; + let triangle_width = Bookshelf::BOOK_WIDTH * 2.; + let triangle_height = 10.; + let text_x = x - text_dimensions.width / 2.; + let text_y = + y - text_dimensions.height + text_dimensions.offset_y - triangle_height - padding; let w = text_dimensions.width; let h = text_dimensions.height; draw_rectangle( - x - padding, - y - text_dimensions.offset_y - padding, - w + padding, - h + padding, + text_x - padding, + text_y - text_dimensions.offset_y - padding, + w + padding * 2., + h + padding * 2., WHITE.with_alpha(0.5), ); - draw_text(text, x, y, font_size, BLACK.with_alpha(0.5)); + draw_text(text, text_x, text_y, font_size, BLACK.with_alpha(0.5)); draw_triangle( - Vec2::new(x + 4., y + 5.), - Vec2::new(x + 12., y + 13.), - Vec2::new(x + 20., y + 5.), + Vec2::new( + x - triangle_width / 2. + Bookshelf::BOOK_WIDTH / 2., + y - triangle_height, + ), + Vec2::new(x + Bookshelf::BOOK_WIDTH / 2., y), + Vec2::new( + x + triangle_width / 2. + Bookshelf::BOOK_WIDTH / 2., + y - triangle_height, + ), WHITE.with_alpha(0.5), ); } diff --git a/client/src/run.rs b/client/src/run.rs @@ -163,7 +163,8 @@ impl ClientState { let mut new_actions = [None, None, None, None, None]; new_actions[0] = handle_movement_input(&local_player, &self.dt); - new_actions[1] = handle_pick_up_book_input(&local_player, self.game.floor_books()); + new_actions[1] = + handle_pick_up_book_input(&local_player, &self.camera, self.game.floor_books()); new_actions[2] = handle_drop_book_input(&local_player, self.selected_inventory_book); new_actions[3] = handle_put_book_on_shelf_input( self.game.bookshelves(), @@ -171,8 +172,11 @@ impl ClientState { &local_player, self.selected_inventory_book, ); - new_actions[4] = - handle_take_book_off_shelf_input(self.game.bookshelves(), &self.camera); + new_actions[4] = handle_take_book_off_shelf_input( + self.game.bookshelves(), + &self.camera, + &local_player, + ); for new_action in new_actions.iter().filter_map(|na| na.clone()) { let action = Action::new(self.sequence_number, id, new_action); diff --git a/common/src/game/state.rs b/common/src/game/state.rs @@ -25,6 +25,7 @@ pub struct GameState { impl GameState { pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.; + pub const MAX_PLAYER_ACTION_RADIUS: f32 = 100.; pub fn apply_action(&mut self, action: &Action) { match &action.kind { @@ -52,6 +53,7 @@ impl GameState { // - bookshelf exists // - slot address is valid for bookshelf // - slot is empty + // - TODO player is close enough if BOOKS.get(book_id).is_some() && let Some(player) = self.players.get_mut(&action.player_id) && let Some(player_book_index) = @@ -74,6 +76,7 @@ impl GameState { // - bookshelf exists // - slot address is valid for bookshelf // - slot contains a book + // - TODO player is close enough if let Some(player) = self.players.get_mut(&action.player_id) && player.books.len() < Player::MAX_BOOKS_COUNT && let Some(bookshelf) = self.bookshelves.get_mut(&slot_address.bookshelf_id()) @@ -214,15 +217,12 @@ impl GameState { .iter() .enumerate() .find(|(_, fb)| fb.id() == *book_id) + && player.position().distance(floor_book.position()) + <= GameState::MAX_PLAYER_ACTION_RADIUS + && player.books.len() < Player::MAX_BOOKS_COUNT { - let player_collider = player.collider.rect().offset(player.position); - let floor_book_collider = floor_book.collider().rect().offset(floor_book.position()); - if player_collider.overlaps(&floor_book_collider) - && player.books.len() < Player::MAX_BOOKS_COUNT - { - let book = self.floor_books.remove(floor_book_index); - player.books.push(book.id()); - } + let book = self.floor_books.remove(floor_book_index); + player.books.push(book.id()); } } fn drop_book(&mut self, player_id: &Uuid, book_id: &BookId) {