Live location tracking and playback for the game "manhunt"
at ben/frontend 209 lines 6.7 kB view raw
1import React, { useEffect, useState } from "react"; 2import { commands, GameSettings, LobbyState, PingStartCondition, PlayerProfile } from "@/bindings"; 3import { useTauriEvent } from "@/lib/hooks"; 4import ProfilePicture, { iconForDecor, ProfileDecor } from "./ProfilePicture"; 5import { defaultSettings } from "./MenuScreen"; 6import { ask } from "@tauri-apps/plugin-dialog"; 7import { 8 IconArrowBigLeftLinesFilled, 9 IconCircleCheckFilled, 10 IconCircleDashedPlus, 11 IconFlagFilled, 12 IconInfoCircleFilled, 13 IconSettingsFilled 14} from "@tabler/icons-react"; 15import LoadingCover from "./LoadingCover"; 16import GameSettingsModal from "./game-settings/GameSettingsModal"; 17 18function ProfileList({ 19 profiles, 20 decoration, 21 emptyText 22}: { 23 profiles: [string, PlayerProfile][]; 24 decoration: ProfileDecor; 25 emptyText: string; 26}) { 27 return ( 28 <div className="pfp-list"> 29 {profiles.length === 0 && <small>{emptyText}</small>} 30 {profiles.map(([k, p]) => ( 31 <ProfilePicture 32 key={k} 33 decoration={decoration} 34 src={p.pfp_base64} 35 fallbackName={p.display_name} 36 /> 37 ))} 38 </div> 39 ); 40} 41 42function TeamButton({ 43 onClick, 44 active, 45 deco, 46 text 47}: { 48 onClick: () => void; 49 active: boolean; 50 deco: ProfileDecor; 51 text: string; 52}) { 53 const Icon = iconForDecor(deco); 54 55 return ( 56 <button onClick={onClick} className={`team-button ${deco}`}> 57 <Icon /> 58 {active ? "You're On" : "Join"} {text} 59 {active ? <IconCircleCheckFilled /> : <IconCircleDashedPlus />} 60 </button> 61 ); 62} 63 64const initLobbyState: LobbyState = { 65 profiles: {}, 66 join_code: "", 67 teams: {}, 68 self_id: "", 69 is_host: false, 70 settings: defaultSettings() 71}; 72 73export default function LobbyScreen() { 74 const [lobbyState, setLobbyState] = useState(initLobbyState); 75 const [loadingCover, setLoadingCover] = useState(true); 76 const [settingOpen, setSettingsOpen] = useState(false); 77 78 useEffect(() => { 79 let cancel = false; 80 const clear = setTimeout(() => { 81 commands.getLobbyState().then((state) => { 82 if (!cancel) { 83 setLobbyState(state); 84 setLoadingCover(false); 85 } 86 }); 87 }, 300); 88 return () => { 89 cancel = true; 90 clearTimeout(clear); 91 }; 92 }, [setLobbyState]); 93 94 useTauriEvent("lobbyStateUpdate", () => { 95 commands.getLobbyState().then((state) => { 96 setLobbyState(state); 97 }); 98 }); 99 100 const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [ 101 string, 102 PlayerProfile 103 ][]; 104 105 const seekers = profiles.filter(([id, _]) => lobbyState.teams[id] ?? false); 106 const hiders = profiles.filter(([id, _]) => !(lobbyState.teams[id] ?? false)); 107 108 const isSeeker = lobbyState.teams[lobbyState.self_id] ?? false; 109 110 const onLeaveLobby = () => { 111 // Don't prompt since the lobby is empty (besides us obv) 112 // <= even though it should never be 0... hopefully.... 113 if (lobbyState.is_host && profiles.length <= 1) { 114 commands.quitToMenu(); 115 return; 116 } 117 const hostMsg = lobbyState.is_host ? " You are the host so this will cancel the lobby" : ""; 118 119 const msg = `Are you sure you want to leave this lobby?${hostMsg}`; 120 121 ask(msg, { 122 title: "Leave Lobby", 123 kind: lobbyState.is_host ? "warning" : "info" 124 }).then((choice) => { 125 if (choice) { 126 commands.quitToMenu(); 127 } 128 }); 129 }; 130 131 const canStart = lobbyState.is_host && seekers.length !== 0 && hiders.length !== 0; 132 133 const onStartGame = () => { 134 if (canStart) { 135 setLoadingCover(true); 136 ask( 137 "Are you ready to start the game? New players will be unable to join. The hiding timer will begin the moment you press start." 138 ) 139 .then((choice) => { 140 if (choice) { 141 commands.hostStartGame().finally(() => { 142 setLoadingCover(false); 143 }); 144 } else { 145 setLoadingCover(false); 146 } 147 }) 148 .catch(() => { 149 setLoadingCover(false); 150 }); 151 } 152 }; 153 154 const onOpenSettings = () => { 155 setSettingsOpen(true); 156 }; 157 158 const onSettingsSave = (settings: GameSettings) => { 159 commands.hostUpdateSettings(settings); 160 setSettingsOpen(false); 161 }; 162 163 return ( 164 <> 165 <LoadingCover show={loadingCover} /> 166 {settingOpen && ( 167 <GameSettingsModal gameSettings={lobbyState.settings} onSave={onSettingsSave} /> 168 )} 169 <header> 170 <span className="grow">Lobby</span> 171 <span>Join: {lobbyState.join_code}</span> 172 </header> 173 <main className="lobby"> 174 <ProfileList profiles={seekers} decoration="seeker" emptyText="No Seekers" /> 175 <TeamButton 176 onClick={() => commands.switchTeams(true)} 177 active={isSeeker} 178 text="Seekers" 179 deco="seeker" 180 /> 181 <div className="frame"> 182 <button onClick={onLeaveLobby} className="fab tl"> 183 <IconArrowBigLeftLinesFilled size="1.5em" /> 184 Leave 185 </button> 186 {lobbyState.is_host && ( 187 <button onClick={onOpenSettings} className="fab bl"> 188 <IconSettingsFilled size="1.5em" /> 189 Rules 190 </button> 191 )} 192 {lobbyState.is_host && ( 193 <button disabled={!canStart} onClick={onStartGame} className="fab br"> 194 <IconFlagFilled size="1.5em" /> 195 Start 196 </button> 197 )} 198 </div> 199 <TeamButton 200 onClick={() => commands.switchTeams(false)} 201 active={!isSeeker} 202 text="Hiders" 203 deco="hider" 204 /> 205 <ProfileList profiles={hiders} decoration="hider" emptyText="No Hiders" /> 206 </main> 207 </> 208 ); 209}