Live location tracking and playback for the game "manhunt"

Add all commands needed?

bwc9876.dev 729d4947 2532b7b8

verified
+422 -74
+1 -1
backend/Cargo.toml
··· 24 24 rand_chacha = "0.9.0" 25 25 futures = "0.3.31" 26 26 matchbox_socket = "0.12.0" 27 - uuid = "1.17.0" 27 + uuid = { version = "1.17.0", features = ["serde", "v4"] } 28 28 rmp-serde = "1.3.0" 29 29 tauri-plugin-store = "2.2.0" 30 30 specta = { version = "=2.0.0-rc.22", features = ["chrono", "uuid"] }
+3 -3
backend/src/game/events.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - use super::{location::Location, state::PlayerPing, PlayerId}; 3 + use super::{location::Location, state::PlayerPing, Id}; 4 4 5 5 /// An event used between players to update state 6 6 #[derive(Debug, Clone, Serialize, Deserialize)] 7 - pub enum GameEvent<Id: PlayerId> { 7 + pub enum GameEvent { 8 8 /// A player has been caught and is now a seeker, contains the ID of the caught player 9 9 PlayerCaught(Id), 10 10 /// Public ping from a player revealing location 11 - Ping(PlayerPing<Id>), 11 + Ping(PlayerPing), 12 12 /// Force the player specified in `0` to ping, optionally display the ping as from the user 13 13 /// specified in `1`. 14 14 ForcePing(Id, Option<Id>),
+33 -40
backend/src/game/mod.rs
··· 2 2 pub use events::GameEvent; 3 3 use powerups::PowerUpType; 4 4 pub use settings::GameSettings; 5 - use std::{ 6 - collections::HashMap, 7 - fmt::{Debug, Display}, 8 - hash::Hash, 9 - sync::Arc, 10 - time::Duration, 11 - }; 5 + use std::{collections::HashMap, sync::Arc, time::Duration}; 12 6 use uuid::Uuid; 13 7 14 8 use tokio::{sync::RwLock, time::MissedTickBehavior}; ··· 24 18 pub use state::GameState; 25 19 pub use transport::Transport; 26 20 27 - pub trait PlayerId: 28 - Display + Debug + Hash + Ord + Eq + PartialEq + Send + Sync + Sized + Copy + Clone + specta::Type 29 - { 30 - 31 - } 32 - 33 - impl PlayerId for Uuid {} 21 + pub type Id = Uuid; 34 22 35 23 /// Convenence alias for UTC DT 36 24 pub type UtcDT = DateTime<Utc>; ··· 38 26 /// Struct representing an ongoing game, handles communication with 39 27 /// other clients via [Transport], gets location with [LocationService], and provides high-level methods for 40 28 /// taking actions in the game. 41 - pub struct Game<Id: PlayerId, L: LocationService, T: Transport<Id>> { 42 - state: RwLock<GameState<Id>>, 29 + pub struct Game<L: LocationService, T: Transport> { 30 + state: RwLock<GameState>, 43 31 transport: Arc<T>, 44 32 location: L, 45 33 interval: Duration, 46 34 } 47 35 48 - impl<Id: PlayerId, L: LocationService, T: Transport<Id>> Game<Id, L, T> { 36 + impl<L: LocationService, T: Transport> Game<L, T> { 49 37 pub fn new( 50 38 my_id: Id, 51 39 interval: Duration, ··· 54 42 transport: Arc<T>, 55 43 location: L, 56 44 ) -> Self { 57 - let state = GameState::<Id>::new(settings, my_id, initial_caught_state); 45 + let state = GameState::new(settings, my_id, initial_caught_state); 58 46 59 47 Self { 60 48 transport, ··· 64 52 } 65 53 } 66 54 67 - pub async fn clone_state(&self) -> GameState<Id> { 55 + pub async fn clone_state(&self) -> GameState { 68 56 self.state.read().await.clone() 69 57 } 70 58 ··· 116 104 } 117 105 } 118 106 119 - async fn consume_event(&self, event: GameEvent<Id>) { 107 + async fn consume_event(&self, event: GameEvent) { 120 108 let mut state = self.state.write().await; 121 109 122 110 match event { ··· 235 223 use super::*; 236 224 use tokio::{sync::Mutex, task::yield_now, test}; 237 225 238 - type GameEventRx = tokio::sync::mpsc::Receiver<GameEvent<u32>>; 239 - type GameEventTx = tokio::sync::mpsc::Sender<GameEvent<u32>>; 240 - 241 - impl PlayerId for u32 {} 226 + type GameEventRx = tokio::sync::mpsc::Receiver<GameEvent>; 227 + type GameEventTx = tokio::sync::mpsc::Sender<GameEvent>; 242 228 243 229 struct MockTransport { 244 230 rx: Mutex<GameEventRx>, 245 231 txs: Vec<GameEventTx>, 246 232 } 247 233 248 - impl Transport<u32> for MockTransport { 249 - async fn receive_message(&self) -> Option<GameEvent<u32>> { 234 + impl Transport for MockTransport { 235 + async fn receive_message(&self) -> Option<GameEvent> { 250 236 let mut rx = self.rx.lock().await; 251 237 rx.recv().await 252 238 } 253 239 254 - async fn send_message(&self, msg: GameEvent<u32>) { 240 + async fn send_message(&self, msg: GameEvent) { 255 241 for (_id, tx) in self.txs.iter().enumerate() { 256 242 tx.send(msg.clone()).await.expect("Failed to send msg"); 257 243 } ··· 270 256 } 271 257 } 272 258 273 - type TestGame = Game<u32, MockLocation, MockTransport>; 259 + type TestGame = Game<MockLocation, MockTransport>; 274 260 275 261 struct MockMatch { 262 + uuids: Vec<Uuid>, 276 263 games: HashMap<u32, Arc<TestGame>>, 277 264 settings: GameSettings, 278 265 mock_now: UtcDT, ··· 282 269 283 270 impl MockMatch { 284 271 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self { 272 + let uuids = (0..players) 273 + .into_iter() 274 + .map(|_| uuid::Uuid::new_v4()) 275 + .collect::<Vec<_>>(); 276 + 285 277 let channels = (0..players) 286 278 .into_iter() 287 279 .map(|_| tokio::sync::mpsc::channel(10)) ··· 289 281 290 282 let initial_caught_state = (0..players) 291 283 .into_iter() 292 - .map(|id| (id, id < seekers)) 284 + .map(|id| (uuids[id as usize], id < seekers)) 293 285 .collect::<HashMap<_, _>>(); 294 286 let txs = channels 295 287 .iter() ··· 306 298 }; 307 299 let location = MockLocation; 308 300 let game = TestGame::new( 309 - id as u32, 301 + uuids[id], 310 302 INTERVAL, 311 303 initial_caught_state.clone(), 312 304 settings.clone(), ··· 321 313 Self { 322 314 settings, 323 315 games, 316 + uuids, 324 317 mock_now: Utc::now(), 325 318 } 326 319 } ··· 339 332 self.mock_now += d; 340 333 } 341 334 342 - pub async fn assert_all_states(&self, f: impl Fn(&GameState<u32>)) { 335 + pub async fn assert_all_states(&self, f: impl Fn(&GameState)) { 343 336 for (_, game) in &self.games { 344 337 let state = game.state.read().await; 345 338 f(&state); ··· 408 401 409 402 mat.assert_all_states(|s| { 410 403 assert_eq!( 411 - s.get_caught(1), 404 + s.get_caught(mat.uuids[1]), 412 405 Some(true), 413 406 "Game {} sees player 1 as not caught", 414 407 s.id ··· 432 425 433 426 mat.assert_all_states(|s| { 434 427 for id in 0..4 { 435 - let ping = s.get_ping(id); 428 + let ping = s.get_ping(mat.uuids[id]); 436 429 if id == 0 { 437 430 assert!( 438 431 ping.is_none(), ··· 457 450 458 451 mat.assert_all_states(|s| { 459 452 for id in 0..4 { 460 - let ping = s.get_ping(id); 453 + let ping = s.get_ping(mat.uuids[id]); 461 454 if id <= 1 { 462 455 assert!( 463 456 ping.is_none(), ··· 535 528 mat.tick().await; 536 529 537 530 mat.assert_all_states(|s| { 538 - if let Some(ping) = s.get_ping(1) { 531 + if let Some(ping) = s.get_ping(mat.uuids[1]) { 539 532 assert_eq!( 540 - ping.real_player, 0, 533 + ping.real_player, mat.uuids[0], 541 534 "Ping for 1 is not truly 0 (in {})", 542 535 s.id 543 536 ); ··· 568 561 mat.assert_all_states(|s| { 569 562 // Player 0 is a seeker, player 1 user the powerup, so 2 is the only one that should 570 563 // could have pinged 571 - assert!(s.get_ping(2).is_some()); 572 - assert!(s.get_ping(0).is_none()); 573 - assert!(s.get_ping(1).is_none()); 564 + assert!(s.get_ping(mat.uuids[2]).is_some()); 565 + assert!(s.get_ping(mat.uuids[0]).is_none()); 566 + assert!(s.get_ping(mat.uuids[1]).is_none()); 574 567 }) 575 568 .await; 576 569 } ··· 594 587 mat.assert_all_states(|s| { 595 588 for id in 0..3 { 596 589 assert!( 597 - s.get_caught(id).is_some(), 590 + s.get_caught(mat.uuids[id]).is_some(), 598 591 "Player {} should be pinged due to the powerup (in {})", 599 592 id, 600 593 s.id
+13 -13
backend/src/game/state.rs
··· 13 13 location::Location, 14 14 powerups::PowerUpType, 15 15 settings::{GameSettings, PingStartCondition}, 16 - PlayerId, UtcDT, 16 + Id, UtcDT, 17 17 }; 18 18 19 19 #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] 20 20 /// An on-map ping of a player 21 - pub struct PlayerPing<Id: PlayerId> { 21 + pub struct PlayerPing { 22 22 /// Location of the ping 23 23 loc: Location, 24 24 /// Time the ping happened ··· 29 29 pub real_player: Id, 30 30 } 31 31 32 - impl<Id: PlayerId> PlayerPing<Id> { 32 + impl PlayerPing { 33 33 pub fn new(loc: Location, display_player: Id, real_player: Id) -> Self { 34 34 Self { 35 35 loc, ··· 42 42 43 43 #[derive(Debug, Clone, Serialize, specta::Type)] 44 44 /// This struct handles all logic regarding state updates 45 - pub struct GameState<Id: PlayerId> { 45 + pub struct GameState { 46 46 /// The id of this player in this game 47 47 pub id: Id, 48 48 ··· 65 65 caught_state: HashMap<Id, bool>, 66 66 67 67 /// A map of the latest global ping results for each player 68 - pings: HashMap<Id, PlayerPing<Id>>, 68 + pings: HashMap<Id, PlayerPing>, 69 69 70 70 /// Powerup on the map that players can grab. Only one at a time 71 71 available_powerup: Option<Location>, ··· 92 92 shared_random_state: u64, 93 93 } 94 94 95 - impl<Id: PlayerId> GameState<Id> { 95 + impl GameState { 96 96 pub fn new(settings: GameSettings, my_id: Id, initial_caught_state: HashMap<Id, bool>) -> Self { 97 97 let mut rand = ChaCha20Rng::seed_from_u64(settings.random_seed as u64); 98 98 let increment = rand.random_range(-100..100); ··· 230 230 } 231 231 232 232 /// Add a ping for a specific player 233 - pub fn add_ping(&mut self, ping: PlayerPing<Id>) { 233 + pub fn add_ping(&mut self, ping: PlayerPing) { 234 234 self.pings.insert(ping.display_player, ping); 235 235 } 236 236 237 237 /// Get a ping for a player 238 238 #[cfg(test)] 239 - pub fn get_ping(&self, player: Id) -> Option<&PlayerPing<Id>> { 239 + pub fn get_ping(&self, player: Id) -> Option<&PlayerPing> { 240 240 self.pings.get(&player) 241 241 } 242 242 243 243 /// Remove a ping from the map 244 - pub fn remove_ping(&mut self, player: Id) -> Option<PlayerPing<Id>> { 244 + pub fn remove_ping(&mut self, player: Id) -> Option<PlayerPing> { 245 245 self.pings.remove(&player) 246 246 } 247 247 248 248 /// Iterate over all seekers in the game 249 - pub fn iter_seekers(&self) -> impl Iterator<Item = Id> + use<'_, Id> { 249 + pub fn iter_seekers(&self) -> impl Iterator<Item = Id> + use<'_> { 250 250 self.caught_state 251 251 .iter() 252 252 .filter_map(|(k, v)| if *v { Some(*k) } else { None }) ··· 260 260 } 261 261 262 262 /// Iterate over all hiders in the game 263 - fn iter_hiders(&self) -> impl Iterator<Item = Id> + use<'_, Id> { 263 + fn iter_hiders(&self) -> impl Iterator<Item = Id> + use<'_> { 264 264 self.caught_state 265 265 .iter() 266 266 .filter_map(|(k, v)| if !*v { Some(*k) } else { None }) ··· 274 274 } 275 275 276 276 /// Create a [PlayerPing] with the latest location saved for the player 277 - pub fn create_self_ping(&self) -> Option<PlayerPing<Id>> { 277 + pub fn create_self_ping(&self) -> Option<PlayerPing> { 278 278 self.create_ping(self.id) 279 279 } 280 280 281 281 /// Create a [PlayerPing] with the latest location as another player 282 - pub fn create_ping(&self, id: Id) -> Option<PlayerPing<Id>> { 282 + pub fn create_ping(&self, id: Id) -> Option<PlayerPing> { 283 283 self.get_loc() 284 284 .map(|loc| PlayerPing::new(loc.clone(), id, self.id)) 285 285 }
+4 -4
backend/src/game/transport.rs
··· 1 - use super::{events::GameEvent, PlayerId}; 1 + use super::events::GameEvent; 2 2 3 - pub trait Transport<Id: PlayerId> { 3 + pub trait Transport { 4 4 /// Receive an event 5 - async fn receive_message(&self) -> Option<GameEvent<Id>>; 5 + async fn receive_message(&self) -> Option<GameEvent>; 6 6 /// Send an event 7 - async fn send_message(&self, msg: GameEvent<Id>); 7 + async fn send_message(&self, msg: GameEvent); 8 8 }
+7 -9
backend/src/lib.rs
··· 6 6 7 7 use std::{sync::Arc, time::Duration}; 8 8 9 - use game::{Game as BaseGame, GameSettings, GameState as BaseGameState}; 9 + use game::{Game as BaseGame, GameSettings, GameState}; 10 10 use lobby::{Lobby, LobbyState, StartGameInfo}; 11 11 use location::TauriLocation; 12 12 use profile::PlayerProfile; ··· 18 18 use transport::MatchboxTransport; 19 19 use uuid::Uuid; 20 20 21 - type Game = BaseGame<Uuid, TauriLocation, MatchboxTransport>; 21 + type Game = BaseGame<TauriLocation, MatchboxTransport>; 22 22 23 23 enum AppState { 24 24 Setup, ··· 241 241 242 242 // AppScreen::Game COMMANDS 243 243 244 - type AppGameState = BaseGameState<Uuid>; 245 - 246 244 #[tauri::command] 247 245 #[specta::specta] 248 246 /// (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state 249 - async fn mark_caught(state: State<'_, AppStateHandle>) -> Result<AppGameState> { 247 + async fn mark_caught(state: State<'_, AppStateHandle>) -> Result<GameState> { 250 248 let state = state.read().await; 251 249 if let AppState::Game(game) = &*state { 252 250 game.mark_caught().await; ··· 260 258 #[specta::specta] 261 259 /// (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of 262 260 /// the powerup. Returns the new game state after rolling for the powerup 263 - async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result<AppGameState> { 261 + async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result<GameState> { 264 262 let state = state.read().await; 265 263 if let AppState::Game(game) = &*state { 266 264 game.get_powerup().await; ··· 274 272 #[specta::specta] 275 273 /// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the 276 274 /// player has none. Returns the updated game state 277 - async fn use_powerup(state: State<'_, AppStateHandle>) -> Result<AppGameState> { 275 + async fn use_powerup(state: State<'_, AppStateHandle>) -> Result<GameState> { 278 276 let state = state.read().await; 279 277 if let AppState::Game(game) = &*state { 280 278 game.use_powerup().await; ··· 298 296 switch_teams, 299 297 host_start_game, 300 298 mark_caught, 301 - // grab_powerup, 302 - // use_powerup, 299 + grab_powerup, 300 + use_powerup, 303 301 ]); 304 302 305 303 #[cfg(debug_assertions)]
+4 -4
backend/src/transport.rs
··· 14 14 #[derive(Debug, Serialize, Deserialize, Clone)] 15 15 pub enum TransportMessage { 16 16 /// Message related to the actual game 17 - Game(GameEvent<Uuid>), 17 + Game(GameEvent), 18 18 /// Message related to the pre-game lobby 19 19 Lobby(LobbyMessage), 20 20 /// Internal message when peer connects ··· 154 154 } 155 155 } 156 156 157 - impl Transport<Uuid> for MatchboxTransport { 158 - async fn receive_message(&self) -> Option<GameEvent<Uuid>> { 157 + impl Transport for MatchboxTransport { 158 + async fn receive_message(&self) -> Option<GameEvent> { 159 159 self.recv_transport_message() 160 160 .await 161 161 .and_then(|(_, msg)| match msg { ··· 164 164 }) 165 165 } 166 166 167 - async fn send_message(&self, msg: GameEvent<Uuid>) { 167 + async fn send_message(&self, msg: GameEvent) { 168 168 let msg = TransportMessage::Game(msg); 169 169 self.send_transport_message(None, msg).await; 170 170 }
+357
frontend/src/bindings.ts
··· 1 + 2 + // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. 3 + 4 + /** user-defined commands **/ 5 + 6 + 7 + export const commands = { 8 + /** 9 + * (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host, 10 + * set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby] 11 + */ 12 + async startLobby(joinCode: string | null, settings: GameSettings) : Promise<Result<null, string>> { 13 + try { 14 + return { status: "ok", data: await TAURI_INVOKE("start_lobby", { joinCode, settings }) }; 15 + } catch (e) { 16 + if(e instanceof Error) throw e; 17 + else return { status: "error", error: e as any }; 18 + } 19 + }, 20 + /** 21 + * Quit a running game or leave a lobby 22 + */ 23 + async quitGameOrLobby() : Promise<Result<null, string>> { 24 + try { 25 + return { status: "ok", data: await TAURI_INVOKE("quit_game_or_lobby") }; 26 + } catch (e) { 27 + if(e instanceof Error) throw e; 28 + else return { status: "error", error: e as any }; 29 + } 30 + }, 31 + /** 32 + * Get the screen the app should currently be on, returns [AppScreen] 33 + */ 34 + async getCurrentScreen() : Promise<Result<AppScreen, string>> { 35 + try { 36 + return { status: "ok", data: await TAURI_INVOKE("get_current_screen") }; 37 + } catch (e) { 38 + if(e instanceof Error) throw e; 39 + else return { status: "error", error: e as any }; 40 + } 41 + }, 42 + /** 43 + * (Screen: Menu) Update the player's profile and persist it 44 + */ 45 + async updateProfile(newProfile: PlayerProfile) : Promise<Result<null, string>> { 46 + try { 47 + return { status: "ok", data: await TAURI_INVOKE("update_profile", { newProfile }) }; 48 + } catch (e) { 49 + if(e instanceof Error) throw e; 50 + else return { status: "error", error: e as any }; 51 + } 52 + }, 53 + /** 54 + * (Screen: Lobby) Get the current state of the lobby, call after receiving an update event 55 + */ 56 + async getLobbyState() : Promise<Result<LobbyState, string>> { 57 + try { 58 + return { status: "ok", data: await TAURI_INVOKE("get_lobby_state") }; 59 + } catch (e) { 60 + if(e instanceof Error) throw e; 61 + else return { status: "error", error: e as any }; 62 + } 63 + }, 64 + /** 65 + * (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the 66 + * new lobby state 67 + */ 68 + async hostUpdateSettings(settings: GameSettings) : Promise<Result<LobbyState, string>> { 69 + try { 70 + return { status: "ok", data: await TAURI_INVOKE("host_update_settings", { settings }) }; 71 + } catch (e) { 72 + if(e instanceof Error) throw e; 73 + else return { status: "error", error: e as any }; 74 + } 75 + }, 76 + /** 77 + * (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState] 78 + */ 79 + async switchTeams(seeker: boolean) : Promise<Result<LobbyState, string>> { 80 + try { 81 + return { status: "ok", data: await TAURI_INVOKE("switch_teams", { seeker }) }; 82 + } catch (e) { 83 + if(e instanceof Error) throw e; 84 + else return { status: "error", error: e as any }; 85 + } 86 + }, 87 + /** 88 + * (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen 89 + * to AppScreen::Game. 90 + */ 91 + async hostStartGame() : Promise<Result<null, string>> { 92 + try { 93 + return { status: "ok", data: await TAURI_INVOKE("host_start_game") }; 94 + } catch (e) { 95 + if(e instanceof Error) throw e; 96 + else return { status: "error", error: e as any }; 97 + } 98 + }, 99 + /** 100 + * (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state 101 + */ 102 + async markCaught() : Promise<Result<GameState, string>> { 103 + try { 104 + return { status: "ok", data: await TAURI_INVOKE("mark_caught") }; 105 + } catch (e) { 106 + if(e instanceof Error) throw e; 107 + else return { status: "error", error: e as any }; 108 + } 109 + }, 110 + /** 111 + * (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of 112 + * the powerup. Returns the new game state after rolling for the powerup 113 + */ 114 + async grabPowerup() : Promise<Result<GameState, string>> { 115 + try { 116 + return { status: "ok", data: await TAURI_INVOKE("grab_powerup") }; 117 + } catch (e) { 118 + if(e instanceof Error) throw e; 119 + else return { status: "error", error: e as any }; 120 + } 121 + }, 122 + /** 123 + * (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the 124 + * player has none. Returns the updated game state 125 + */ 126 + async usePowerup() : Promise<Result<GameState, string>> { 127 + try { 128 + return { status: "ok", data: await TAURI_INVOKE("use_powerup") }; 129 + } catch (e) { 130 + if(e instanceof Error) throw e; 131 + else return { status: "error", error: e as any }; 132 + } 133 + } 134 + } 135 + 136 + /** user-defined events **/ 137 + 138 + 139 + 140 + /** user-defined constants **/ 141 + 142 + 143 + 144 + /** user-defined types **/ 145 + 146 + export type AppScreen = "Setup" | "Menu" | "Lobby" | "Game" 147 + /** 148 + * Settings for the game, host is the only person able to change these 149 + */ 150 + export type GameSettings = { 151 + /** 152 + * The random seed used for shared rng 153 + */ 154 + random_seed: number; 155 + /** 156 + * The number of seconds to wait before seekers are allowed to go 157 + */ 158 + hiding_time_seconds: number; 159 + /** 160 + * Condition to wait for global pings to begin 161 + */ 162 + ping_start: PingStartCondition; 163 + /** 164 + * Time between pings after the condition is met (first ping is either after the interval or 165 + * instantly after the condition is met depending on the condition) 166 + */ 167 + ping_minutes_interval: number; 168 + /** 169 + * Condition for powerups to start spawning 170 + */ 171 + powerup_start: PingStartCondition; 172 + /** 173 + * Chance every minute of a powerup spawning, out of 100 174 + */ 175 + powerup_chance: number; 176 + /** 177 + * Hard cooldown between powerups spawning 178 + */ 179 + powerup_minutes_cooldown: number; 180 + /** 181 + * Locations that powerups may spawn at 182 + */ 183 + powerup_locations: Location[] } 184 + /** 185 + * This struct handles all logic regarding state updates 186 + */ 187 + export type GameState = { 188 + /** 189 + * The id of this player in this game 190 + */ 191 + id: string; 192 + /** 193 + * The powerup the player is currently holding 194 + */ 195 + held_powerup: PowerUpType | null; 196 + /** 197 + * When the game started 198 + */ 199 + game_started: string; 200 + /** 201 + * When seekers were allowed to begin 202 + */ 203 + seekers_started: string | null; 204 + /** 205 + * Last time we pinged all players 206 + */ 207 + last_global_ping: string | null; 208 + /** 209 + * Last time a powerup was spawned 210 + */ 211 + last_powerup_spawn: string | null; 212 + /** 213 + * Hashmap tracking if a player is a seeker (true) or a hider (false) 214 + */ 215 + caught_state: Partial<{ [key in string]: boolean }>; 216 + /** 217 + * A map of the latest global ping results for each player 218 + */ 219 + pings: Partial<{ [key in string]: PlayerPing }>; 220 + /** 221 + * Powerup on the map that players can grab. Only one at a time 222 + */ 223 + available_powerup: Location | null } 224 + export type LobbyState = { profiles: Partial<{ [key in string]: PlayerProfile }>; join_code: string; 225 + /** 226 + * True represents seeker, false hider 227 + */ 228 + teams: Partial<{ [key in string]: boolean }>; self_seeker: boolean; settings: GameSettings } 229 + /** 230 + * Some location in the world as gotten from a Geolocation API 231 + */ 232 + export type Location = { 233 + /** 234 + * Latitude 235 + */ 236 + lat: number; 237 + /** 238 + * Longitude 239 + */ 240 + long: number; 241 + /** 242 + * The bearing (float normalized from 0 to 1) optional as GPS can't always determine 243 + */ 244 + heading: number | null } 245 + /** 246 + * The starting condition for global pings to begin 247 + */ 248 + export type PingStartCondition = 249 + /** 250 + * Wait For X players to be caught before beginning global pings 251 + */ 252 + { Players: number } | 253 + /** 254 + * Wait for X minutes after game start to begin global pings 255 + */ 256 + { Minutes: number } | 257 + /** 258 + * Don't wait at all, ping location after seekers are released 259 + */ 260 + "Instant" 261 + /** 262 + * An on-map ping of a player 263 + */ 264 + export type PlayerPing = { 265 + /** 266 + * Location of the ping 267 + */ 268 + loc: Location; 269 + /** 270 + * Time the ping happened 271 + */ 272 + timestamp: string; 273 + /** 274 + * The player to display as 275 + */ 276 + display_player: string; 277 + /** 278 + * The actual player that initialized this ping 279 + */ 280 + real_player: string } 281 + export type PlayerProfile = { display_name: string; pfp_base64: string | null } 282 + /** 283 + * Type of powerup 284 + */ 285 + export type PowerUpType = 286 + /** 287 + * Ping a random seeker instead of a hider 288 + */ 289 + "PingSeeker" | 290 + /** 291 + * Pings all seekers locations on the map for hiders 292 + */ 293 + "PingAllSeekers" | 294 + /** 295 + * Ping another random hider instantly 296 + */ 297 + "ForcePingOther" 298 + 299 + /** tauri-specta globals **/ 300 + 301 + import { 302 + invoke as TAURI_INVOKE, 303 + Channel as TAURI_CHANNEL, 304 + } from "@tauri-apps/api/core"; 305 + import * as TAURI_API_EVENT from "@tauri-apps/api/event"; 306 + import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; 307 + 308 + type __EventObj__<T> = { 309 + listen: ( 310 + cb: TAURI_API_EVENT.EventCallback<T>, 311 + ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>; 312 + once: ( 313 + cb: TAURI_API_EVENT.EventCallback<T>, 314 + ) => ReturnType<typeof TAURI_API_EVENT.once<T>>; 315 + emit: null extends T 316 + ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit> 317 + : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>; 318 + }; 319 + 320 + export type Result<T, E> = 321 + | { status: "ok"; data: T } 322 + | { status: "error"; error: E }; 323 + 324 + function __makeEvents__<T extends Record<string, any>>( 325 + mappings: Record<keyof T, string>, 326 + ) { 327 + return new Proxy( 328 + {} as unknown as { 329 + [K in keyof T]: __EventObj__<T[K]> & { 330 + (handle: __WebviewWindow__): __EventObj__<T[K]>; 331 + }; 332 + }, 333 + { 334 + get: (_, event) => { 335 + const name = mappings[event as keyof T]; 336 + 337 + return new Proxy((() => {}) as any, { 338 + apply: (_, __, [window]: [__WebviewWindow__]) => ({ 339 + listen: (arg: any) => window.listen(name, arg), 340 + once: (arg: any) => window.once(name, arg), 341 + emit: (arg: any) => window.emit(name, arg), 342 + }), 343 + get: (_, command: keyof __EventObj__<any>) => { 344 + switch (command) { 345 + case "listen": 346 + return (arg: any) => TAURI_API_EVENT.listen(name, arg); 347 + case "once": 348 + return (arg: any) => TAURI_API_EVENT.once(name, arg); 349 + case "emit": 350 + return (arg: any) => TAURI_API_EVENT.emit(name, arg); 351 + } 352 + }, 353 + }); 354 + }, 355 + }, 356 + ); 357 + }