tangled
alpha
login
or
join now
bwc9876.dev
/
manhunt-app
0
fork
atom
Live location tracking and playback for the game "manhunt"
0
fork
atom
overview
issues
pulls
1
pipelines
Lobby screen mockup
bwc9876.dev
1 week ago
b218c8d3
63fb8643
verified
This commit was signed with the committer's
known signature
.
bwc9876.dev
SSH Key Fingerprint:
SHA256:DanMEP/RNlSC7pAVbnXO6wzQV00rqyKj053tz4uH5gQ=
+121
-69
5 changed files
expand all
collapse all
unified
split
Cargo.lock
frontend
src
components
LobbyScreen.tsx
MenuScreen.tsx
ProfilePicture.tsx
style.css
+6
-6
Cargo.lock
···
706
706
707
707
[[package]]
708
708
name = "bumpalo"
709
709
-
version = "3.19.1"
709
709
+
version = "3.20.1"
710
710
source = "registry+https://github.com/rust-lang/crates.io-index"
711
711
-
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
711
711
+
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
712
712
713
713
[[package]]
714
714
name = "byte-unit"
···
4407
4407
"quinn-udp",
4408
4408
"rustc-hash",
4409
4409
"rustls 0.23.36",
4410
4410
-
"socket2 0.6.2",
4410
4410
+
"socket2 0.5.10",
4411
4411
"thiserror 2.0.18",
4412
4412
"tokio",
4413
4413
"tracing",
···
4445
4445
"cfg_aliases",
4446
4446
"libc",
4447
4447
"once_cell",
4448
4448
-
"socket2 0.6.2",
4448
4448
+
"socket2 0.5.10",
4449
4449
"tracing",
4450
4450
-
"windows-sys 0.60.2",
4450
4450
+
"windows-sys 0.52.0",
4451
4451
]
4452
4452
4453
4453
[[package]]
···
5054
5054
"security-framework",
5055
5055
"security-framework-sys",
5056
5056
"webpki-root-certs",
5057
5057
-
"windows-sys 0.61.2",
5057
5057
+
"windows-sys 0.52.0",
5058
5058
]
5059
5059
5060
5060
[[package]]
+59
-47
frontend/src/components/LobbyScreen.tsx
···
1
1
-
import React from "react";
2
2
-
import { commands } from "@/bindings";
3
3
-
import { sharedSwrConfig, useTauriEvent } from "@/lib/hooks";
4
4
-
import useSWR from "swr";
1
1
+
import React, { useEffect, useState } from "react";
2
2
+
import { commands, LobbyState, PlayerProfile } from "@/bindings";
3
3
+
import { useTauriEvent } from "@/lib/hooks";
4
4
+
import ProfilePicture, { ProfileDecor } from "./ProfilePicture";
5
5
+
import { tempSettings } from "./MenuScreen";
6
6
+
import { IconCircleCheckFilled, IconCircleDashedPlus } from "@tabler/icons-react";
7
7
+
8
8
+
function ProfileList({ profiles, decoration }: { profiles: [string, PlayerProfile][], decoration: ProfileDecor }) {
9
9
+
return <div className="lobby-pfps">
10
10
+
{profiles.map(([k, p]) => <ProfilePicture key={k} decoration={decoration} src={p.pfp_base64} fallbackName={p.display_name} />)}
11
11
+
</div>;
12
12
+
}
13
13
+
14
14
+
function TeamButton({ onClick, active, deco, text }: { onClick: () => void, active: boolean, deco: ProfileDecor, text: string }) {
15
15
+
return <button onClick={onClick} className={`team-button ${deco}`}>
16
16
+
{active ? "You're On" : "Join"} {text}
17
17
+
{active ? <IconCircleCheckFilled /> : <IconCircleDashedPlus />}
18
18
+
</button>;
19
19
+
}
20
20
+
21
21
+
const initLobbyState: LobbyState = {
22
22
+
profiles: {},
23
23
+
join_code: "",
24
24
+
teams: {},
25
25
+
self_id: "",
26
26
+
is_host: false,
27
27
+
settings: tempSettings,
28
28
+
};
5
29
6
30
export default function LobbyScreen() {
7
7
-
const { data: lobbyState, mutate } = useSWR(
8
8
-
"fetch-lobby-state",
9
9
-
commands.getLobbyState,
10
10
-
sharedSwrConfig
11
11
-
);
31
31
+
const [lobbyState, setLobbyState] = useState(initLobbyState);
32
32
+
33
33
+
useEffect(() => {
34
34
+
let cancel = false;
35
35
+
commands.getLobbyState().then((state) => {
36
36
+
if (!cancel) {
37
37
+
setLobbyState(state);
38
38
+
}
39
39
+
});
40
40
+
return () => {
41
41
+
cancel = true;
42
42
+
};
43
43
+
}, [setLobbyState]);
12
44
13
45
useTauriEvent("lobbyStateUpdate", () => {
14
14
-
mutate();
46
46
+
commands.getLobbyState().then((state) => {
47
47
+
setLobbyState(state);
48
48
+
});
15
49
});
16
50
17
17
-
const setSeeker = async (seeker: boolean) => {
18
18
-
await commands.switchTeams(seeker);
19
19
-
};
51
51
+
const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [string, PlayerProfile][];
20
52
21
21
-
const startGame = async () => {
22
22
-
await commands.hostStartGame();
23
23
-
};
53
53
+
const seekers = profiles.filter(([id, _]) => lobbyState.teams[id] ?? false);
54
54
+
const hiders = profiles.filter(([id, _]) => !(lobbyState.teams[id] ?? false));
24
55
25
25
-
const quit = async () => {
26
26
-
await commands.quitToMenu();
27
27
-
};
28
28
-
29
29
-
if (lobbyState.self_id === null) {
30
30
-
return <h2>Connecting to Lobby...</h2>;
31
31
-
}
56
56
+
const isSeeker = lobbyState.teams[lobbyState.self_id] ?? false;
32
57
33
58
return (
34
59
<>
35
35
-
<h2>Join Code: {lobbyState.join_code}</h2>
36
36
-
37
37
-
{lobbyState.is_host && <button onClick={startGame}>Start Game</button>}
38
38
-
39
39
-
<button onClick={() => setSeeker(true)}>Become Seeker</button>
40
40
-
<button onClick={() => setSeeker(false)}>Become Hider</button>
41
41
-
42
42
-
<h3>Seekers</h3>
43
43
-
<ul>
44
44
-
{Object.keys(lobbyState.teams)
45
45
-
.filter((k) => lobbyState.teams[k])
46
46
-
.map((key) => (
47
47
-
<li key={key}>{lobbyState.profiles[key]?.display_name ?? key}</li>
48
48
-
))}
49
49
-
</ul>
50
50
-
<h3>Hiders</h3>
51
51
-
<ul>
52
52
-
{Object.keys(lobbyState.teams)
53
53
-
.filter((k) => !lobbyState.teams[k])
54
54
-
.map((key) => (
55
55
-
<li key={key}>{lobbyState.profiles[key]?.display_name ?? key}</li>
56
56
-
))}
57
57
-
</ul>
58
58
-
<button onClick={quit}>Quit to Menu</button>
60
60
+
<header>
61
61
+
<span className="grow">Lobby</span>
62
62
+
<span>Join: {lobbyState.join_code}</span>
63
63
+
</header>
64
64
+
<main>
65
65
+
<ProfileList profiles={seekers} decoration="seeker" />
66
66
+
<TeamButton onClick={() => commands.switchTeams(true)} active={isSeeker} text="Seekers" deco="seeker" />
67
67
+
<div className="map" />
68
68
+
<TeamButton onClick={() => commands.switchTeams(false)} active={!isSeeker} text="Hiders" deco="hider" />
69
69
+
<ProfileList profiles={hiders} decoration="hider" />
70
70
+
</main>
59
71
</>
60
72
);
61
73
}
+5
-5
frontend/src/components/MenuScreen.tsx
···
5
5
import ProfilePicture from "./ProfilePicture";
6
6
7
7
// Temp settings for now.
8
8
-
const settings: GameSettings = {
8
8
+
export const tempSettings: GameSettings = {
9
9
random_seed: 21341234,
10
10
hiding_time_seconds: 10,
11
11
ping_start: "Instant",
···
43
43
}, [setProfile]);
44
44
45
45
const startLobby = useCallback(() => {
46
46
-
commands.startLobby(null, settings);
46
46
+
commands.startLobby(null, tempSettings);
47
47
}, []);
48
48
49
49
const joinLobby = useCallback(() => {
···
51
51
if (!code) {
52
52
return;
53
53
}
54
54
-
const cleanedCode = code.toLowerCase().trim();
54
54
+
const cleanedCode = code.toUpperCase().trim();
55
55
commands.checkRoomCode(cleanedCode).then((valid) => {
56
56
if (valid) {
57
57
-
commands.startLobby(cleanedCode, settings);
57
57
+
commands.startLobby(cleanedCode, tempSettings);
58
58
} else {
59
59
window.alert("Invalid Join Code");
60
60
}
···
66
66
<ProfilePicture fallbackName={profile.display_name} src={profile.pfp_base64} />
67
67
{profile.display_name}
68
68
</header>
69
69
-
<main>
69
69
+
<main className="menu">
70
70
<button onClick={startLobby}>
71
71
<IconBuildingBroadcastTowerFilled size="5em" />
72
72
Start Lobby
+3
-1
frontend/src/components/ProfilePicture.tsx
···
1
1
import React from "react";
2
2
import fallbackPicture from "@/default-pfp.png";
3
3
4
4
+
export type ProfileDecor = "hider" | "seeker";
5
5
+
4
6
export type ProfilePictureProps = {
5
7
fallbackName: string;
6
8
src: string | null;
7
7
-
decoration?: "hider" | "seeker";
9
9
+
decoration?: ProfileDecor;
8
10
};
9
11
10
12
const hashName = (str: string, seed = 3) => {
+48
-10
frontend/src/style.css
···
43
43
padding: var(--1);
44
44
gap: var(--1);
45
45
46
46
-
img {
47
47
-
width: 2em;
48
48
-
height: 2em;
49
49
-
background-color: grey;
50
50
-
border-radius: 50%;
46
46
+
.grow {
47
47
+
flex-grow: 1;
51
48
}
52
49
}
53
50
···
56
53
flex-direction: column;
57
54
flex-grow: 1;
58
55
59
59
-
button {
56
56
+
.map {
57
57
+
flex-grow: 1;
58
58
+
background-color: #111;
59
59
+
}
60
60
+
61
61
+
.lobby-pfps {
62
62
+
z-index: 2;
63
63
+
overflow-x: auto;
64
64
+
box-shadow: 0 0 10px #0004;
65
65
+
display: flex;
66
66
+
flex-direction: row;
67
67
+
gap: var(--2);
68
68
+
width: 100%;
69
69
+
font-size: 20pt;
70
70
+
min-height: calc(20pt + var(--2));
71
71
+
padding: var(--small);
72
72
+
}
73
73
+
74
74
+
.team-button {
75
75
+
font-family: "Bungee";
76
76
+
display: flex;
77
77
+
flex-direction: row;
78
78
+
align-items: center;
79
79
+
justify-content: center;
80
80
+
font-size: 16pt;
81
81
+
gap: var(--small);
82
82
+
padding: var(--1) 0;
83
83
+
border: none;
84
84
+
85
85
+
&.hider {
86
86
+
background-color: #67c;
87
87
+
}
88
88
+
89
89
+
&.seeker {
90
90
+
background-color: #c67;
91
91
+
}
92
92
+
}
93
93
+
94
94
+
&.menu button {
60
95
font-family: "Bungee";
61
96
display: flex;
62
97
flex-direction: row;
···
85
120
background-color: #67c;
86
121
margin-top: calc(-1 * var(--8));
87
122
padding-bottom: var(--5);
88
88
-
transform: rotateZ(-2deg) translateX(-6px);
123
123
+
transform: rotateZ(-2deg) translateX(-6px);
89
124
}
90
125
91
126
&:nth-child(3) {
···
112
147
border-width: 4px;
113
148
border-color: #0000;
114
149
150
150
+
img {
151
151
+
width: 2em;
152
152
+
height: 2em;
153
153
+
border-radius: 50%;
154
154
+
}
155
155
+
115
156
&[data-initial]::after {
116
157
content: attr(data-initial);
117
158
color: white;
···
130
171
border-color: #67c;
131
172
}
132
173
}
133
133
-
134
134
-
135
135
-