···24rand_chacha = "0.9.0"
25futures = "0.3.31"
26matchbox_socket = "0.12.0"
27+uuid = { version = "1.17.0", features = ["serde", "v4"] }
28rmp-serde = "1.3.0"
29tauri-plugin-store = "2.2.0"
30specta = { version = "=2.0.0-rc.22", features = ["chrono", "uuid"] }
+3-3
backend/src/game/events.rs
···1use serde::{Deserialize, Serialize};
23-use super::{location::Location, state::PlayerPing, PlayerId};
45/// An event used between players to update state
6#[derive(Debug, Clone, Serialize, Deserialize)]
7-pub enum GameEvent<Id: PlayerId> {
8 /// A player has been caught and is now a seeker, contains the ID of the caught player
9 PlayerCaught(Id),
10 /// Public ping from a player revealing location
11- Ping(PlayerPing<Id>),
12 /// Force the player specified in `0` to ping, optionally display the ping as from the user
13 /// specified in `1`.
14 ForcePing(Id, Option<Id>),
···1use serde::{Deserialize, Serialize};
23+use super::{location::Location, state::PlayerPing, Id};
45/// An event used between players to update state
6#[derive(Debug, Clone, Serialize, Deserialize)]
7+pub enum GameEvent {
8 /// A player has been caught and is now a seeker, contains the ID of the caught player
9 PlayerCaught(Id),
10 /// Public ping from a player revealing location
11+ Ping(PlayerPing),
12 /// Force the player specified in `0` to ping, optionally display the ping as from the user
13 /// specified in `1`.
14 ForcePing(Id, Option<Id>),
+33-40
backend/src/game/mod.rs
···2pub use events::GameEvent;
3use powerups::PowerUpType;
4pub use settings::GameSettings;
5-use std::{
6- collections::HashMap,
7- fmt::{Debug, Display},
8- hash::Hash,
9- sync::Arc,
10- time::Duration,
11-};
12use uuid::Uuid;
1314use tokio::{sync::RwLock, time::MissedTickBehavior};
···24pub use state::GameState;
25pub use transport::Transport;
2627-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 {}
3435/// Convenence alias for UTC DT
36pub type UtcDT = DateTime<Utc>;
···38/// Struct representing an ongoing game, handles communication with
39/// other clients via [Transport], gets location with [LocationService], and provides high-level methods for
40/// taking actions in the game.
41-pub struct Game<Id: PlayerId, L: LocationService, T: Transport<Id>> {
42- state: RwLock<GameState<Id>>,
43 transport: Arc<T>,
44 location: L,
45 interval: Duration,
46}
4748-impl<Id: PlayerId, L: LocationService, T: Transport<Id>> Game<Id, L, T> {
49 pub fn new(
50 my_id: Id,
51 interval: Duration,
···54 transport: Arc<T>,
55 location: L,
56 ) -> Self {
57- let state = GameState::<Id>::new(settings, my_id, initial_caught_state);
5859 Self {
60 transport,
···64 }
65 }
6667- pub async fn clone_state(&self) -> GameState<Id> {
68 self.state.read().await.clone()
69 }
70···116 }
117 }
118119- async fn consume_event(&self, event: GameEvent<Id>) {
120 let mut state = self.state.write().await;
121122 match event {
···235 use super::*;
236 use tokio::{sync::Mutex, task::yield_now, test};
237238- type GameEventRx = tokio::sync::mpsc::Receiver<GameEvent<u32>>;
239- type GameEventTx = tokio::sync::mpsc::Sender<GameEvent<u32>>;
240-241- impl PlayerId for u32 {}
242243 struct MockTransport {
244 rx: Mutex<GameEventRx>,
245 txs: Vec<GameEventTx>,
246 }
247248- impl Transport<u32> for MockTransport {
249- async fn receive_message(&self) -> Option<GameEvent<u32>> {
250 let mut rx = self.rx.lock().await;
251 rx.recv().await
252 }
253254- async fn send_message(&self, msg: GameEvent<u32>) {
255 for (_id, tx) in self.txs.iter().enumerate() {
256 tx.send(msg.clone()).await.expect("Failed to send msg");
257 }
···270 }
271 }
272273- type TestGame = Game<u32, MockLocation, MockTransport>;
274275 struct MockMatch {
0276 games: HashMap<u32, Arc<TestGame>>,
277 settings: GameSettings,
278 mock_now: UtcDT,
···282283 impl MockMatch {
284 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self {
00000285 let channels = (0..players)
286 .into_iter()
287 .map(|_| tokio::sync::mpsc::channel(10))
···289290 let initial_caught_state = (0..players)
291 .into_iter()
292- .map(|id| (id, id < seekers))
293 .collect::<HashMap<_, _>>();
294 let txs = channels
295 .iter()
···306 };
307 let location = MockLocation;
308 let game = TestGame::new(
309- id as u32,
310 INTERVAL,
311 initial_caught_state.clone(),
312 settings.clone(),
···321 Self {
322 settings,
323 games,
0324 mock_now: Utc::now(),
325 }
326 }
···339 self.mock_now += d;
340 }
341342- pub async fn assert_all_states(&self, f: impl Fn(&GameState<u32>)) {
343 for (_, game) in &self.games {
344 let state = game.state.read().await;
345 f(&state);
···408409 mat.assert_all_states(|s| {
410 assert_eq!(
411- s.get_caught(1),
412 Some(true),
413 "Game {} sees player 1 as not caught",
414 s.id
···432433 mat.assert_all_states(|s| {
434 for id in 0..4 {
435- let ping = s.get_ping(id);
436 if id == 0 {
437 assert!(
438 ping.is_none(),
···457458 mat.assert_all_states(|s| {
459 for id in 0..4 {
460- let ping = s.get_ping(id);
461 if id <= 1 {
462 assert!(
463 ping.is_none(),
···535 mat.tick().await;
536537 mat.assert_all_states(|s| {
538- if let Some(ping) = s.get_ping(1) {
539 assert_eq!(
540- ping.real_player, 0,
541 "Ping for 1 is not truly 0 (in {})",
542 s.id
543 );
···568 mat.assert_all_states(|s| {
569 // Player 0 is a seeker, player 1 user the powerup, so 2 is the only one that should
570 // 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());
574 })
575 .await;
576 }
···594 mat.assert_all_states(|s| {
595 for id in 0..3 {
596 assert!(
597- s.get_caught(id).is_some(),
598 "Player {} should be pinged due to the powerup (in {})",
599 id,
600 s.id
···2pub use events::GameEvent;
3use powerups::PowerUpType;
4pub use settings::GameSettings;
5+use std::{collections::HashMap, sync::Arc, time::Duration};
0000006use uuid::Uuid;
78use tokio::{sync::RwLock, time::MissedTickBehavior};
···18pub use state::GameState;
19pub use transport::Transport;
2021+pub type Id = Uuid;
0000002223/// Convenence alias for UTC DT
24pub type UtcDT = DateTime<Utc>;
···26/// Struct representing an ongoing game, handles communication with
27/// other clients via [Transport], gets location with [LocationService], and provides high-level methods for
28/// taking actions in the game.
29+pub struct Game<L: LocationService, T: Transport> {
30+ state: RwLock<GameState>,
31 transport: Arc<T>,
32 location: L,
33 interval: Duration,
34}
3536+impl<L: LocationService, T: Transport> Game<L, T> {
37 pub fn new(
38 my_id: Id,
39 interval: Duration,
···42 transport: Arc<T>,
43 location: L,
44 ) -> Self {
45+ let state = GameState::new(settings, my_id, initial_caught_state);
4647 Self {
48 transport,
···52 }
53 }
5455+ pub async fn clone_state(&self) -> GameState {
56 self.state.read().await.clone()
57 }
58···104 }
105 }
106107+ async fn consume_event(&self, event: GameEvent) {
108 let mut state = self.state.write().await;
109110 match event {
···223 use super::*;
224 use tokio::{sync::Mutex, task::yield_now, test};
225226+ type GameEventRx = tokio::sync::mpsc::Receiver<GameEvent>;
227+ type GameEventTx = tokio::sync::mpsc::Sender<GameEvent>;
00228229 struct MockTransport {
230 rx: Mutex<GameEventRx>,
231 txs: Vec<GameEventTx>,
232 }
233234+ impl Transport for MockTransport {
235+ async fn receive_message(&self) -> Option<GameEvent> {
236 let mut rx = self.rx.lock().await;
237 rx.recv().await
238 }
239240+ async fn send_message(&self, msg: GameEvent) {
241 for (_id, tx) in self.txs.iter().enumerate() {
242 tx.send(msg.clone()).await.expect("Failed to send msg");
243 }
···256 }
257 }
258259+ type TestGame = Game<MockLocation, MockTransport>;
260261 struct MockMatch {
262+ uuids: Vec<Uuid>,
263 games: HashMap<u32, Arc<TestGame>>,
264 settings: GameSettings,
265 mock_now: UtcDT,
···269270 impl MockMatch {
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+277 let channels = (0..players)
278 .into_iter()
279 .map(|_| tokio::sync::mpsc::channel(10))
···281282 let initial_caught_state = (0..players)
283 .into_iter()
284+ .map(|id| (uuids[id as usize], id < seekers))
285 .collect::<HashMap<_, _>>();
286 let txs = channels
287 .iter()
···298 };
299 let location = MockLocation;
300 let game = TestGame::new(
301+ uuids[id],
302 INTERVAL,
303 initial_caught_state.clone(),
304 settings.clone(),
···313 Self {
314 settings,
315 games,
316+ uuids,
317 mock_now: Utc::now(),
318 }
319 }
···332 self.mock_now += d;
333 }
334335+ pub async fn assert_all_states(&self, f: impl Fn(&GameState)) {
336 for (_, game) in &self.games {
337 let state = game.state.read().await;
338 f(&state);
···401402 mat.assert_all_states(|s| {
403 assert_eq!(
404+ s.get_caught(mat.uuids[1]),
405 Some(true),
406 "Game {} sees player 1 as not caught",
407 s.id
···425426 mat.assert_all_states(|s| {
427 for id in 0..4 {
428+ let ping = s.get_ping(mat.uuids[id]);
429 if id == 0 {
430 assert!(
431 ping.is_none(),
···450451 mat.assert_all_states(|s| {
452 for id in 0..4 {
453+ let ping = s.get_ping(mat.uuids[id]);
454 if id <= 1 {
455 assert!(
456 ping.is_none(),
···528 mat.tick().await;
529530 mat.assert_all_states(|s| {
531+ if let Some(ping) = s.get_ping(mat.uuids[1]) {
532 assert_eq!(
533+ ping.real_player, mat.uuids[0],
534 "Ping for 1 is not truly 0 (in {})",
535 s.id
536 );
···561 mat.assert_all_states(|s| {
562 // Player 0 is a seeker, player 1 user the powerup, so 2 is the only one that should
563 // could have pinged
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());
567 })
568 .await;
569 }
···587 mat.assert_all_states(|s| {
588 for id in 0..3 {
589 assert!(
590+ s.get_caught(mat.uuids[id]).is_some(),
591 "Player {} should be pinged due to the powerup (in {})",
592 id,
593 s.id
+13-13
backend/src/game/state.rs
···13 location::Location,
14 powerups::PowerUpType,
15 settings::{GameSettings, PingStartCondition},
16- PlayerId, UtcDT,
17};
1819#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
20/// An on-map ping of a player
21-pub struct PlayerPing<Id: PlayerId> {
22 /// Location of the ping
23 loc: Location,
24 /// Time the ping happened
···29 pub real_player: Id,
30}
3132-impl<Id: PlayerId> PlayerPing<Id> {
33 pub fn new(loc: Location, display_player: Id, real_player: Id) -> Self {
34 Self {
35 loc,
···4243#[derive(Debug, Clone, Serialize, specta::Type)]
44/// This struct handles all logic regarding state updates
45-pub struct GameState<Id: PlayerId> {
46 /// The id of this player in this game
47 pub id: Id,
48···65 caught_state: HashMap<Id, bool>,
6667 /// A map of the latest global ping results for each player
68- pings: HashMap<Id, PlayerPing<Id>>,
6970 /// Powerup on the map that players can grab. Only one at a time
71 available_powerup: Option<Location>,
···92 shared_random_state: u64,
93}
9495-impl<Id: PlayerId> GameState<Id> {
96 pub fn new(settings: GameSettings, my_id: Id, initial_caught_state: HashMap<Id, bool>) -> Self {
97 let mut rand = ChaCha20Rng::seed_from_u64(settings.random_seed as u64);
98 let increment = rand.random_range(-100..100);
···230 }
231232 /// Add a ping for a specific player
233- pub fn add_ping(&mut self, ping: PlayerPing<Id>) {
234 self.pings.insert(ping.display_player, ping);
235 }
236237 /// Get a ping for a player
238 #[cfg(test)]
239- pub fn get_ping(&self, player: Id) -> Option<&PlayerPing<Id>> {
240 self.pings.get(&player)
241 }
242243 /// Remove a ping from the map
244- pub fn remove_ping(&mut self, player: Id) -> Option<PlayerPing<Id>> {
245 self.pings.remove(&player)
246 }
247248 /// Iterate over all seekers in the game
249- pub fn iter_seekers(&self) -> impl Iterator<Item = Id> + use<'_, Id> {
250 self.caught_state
251 .iter()
252 .filter_map(|(k, v)| if *v { Some(*k) } else { None })
···260 }
261262 /// Iterate over all hiders in the game
263- fn iter_hiders(&self) -> impl Iterator<Item = Id> + use<'_, Id> {
264 self.caught_state
265 .iter()
266 .filter_map(|(k, v)| if !*v { Some(*k) } else { None })
···274 }
275276 /// Create a [PlayerPing] with the latest location saved for the player
277- pub fn create_self_ping(&self) -> Option<PlayerPing<Id>> {
278 self.create_ping(self.id)
279 }
280281 /// Create a [PlayerPing] with the latest location as another player
282- pub fn create_ping(&self, id: Id) -> Option<PlayerPing<Id>> {
283 self.get_loc()
284 .map(|loc| PlayerPing::new(loc.clone(), id, self.id))
285 }
···13 location::Location,
14 powerups::PowerUpType,
15 settings::{GameSettings, PingStartCondition},
16+ Id, UtcDT,
17};
1819#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
20/// An on-map ping of a player
21+pub struct PlayerPing {
22 /// Location of the ping
23 loc: Location,
24 /// Time the ping happened
···29 pub real_player: Id,
30}
3132+impl PlayerPing {
33 pub fn new(loc: Location, display_player: Id, real_player: Id) -> Self {
34 Self {
35 loc,
···4243#[derive(Debug, Clone, Serialize, specta::Type)]
44/// This struct handles all logic regarding state updates
45+pub struct GameState {
46 /// The id of this player in this game
47 pub id: Id,
48···65 caught_state: HashMap<Id, bool>,
6667 /// A map of the latest global ping results for each player
68+ pings: HashMap<Id, PlayerPing>,
6970 /// Powerup on the map that players can grab. Only one at a time
71 available_powerup: Option<Location>,
···92 shared_random_state: u64,
93}
9495+impl GameState {
96 pub fn new(settings: GameSettings, my_id: Id, initial_caught_state: HashMap<Id, bool>) -> Self {
97 let mut rand = ChaCha20Rng::seed_from_u64(settings.random_seed as u64);
98 let increment = rand.random_range(-100..100);
···230 }
231232 /// Add a ping for a specific player
233+ pub fn add_ping(&mut self, ping: PlayerPing) {
234 self.pings.insert(ping.display_player, ping);
235 }
236237 /// Get a ping for a player
238 #[cfg(test)]
239+ pub fn get_ping(&self, player: Id) -> Option<&PlayerPing> {
240 self.pings.get(&player)
241 }
242243 /// Remove a ping from the map
244+ pub fn remove_ping(&mut self, player: Id) -> Option<PlayerPing> {
245 self.pings.remove(&player)
246 }
247248 /// Iterate over all seekers in the game
249+ pub fn iter_seekers(&self) -> impl Iterator<Item = Id> + use<'_> {
250 self.caught_state
251 .iter()
252 .filter_map(|(k, v)| if *v { Some(*k) } else { None })
···260 }
261262 /// Iterate over all hiders in the game
263+ fn iter_hiders(&self) -> impl Iterator<Item = Id> + use<'_> {
264 self.caught_state
265 .iter()
266 .filter_map(|(k, v)| if !*v { Some(*k) } else { None })
···274 }
275276 /// Create a [PlayerPing] with the latest location saved for the player
277+ pub fn create_self_ping(&self) -> Option<PlayerPing> {
278 self.create_ping(self.id)
279 }
280281 /// Create a [PlayerPing] with the latest location as another player
282+ pub fn create_ping(&self, id: Id) -> Option<PlayerPing> {
283 self.get_loc()
284 .map(|loc| PlayerPing::new(loc.clone(), id, self.id))
285 }
···1+use super::events::GameEvent;
23+pub trait Transport {
4 /// Receive an event
5+ async fn receive_message(&self) -> Option<GameEvent>;
6 /// Send an event
7+ async fn send_message(&self, msg: GameEvent);
8}
+7-9
backend/src/lib.rs
···67use std::{sync::Arc, time::Duration};
89-use game::{Game as BaseGame, GameSettings, GameState as BaseGameState};
10use lobby::{Lobby, LobbyState, StartGameInfo};
11use location::TauriLocation;
12use profile::PlayerProfile;
···18use transport::MatchboxTransport;
19use uuid::Uuid;
2021-type Game = BaseGame<Uuid, TauriLocation, MatchboxTransport>;
2223enum AppState {
24 Setup,
···241242// AppScreen::Game COMMANDS
243244-type AppGameState = BaseGameState<Uuid>;
245-246#[tauri::command]
247#[specta::specta]
248/// (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> {
250 let state = state.read().await;
251 if let AppState::Game(game) = &*state {
252 game.mark_caught().await;
···260#[specta::specta]
261/// (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
262/// the powerup. Returns the new game state after rolling for the powerup
263-async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result<AppGameState> {
264 let state = state.read().await;
265 if let AppState::Game(game) = &*state {
266 game.get_powerup().await;
···274#[specta::specta]
275/// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
276/// player has none. Returns the updated game state
277-async fn use_powerup(state: State<'_, AppStateHandle>) -> Result<AppGameState> {
278 let state = state.read().await;
279 if let AppState::Game(game) = &*state {
280 game.use_powerup().await;
···298 switch_teams,
299 host_start_game,
300 mark_caught,
301- // grab_powerup,
302- // use_powerup,
303 ]);
304305 #[cfg(debug_assertions)]
···67use std::{sync::Arc, time::Duration};
89+use game::{Game as BaseGame, GameSettings, GameState};
10use lobby::{Lobby, LobbyState, StartGameInfo};
11use location::TauriLocation;
12use profile::PlayerProfile;
···18use transport::MatchboxTransport;
19use uuid::Uuid;
2021+type Game = BaseGame<TauriLocation, MatchboxTransport>;
2223enum AppState {
24 Setup,
···241242// AppScreen::Game COMMANDS
24300244#[tauri::command]
245#[specta::specta]
246/// (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state
247+async fn mark_caught(state: State<'_, AppStateHandle>) -> Result<GameState> {
248 let state = state.read().await;
249 if let AppState::Game(game) = &*state {
250 game.mark_caught().await;
···258#[specta::specta]
259/// (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
260/// the powerup. Returns the new game state after rolling for the powerup
261+async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result<GameState> {
262 let state = state.read().await;
263 if let AppState::Game(game) = &*state {
264 game.get_powerup().await;
···272#[specta::specta]
273/// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
274/// player has none. Returns the updated game state
275+async fn use_powerup(state: State<'_, AppStateHandle>) -> Result<GameState> {
276 let state = state.read().await;
277 if let AppState::Game(game) = &*state {
278 game.use_powerup().await;
···296 switch_teams,
297 host_start_game,
298 mark_caught,
299+ grab_powerup,
300+ use_powerup,
301 ]);
302303 #[cfg(debug_assertions)]
+4-4
backend/src/transport.rs
···14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub enum TransportMessage {
16 /// Message related to the actual game
17- Game(GameEvent<Uuid>),
18 /// Message related to the pre-game lobby
19 Lobby(LobbyMessage),
20 /// Internal message when peer connects
···154 }
155}
156157-impl Transport<Uuid> for MatchboxTransport {
158- async fn receive_message(&self) -> Option<GameEvent<Uuid>> {
159 self.recv_transport_message()
160 .await
161 .and_then(|(_, msg)| match msg {
···164 })
165 }
166167- async fn send_message(&self, msg: GameEvent<Uuid>) {
168 let msg = TransportMessage::Game(msg);
169 self.send_transport_message(None, msg).await;
170 }
···14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub enum TransportMessage {
16 /// Message related to the actual game
17+ Game(GameEvent),
18 /// Message related to the pre-game lobby
19 Lobby(LobbyMessage),
20 /// Internal message when peer connects
···154 }
155}
156157+impl Transport for MatchboxTransport {
158+ async fn receive_message(&self) -> Option<GameEvent> {
159 self.recv_transport_message()
160 .await
161 .and_then(|(_, msg)| match msg {
···164 })
165 }
166167+ async fn send_message(&self, msg: GameEvent) {
168 let msg = TransportMessage::Game(msg);
169 self.send_transport_message(None, msg).await;
170 }
···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+}