library

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

state.rs (11156B)


      1 use std::{collections::HashMap, time::SystemTime};
      2 
      3 use macroquad::{
      4     math::{Rect, Vec2},
      5     rand::{rand, srand},
      6 };
      7 use serde::{Deserialize, Serialize};
      8 use structdiff::{Difference, StructDiff};
      9 use uuid::Uuid;
     10 
     11 use crate::game::{
     12     Action, ActionKind, Player,
     13     book::{BOOKS, BookId, Bookshelf, BookshelfId, FloorBook, Slot},
     14     rects_strictly_overlap,
     15 };
     16 
     17 #[derive(Clone, Debug, Difference, Deserialize, PartialEq, Serialize)]
     18 pub struct GameState {
     19     #[difference(skip)]
     20     timestamp: SystemTime,
     21     players: HashMap<Uuid, Player>,
     22     bookshelves: HashMap<BookshelfId, Bookshelf>,
     23     floor_books: Vec<FloorBook>,
     24 }
     25 
     26 impl GameState {
     27     pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.;
     28     pub const MAX_PLAYER_ACTION_RADIUS: f32 = 100.;
     29 
     30     pub fn apply_action(&mut self, action: &Action) {
     31         match &action.kind {
     32             ActionKind::Join { name } => {
     33                 self.players
     34                     .insert(action.player_id, Player::new(name.to_string()));
     35             }
     36             ActionKind::Move { new_position } => {
     37                 self.move_player(action.player_id, *new_position, action.timestamp);
     38             }
     39             ActionKind::PickUpBook { book_id } => {
     40                 self.pick_up_book(&action.player_id, book_id);
     41             }
     42             ActionKind::DropBook { book_id } => {
     43                 self.drop_book(&action.player_id, book_id);
     44             }
     45             ActionKind::PutBookOnShelf {
     46                 book_id,
     47                 slot_address,
     48             } => {
     49                 // Checks:
     50                 // - book id exists
     51                 // - player exists
     52                 // - book id is in player inventory
     53                 // - bookshelf exists
     54                 // - slot address is valid for bookshelf
     55                 // - slot is empty
     56                 // - TODO player is close enough
     57                 if BOOKS.get(book_id).is_some()
     58                     && let Some(player) = self.players.get_mut(&action.player_id)
     59                     && let Some(player_book_index) =
     60                         player.books.iter().enumerate().find_map(|(index, b_id)| {
     61                             if b_id == book_id { Some(index) } else { None }
     62                         })
     63                     && let Some(bookshelf) = self.bookshelves.get_mut(&slot_address.bookshelf_id())
     64                     && let Some(slot) = bookshelf.get_slot_mut(slot_address)
     65                     && let Slot::Empty = slot
     66                 {
     67                     *slot = Slot::Book {
     68                         book_id: player.books.remove(player_book_index),
     69                     };
     70                 }
     71             }
     72             ActionKind::TakeBookOffShelf { slot_address } => {
     73                 // Checks:
     74                 // - player exists
     75                 // - player has free inventory space
     76                 // - bookshelf exists
     77                 // - slot address is valid for bookshelf
     78                 // - slot contains a book
     79                 // - TODO player is close enough
     80                 if let Some(player) = self.players.get_mut(&action.player_id)
     81                     && player.books.len() < Player::MAX_BOOKS_COUNT
     82                     && let Some(bookshelf) = self.bookshelves.get_mut(&slot_address.bookshelf_id())
     83                     && let Some(slot) = bookshelf.get_slot_mut(slot_address)
     84                     && let Slot::Book { book_id } = *slot
     85                 {
     86                     *slot = Slot::Empty;
     87                     player.books.push(book_id);
     88                 }
     89             }
     90         }
     91     }
     92     pub fn interpolate(
     93         old: &Self,
     94         new: &Self,
     95         basis: SystemTime,
     96         local_player_id: Option<Uuid>,
     97     ) -> GameState {
     98         // Some fields should not be interpolated, and to minimize apparently acausal behavior we
     99         // default to the old game state's version
    100         let mut output = GameState {
    101             bookshelves: old.bookshelves.clone(),
    102             floor_books: old.floor_books.clone(),
    103             ..Default::default()
    104         };
    105 
    106         for (new_id, new_player) in new.players.iter() {
    107             if let Some(old_player) = old.players.get(new_id) {
    108                 if let Some(ref lpid) = local_player_id
    109                     && new_id == lpid
    110                 {
    111                     // If the player we're interpolating is the local player (that is, the one that
    112                     // a user is controlling in their client), then skip interpolation and use the
    113                     // new game state's version
    114                     output.players.insert(*new_id, new_player.clone());
    115                 } else {
    116                     // For all other players, linearly interpolate their positions based on the two
    117                     // known game states and the elapsed time between them; this gives the illusion
    118                     // of smooth motion at the expense of latency
    119                     let span = new.timestamp.duration_since(old.timestamp);
    120                     let fraction = SystemTime::now().duration_since(basis);
    121                     let t = match (fraction, span) {
    122                         (Ok(f), Ok(s)) => (f.as_secs_f32() / s.as_secs_f32()).clamp(0., 1.),
    123                         _ => return new.clone(),
    124                     };
    125                     let interpolated_player = Player {
    126                         position: (new_player.position() - old_player.position()) * t
    127                             + old_player.position(),
    128                         ..new_player.clone()
    129                     };
    130                     output.players.insert(*new_id, interpolated_player);
    131                 }
    132             } else {
    133                 // If the player exists in the new game state but not the old one, then don't try to
    134                 // interpolate anything
    135                 output.players.insert(*new_id, new_player.clone());
    136             }
    137         }
    138         output
    139     }
    140     pub fn get_player(&self, id: &Uuid) -> Option<&Player> {
    141         self.players.get(id)
    142     }
    143     pub fn get_player_mut(&mut self, id: &Uuid) -> Option<&mut Player> {
    144         self.players.get_mut(id)
    145     }
    146     pub fn players(&self) -> &HashMap<Uuid, Player> {
    147         &self.players
    148     }
    149     pub fn insert_player(&mut self, id: Uuid, player: Player) -> Option<Player> {
    150         self.players.insert(id, player)
    151     }
    152     pub fn remove_player(&mut self, id: &Uuid) -> Option<Player> {
    153         self.players.remove(id)
    154     }
    155     pub fn set_timestamp(&mut self, timestamp: SystemTime) {
    156         self.timestamp = timestamp;
    157     }
    158     pub fn get_timestamp(&self) -> SystemTime {
    159         self.timestamp
    160     }
    161     pub fn update_timestamp(&mut self) {
    162         self.set_timestamp(SystemTime::now());
    163     }
    164     pub fn bookshelves(&self) -> &HashMap<BookshelfId, Bookshelf> {
    165         &self.bookshelves
    166     }
    167     pub fn floor_books(&self) -> &Vec<FloorBook> {
    168         self.floor_books.as_ref()
    169     }
    170     pub fn move_player(&mut self, player_id: Uuid, new_position: Vec2, timestamp: SystemTime) {
    171         if let Some(player) = self.players.get_mut(&player_id)
    172             && let Ok(dt) = timestamp.duration_since(player.timestamp)
    173         {
    174             let velocity = (new_position - player.position) / dt.as_secs_f32();
    175             let limited_velocity = if velocity.length() > GameState::MAX_PLAYER_SPEED_PX_S {
    176                 velocity.normalize() * GameState::MAX_PLAYER_SPEED_PX_S
    177             } else {
    178                 velocity
    179             };
    180             player.position += limited_velocity * dt.as_secs_f32();
    181 
    182             // Handle collisions with bookshelves
    183             let player_collider = player.collider.rect().offset(player.position);
    184             for bookshelf in self.bookshelves.values() {
    185                 // If the player's movement would cause their collider to intersect with a
    186                 // bookshelf's collider, then compute the minimum axis-aligned motion
    187                 // required to make them touch but not intersect
    188                 let bookshelf_collider = bookshelf.rect();
    189                 let (a, b) = (bookshelf_collider, player_collider);
    190                 if rects_strictly_overlap(&a, &b) {
    191                     // Compute the required motion in each possible direction and select the
    192                     // smallest among them
    193                     let up = Vec2::new(0., a.top() - b.bottom());
    194                     let down = Vec2::new(0., a.bottom() - b.top());
    195                     let left = Vec2::new(a.left() - b.right(), 0.);
    196                     let right = Vec2::new(a.right() - b.left(), 0.);
    197                     let min_y = if up.y.abs() < down.y.abs() { up } else { down };
    198                     let min_x = if right.x.abs() < left.x.abs() {
    199                         right
    200                     } else {
    201                         left
    202                     };
    203                     let min = if min_x.x.abs() < min_y.y.abs() {
    204                         min_x
    205                     } else {
    206                         min_y
    207                     };
    208                     player.position += min;
    209                 }
    210             }
    211         }
    212     }
    213     fn pick_up_book(&mut self, player_id: &Uuid, book_id: &BookId) {
    214         if let Some(player) = self.players.get_mut(player_id)
    215             && let Some((floor_book_index, floor_book)) = self
    216                 .floor_books
    217                 .iter()
    218                 .enumerate()
    219                 .find(|(_, fb)| fb.id() == *book_id)
    220             && player.position().distance(floor_book.position())
    221                 <= GameState::MAX_PLAYER_ACTION_RADIUS
    222             && player.books.len() < Player::MAX_BOOKS_COUNT
    223         {
    224             let book = self.floor_books.remove(floor_book_index);
    225             player.books.push(book.id());
    226         }
    227     }
    228     fn drop_book(&mut self, player_id: &Uuid, book_id: &BookId) {
    229         if let Some(player) = self.players.get_mut(player_id)
    230             && let Some(book_index) = player.books.iter().position(|b| *b == *book_id)
    231         {
    232             let book = player.books.remove(book_index);
    233             self.floor_books.push(FloorBook::new(
    234                 book,
    235                 player.position,
    236                 Rect::new(0., 0., 30., 23.6),
    237             ));
    238         }
    239     }
    240 }
    241 
    242 impl Default for GameState {
    243     fn default() -> Self {
    244         Self {
    245             timestamp: SystemTime::now(),
    246             players: HashMap::with_capacity(32),
    247             bookshelves: HashMap::from_iter(vec![
    248                 (
    249                     Uuid::from_u128(0xa63ace6da898476a98645abe3665fa17),
    250                     Bookshelf::new(Vec2::new(-100., -300.), 3, 50),
    251                 ),
    252                 (
    253                     Uuid::from_u128(0x48faf42fb7ca424b8dbb4ddcfdf3ce2a),
    254                     Bookshelf::new(Vec2::new(500., -300.), 1, 50),
    255                 ),
    256             ]),
    257             floor_books: {
    258                 srand(0);
    259                 BOOKS
    260                     .keys()
    261                     .map(|&id| {
    262                         FloorBook::new(
    263                             id,
    264                             Vec2::new((rand() % 750) as f32, (rand() % 750) as f32),
    265                             Rect::new(0., 0., 30., 23.6),
    266                         )
    267                     })
    268                     .collect()
    269             },
    270         }
    271     }
    272 }