Live location tracking and playback for the game "manhunt"

Add start game button, lobby screen cleanup

bwc9876.dev 79517e8d aaa78e33

verified
+137 -73
+2 -1
TODO.md
··· 1 1 # TODO 2 2 3 - - [ ] Start on start game and game settings buttons 3 + - [ ] Start on game settings form 4 + - [ ] Setup screen
+48 -9
frontend/src/components/LobbyScreen.tsx
··· 2 2 import { commands, LobbyState, PlayerProfile } from "@/bindings"; 3 3 import { useTauriEvent } from "@/lib/hooks"; 4 4 import ProfilePicture, { iconForDecor, ProfileDecor } from "./ProfilePicture"; 5 - import { tempSettings } from "./MenuScreen"; 5 + import { defaultSettings } from "./MenuScreen"; 6 6 import { ask } from "@tauri-apps/plugin-dialog"; 7 7 import { 8 8 IconArrowBigLeftLinesFilled, 9 9 IconCircleCheckFilled, 10 - IconCircleDashedPlus 10 + IconCircleDashedPlus, 11 + IconFlagFilled 11 12 } from "@tabler/icons-react"; 12 13 import LoadingCover from "./LoadingCover"; 13 14 14 15 function ProfileList({ 15 16 profiles, 16 - decoration 17 + decoration, 18 + emptyText 17 19 }: { 18 20 profiles: [string, PlayerProfile][]; 19 21 decoration: ProfileDecor; 22 + emptyText: string; 20 23 }) { 21 24 return ( 22 - <div className="lobby-pfps"> 25 + <div className="pfp-list"> 26 + {profiles.length === 0 && <small>{emptyText}</small>} 23 27 {profiles.map(([k, p]) => ( 24 28 <ProfilePicture 25 29 key={k} ··· 60 64 teams: {}, 61 65 self_id: "", 62 66 is_host: false, 63 - settings: tempSettings 67 + settings: defaultSettings() 64 68 }; 65 69 66 70 export default function LobbyScreen() { ··· 111 115 }); 112 116 }; 113 117 118 + const canStart = lobbyState.is_host && seekers.length !== 0 && hiders.length !== 0; 119 + 120 + const onStartGame = () => { 121 + if (canStart) { 122 + setLoadingCover(true); 123 + ask( 124 + "Are you ready to start the game? New players will be unable to join. The hiding timer will begin the moment you press start." 125 + ) 126 + .then((choice) => { 127 + if (choice) { 128 + commands.hostStartGame().finally(() => { 129 + setLoadingCover(false); 130 + }); 131 + } else { 132 + setLoadingCover(false); 133 + } 134 + }) 135 + .catch(() => { 136 + setLoadingCover(false); 137 + }); 138 + } 139 + }; 140 + 114 141 return ( 115 142 <> 116 - <LoadingCover text="Fetching Info" show={loadingCover} /> 143 + <LoadingCover show={loadingCover} /> 117 144 <header> 118 145 <span className="grow">Lobby</span> 119 146 <span>Join: {lobbyState.join_code}</span> 120 147 </header> 121 148 <main className="lobby"> 122 - <ProfileList profiles={seekers} decoration="seeker" /> 149 + <ProfileList profiles={seekers} decoration="seeker" emptyText="No Seekers" /> 123 150 <TeamButton 124 151 onClick={() => commands.switchTeams(true)} 125 152 active={isSeeker} ··· 128 155 /> 129 156 <div className="frame"> 130 157 <button onClick={onLeaveLobby} aria-label="Leave Lobby" className="fab left"> 131 - <IconArrowBigLeftLinesFilled size="2em" /> 158 + <IconArrowBigLeftLinesFilled size="1.5em" /> 159 + Leave 132 160 </button> 161 + {lobbyState.is_host && ( 162 + <button 163 + disabled={!canStart} 164 + onClick={onStartGame} 165 + aria-label="Start Game" 166 + className="fab right" 167 + > 168 + <IconFlagFilled size="1.5em" /> 169 + Start 170 + </button> 171 + )} 133 172 </div> 134 173 <TeamButton 135 174 onClick={() => commands.switchTeams(false)} ··· 137 176 text="Hiders" 138 177 deco="hider" 139 178 /> 140 - <ProfileList profiles={hiders} decoration="hider" /> 179 + <ProfileList profiles={hiders} decoration="hider" emptyText="No Hiders" /> 141 180 </main> 142 181 </> 143 182 );
+7 -14
frontend/src/components/MenuScreen.tsx
··· 10 10 import LoadingCover from "./LoadingCover"; 11 11 import { message } from "@tauri-apps/plugin-dialog"; 12 12 13 - // Temp settings for now. 14 - export const tempSettings: GameSettings = { 15 - random_seed: 21341234, 16 - hiding_time_seconds: 10, 13 + export const defaultSettings: () => GameSettings = () => ({ 14 + random_seed: Math.floor(Math.random() * 2 ** 32), 15 + hiding_time_seconds: 60 * 5, 17 16 ping_start: "Instant", 18 17 ping_minutes_interval: 1, 19 18 powerup_start: "Instant", 20 19 powerup_chance: 60, 21 20 powerup_minutes_cooldown: 1, 22 - powerup_locations: [ 23 - { 24 - lat: 0, 25 - long: 0, 26 - heading: null 27 - } 28 - ] 29 - }; 21 + powerup_locations: [] 22 + }); 30 23 31 24 const defaultProfile: PlayerProfile = { 32 25 display_name: "", ··· 50 43 }, [setProfile]); 51 44 52 45 const startLobby = useCallback(() => { 53 - commands.startLobby(null, tempSettings); 46 + commands.startLobby(null, defaultSettings()); 54 47 }, []); 55 48 56 49 const joinLobby = useCallback(() => { ··· 65 58 .checkRoomCode(cleanedCode) 66 59 .then((valid) => { 67 60 if (valid) { 68 - commands.startLobby(cleanedCode, tempSettings).finally(() => { 61 + commands.startLobby(cleanedCode, defaultSettings()).finally(() => { 69 62 setLoadingCover(false); 70 63 }); 71 64 } else {
+80 -49
frontend/src/style.css
··· 66 66 67 67 .map { 68 68 flex-grow: 1; 69 - background-color: #111; 69 + background-color: #211; /* TEMP: Until I set up and actual map */ 70 70 } 71 71 72 - .lobby-pfps { 73 - z-index: 2; 74 - overflow-x: auto; 75 - box-shadow: 0 0 10px #0004; 76 - display: flex; 77 - flex-direction: row; 78 - gap: var(--2); 79 - width: 100%; 80 - font-size: 20pt; 81 - min-height: calc(20pt + var(--2)); 82 - padding: var(--small); 83 - } 72 + &.lobby { 73 + .frame { 74 + flex-grow: 1; 75 + background-color: #aaa; 76 + position: relative; 77 + overflow-y: scroll; 78 + width: 100%; 79 + } 80 + 81 + .pfp-list { 82 + z-index: 4; 83 + overflow-x: auto; 84 + box-shadow: 0 0 10px #0004; 85 + display: flex; 86 + flex-direction: row; 87 + align-items: center; 88 + justify-content: center; 89 + gap: var(--2); 90 + width: 100%; 91 + font-size: 20pt; 92 + height: 5vh; 93 + padding: var(--small) 0; 84 94 85 - .team-button { 86 - display: flex; 87 - flex-direction: row; 88 - align-items: center; 89 - justify-content: center; 90 - font-size: 16pt; 91 - gap: var(--small); 92 - padding: var(--1); 93 - border: none; 95 + small { 96 + font-size: 0.7em; 97 + font-style: italic; 98 + color: #383838ff; 99 + } 94 100 95 - &.hider { 96 - background-color: #67c; 97 - } 101 + .pfp { 102 + transition: transform 175ms linear; 98 103 99 - &.seeker { 100 - background-color: #c67; 104 + @starting-style { 105 + transform: scale(0); 106 + } 107 + } 101 108 } 102 - } 103 109 104 - &.lobby > div.frame { 105 - flex-grow: 1; 106 - background-color: #aaa; 107 - position: relative; 108 - overflow-y: scroll; 109 - width: 100%; 110 - 111 - button.fab { 112 - background-color: black; 113 - color: white; 114 - border-radius: 50%; 115 - box-shadow: 0 0 5px black; 116 - border: none; 110 + .team-button { 117 111 display: flex; 112 + --deco: black; 113 + z-index: 3; 114 + flex-direction: row; 118 115 align-items: center; 119 116 justify-content: center; 120 - font-size: 12pt; 121 - padding: var(--2); 122 - position: absolute; 123 - bottom: var(--small); 117 + box-shadow: 0 0 5px var(--deco); 118 + background-color: var(--deco); 119 + font-size: 16pt; 120 + gap: var(--small); 121 + padding: var(--1) 0; 122 + border: none; 124 123 125 - &.left { 126 - left: var(--small); 124 + &.hider { 125 + --deco: #67c; 127 126 } 128 127 129 - &.right { 130 - right: var(--small); 128 + &.seeker { 129 + --deco: #c67; 131 130 } 132 131 } 133 132 } ··· 267 266 --deco-color: #67c; 268 267 } 269 268 } 269 + 270 + button.fab { 271 + transition: 100ms linear; 272 + transition-property: color, background-color, box-shadow; 273 + background-color: #151515ff; 274 + color: #eee; 275 + border-radius: 100px; 276 + box-shadow: 0 0 6px #222; 277 + border: none; 278 + display: flex; 279 + align-items: center; 280 + justify-content: center; 281 + gap: 0.3em; 282 + font-size: 14pt; 283 + padding: var(--2); 284 + position: absolute; 285 + bottom: var(--small); 286 + 287 + &:disabled { 288 + background-color: #999; 289 + color: #555; 290 + box-shadow: 0 0 5px #4445 inset; 291 + } 292 + 293 + &.left { 294 + left: var(--small); 295 + } 296 + 297 + &.right { 298 + right: var(--small); 299 + } 300 + }