Live location tracking and playback for the game "manhunt"

Profile Picture Component

bwc9876.dev 63fb8643 837404c7

verified
+117 -7
+43 -7
frontend/src/components/MenuScreen.tsx
··· 1 - import React from "react"; 1 + import React, { useCallback, useEffect, useState } from "react"; 2 2 import "@fontsource/bungee"; 3 3 import { IconBuildingBroadcastTowerFilled, IconHexagonPlusFilled, IconClockFilled } from "@tabler/icons-react"; 4 - import { GameSettings } from "@/bindings"; 4 + import { commands, GameSettings, PlayerProfile } from "@/bindings"; 5 + import ProfilePicture from "./ProfilePicture"; 5 6 6 7 // Temp settings for now. 7 8 const settings: GameSettings = { ··· 21 22 ] 22 23 }; 23 24 25 + const defaultProfile: PlayerProfile = { 26 + display_name: "", 27 + pfp_base64: null, 28 + }; 29 + 24 30 export default function MenuScreen() { 31 + const [profile, setProfile] = useState<PlayerProfile>(defaultProfile); 25 32 26 - const name = "Jeff"; 33 + useEffect(() => { 34 + let cancel = false; 35 + commands.getProfile().then((profile) => { 36 + if (!cancel) { 37 + setProfile(profile); 38 + } 39 + }); 40 + return () => { 41 + cancel = true; 42 + }; 43 + }, [setProfile]); 44 + 45 + const startLobby = useCallback(() => { 46 + commands.startLobby(null, settings); 47 + }, []); 48 + 49 + const joinLobby = useCallback(() => { 50 + const code = window.prompt("Enter join code"); 51 + if (!code) { 52 + return; 53 + } 54 + const cleanedCode = code.toLowerCase().trim(); 55 + commands.checkRoomCode(cleanedCode).then((valid) => { 56 + if (valid) { 57 + commands.startLobby(cleanedCode, settings); 58 + } else { 59 + window.alert("Invalid Join Code"); 60 + } 61 + }); 62 + }, []); 27 63 28 64 return <> 29 65 <header> 30 - <img alt="Profile Picture" width={256} height={256} /> 31 - {name} 66 + <ProfilePicture fallbackName={profile.display_name} src={profile.pfp_base64} /> 67 + {profile.display_name} 32 68 </header> 33 69 <main> 34 - <button> 70 + <button onClick={startLobby}> 35 71 <IconBuildingBroadcastTowerFilled size="5em" /> 36 72 Start Lobby 37 73 </button> 38 - <button> 74 + <button onClick={joinLobby}> 39 75 <IconHexagonPlusFilled size="2.5em" /> 40 76 Join Lobby 41 77 </button>
+39
frontend/src/components/ProfilePicture.tsx
··· 1 + import React from "react"; 2 + import fallbackPicture from "@/default-pfp.png"; 3 + 4 + export type ProfilePictureProps = { 5 + fallbackName: string; 6 + src: string | null; 7 + decoration?: "hider" | "seeker"; 8 + }; 9 + 10 + const hashName = (str: string, seed = 3) => { 11 + let h1 = 0xdeadbeef ^ seed, 12 + h2 = 0x41c6ce57 ^ seed; 13 + for (let i = 0, ch; i < str.length; i++) { 14 + ch = str.charCodeAt(i); 15 + h1 = Math.imul(h1 ^ ch, 2654435761); 16 + h2 = Math.imul(h2 ^ ch, 1597334677); 17 + } 18 + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); 19 + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); 20 + return 4294967296 * (2097151 & h2) + (h1 >>> 0); 21 + }; 22 + 23 + export default function ProfilePicture({ fallbackName, src, decoration }: ProfilePictureProps) { 24 + const fallback = src ?? fallbackPicture; 25 + const hueshift = hashName(fallbackName) % 360; 26 + 27 + const style = src === null ? { 28 + filter: `hue-rotate(${hueshift}deg)`, 29 + } : undefined; 30 + 31 + const className = "pfp" + (decoration !== undefined ? ` ${decoration}` : ""); 32 + 33 + const fallbackInitial = src === null ? (fallbackName[0] ?? "?") : undefined; 34 + 35 + return <span data-initial={fallbackInitial} className={className}> 36 + <img width={256} height={256} style={style} alt="Profile Picture" src={fallback} /> 37 + </span>; 38 + } 39 +
frontend/src/default-pfp.png

This is a binary file and will not be displayed.

frontend/src/evil-cow.png

This is a binary file and will not be displayed.

+35
frontend/src/style.css
··· 98 98 } 99 99 } 100 100 } 101 + 102 + 103 + span.pfp { 104 + border-radius: 50%; 105 + position: relative; 106 + display: flex; 107 + align-items: center; 108 + justify-content: center; 109 + 110 + box-sizing: border-box; 111 + border-style: solid; 112 + border-width: 4px; 113 + border-color: #0000; 114 + 115 + &[data-initial]::after { 116 + content: attr(data-initial); 117 + color: white; 118 + filter: drop-shadow(0 0 4px black); 119 + position: absolute; 120 + display: flex; 121 + align-items: center; 122 + justify-content: center; 123 + } 124 + 125 + &.seeker { 126 + border-color: #c67; 127 + } 128 + 129 + &.hider { 130 + border-color: #67c; 131 + } 132 + } 133 + 134 + 135 +