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 }