309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
import React, { useState, useEffect, useCallback, useContext, useMemo } from 'react';
|
|
import Paper from '@mui/material/Paper';
|
|
import List from '@mui/material/List';
|
|
import './PlayerList.css';
|
|
import { MediaControl, MediaAgent, Peer } from './MediaControl';
|
|
import { PlayerColor } from './PlayerColor';
|
|
import Box from '@mui/material/Box';
|
|
import { GlobalContext } from './GlobalContext';
|
|
import { styles } from './Styles';
|
|
|
|
type Player = {
|
|
name: string;
|
|
session_id: string;
|
|
live: boolean;
|
|
local: boolean /* Client side variable */;
|
|
protected?: boolean;
|
|
has_media?: boolean; // Whether this Player provides audio/video streams
|
|
color?: string;
|
|
bot_run_id?: string;
|
|
bot_provider_id?: string;
|
|
bot_instance_id?: string; // For bot instances
|
|
muted?: boolean;
|
|
video_on?: boolean;
|
|
};
|
|
|
|
const PlayerList: React.FC = () => {
|
|
const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
|
const [players, setPlayers] = useState<Player[]>([]);
|
|
const [player, setPlayer] = useState<Player | null>(null);
|
|
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
|
const [gameState, setGameState] = useState<string | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
console.log('player-list - Mounted - requesting fields');
|
|
if (sendJsonMessage) {
|
|
sendJsonMessage({
|
|
type: 'get',
|
|
fields: ['participants'],
|
|
});
|
|
}
|
|
}, [sendJsonMessage]);
|
|
|
|
const sortPlayers = useCallback(
|
|
(A: any, B: any) => {
|
|
if (!session) {
|
|
return 0;
|
|
}
|
|
/* active Player first */
|
|
if (A.name === session.name) {
|
|
return -1;
|
|
}
|
|
if (B.name === session.name) {
|
|
return +1;
|
|
}
|
|
/* Sort active Players first */
|
|
if (A.name && !B.name) {
|
|
return -1;
|
|
}
|
|
if (B.name && !A.name) {
|
|
return +1;
|
|
}
|
|
/* Otherwise, sort by color */
|
|
if (A.color && B.color) {
|
|
return A.color.localeCompare(B.color);
|
|
}
|
|
return 0;
|
|
},
|
|
[session]
|
|
);
|
|
|
|
// Use the WebSocket hook for room events with automatic reconnection
|
|
useEffect(() => {
|
|
if (!lastJsonMessage) {
|
|
return;
|
|
}
|
|
const data: any = lastJsonMessage;
|
|
switch (data.type) {
|
|
case 'game-update': {
|
|
console.log(`player-list - game-update:`, data.update);
|
|
|
|
// Track game state if provided
|
|
if ('state' in data.update) {
|
|
setGameState(data.update.state);
|
|
}
|
|
|
|
// Handle participants list
|
|
if ('participants' in data.update && data.update.participants) {
|
|
const participantsList: Player[] = data.update.participants;
|
|
console.log(`player-list - participants:`, participantsList);
|
|
|
|
participantsList.forEach(player => {
|
|
player.local = player.session_id === session?.id;
|
|
if (player.local) {
|
|
setPlayer(player);
|
|
}
|
|
});
|
|
participantsList.sort(sortPlayers);
|
|
console.log(`player-list - sorted participants:`, participantsList);
|
|
setPlayers(participantsList);
|
|
|
|
// Initialize peers with remote mute/video state
|
|
setPeers(prevPeers => {
|
|
const updated: Record<string, Peer> = { ...prevPeers };
|
|
participantsList.forEach(player => {
|
|
// Only update remote peers, never overwrite local peer object
|
|
if (!player.local && updated[player.session_id]) {
|
|
updated[player.session_id] = {
|
|
...updated[player.session_id],
|
|
muted: player.muted ?? false,
|
|
video_on: player.video_on ?? true,
|
|
};
|
|
}
|
|
});
|
|
return updated;
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'peer_state_update': {
|
|
// Update peer state in peers, but do not override local mute
|
|
setPeers(prevPeers => {
|
|
const updated = { ...prevPeers };
|
|
const peerId = data.data?.peer_id || data.peer_id;
|
|
if (peerId && updated[peerId]) {
|
|
updated[peerId] = {
|
|
...updated[peerId],
|
|
muted: data.data?.muted ?? data.muted,
|
|
video_on: data.data?.video_on ?? data.video_on,
|
|
};
|
|
}
|
|
return updated;
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
// console.log(`player-list - ignoring message: ${data.type}`);
|
|
break;
|
|
}
|
|
}, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]);
|
|
|
|
// Compute which colors are already taken
|
|
const availableColors = useMemo(() => {
|
|
const assignedColors = new Set(players.filter(p => p.color !== 'unassigned').map(p => p.color));
|
|
return ['O', 'R', 'W', 'B'].filter(color => !assignedColors.has(color));
|
|
}, [players]);
|
|
|
|
useEffect(() => {
|
|
if (players.length !== 0 || !sendJsonMessage) {
|
|
return;
|
|
}
|
|
// Request participants list
|
|
sendJsonMessage({
|
|
type: 'get',
|
|
fields: ['participants'],
|
|
});
|
|
}, [players, sendJsonMessage]);
|
|
|
|
return (
|
|
<Box sx={{ position: 'relative', width: '100%' }}>
|
|
<Paper
|
|
className={`PlayerList Medium`}
|
|
sx={{
|
|
maxWidth: { xs: '100%', sm: 500 },
|
|
p: { xs: 1, sm: 2 },
|
|
m: { xs: 0, sm: 2 },
|
|
}}
|
|
>
|
|
<MediaAgent {...{ session, peers, setPeers }} />
|
|
<List className="PlayerSelector Players">
|
|
{players
|
|
.filter(p => p.color && p.color !== 'unassigned')
|
|
.map(player => {
|
|
const peerObj = peers[player.session_id] || peers[player.name];
|
|
const playerStyle = player.color !== 'unassigned' ? styles[player.color] : {};
|
|
return (
|
|
<Box
|
|
key={player.session_id}
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
p: 1,
|
|
borderRadius: '0.25rem',
|
|
...playerStyle,
|
|
}}
|
|
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`}
|
|
>
|
|
<Box
|
|
style={{
|
|
display: 'flex-wrap',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
|
|
{player.bot_instance_id && <>🤖</>}
|
|
{player.name ? player.name : player.session_id}
|
|
</Box>
|
|
{player.name && !player.live && <Box className="NoNetwork"></Box>}
|
|
</Box>
|
|
{player.name && player.live ? (
|
|
<MediaControl
|
|
key={player.session_id}
|
|
peer={peerObj}
|
|
isSelf={player.local}
|
|
sendJsonMessage={player.local ? sendJsonMessage : undefined}
|
|
remoteAudioMuted={peerObj?.muted}
|
|
remoteVideoOff={peerObj?.video_on === false}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="Video fade-in"
|
|
style={{
|
|
background: '#333',
|
|
color: '#fff',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: '100%',
|
|
height: '100%',
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
💬 Chat Only
|
|
</div>
|
|
)}
|
|
</Box>
|
|
);
|
|
})}
|
|
</List>
|
|
{gameState === 'lobby' && (
|
|
<Paper sx={{ p: 0.5, mt: 0.5, backgroundColor: '#f9f9f9' }}>
|
|
<div style={{ marginBottom: 6, fontSize: '0.9em' }}>
|
|
{player && player.color !== 'unassigned' ? 'Change' : 'Pick'} your color:
|
|
</div>
|
|
<Box style={{ display: 'flex', gap: 8 }}>
|
|
{availableColors.map(c => {
|
|
return (
|
|
<Box
|
|
key={c}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
cursor: 'pointer',
|
|
}}
|
|
onClick={() => {
|
|
sendJsonMessage({ type: 'set', field: 'color', value: c });
|
|
}}
|
|
>
|
|
<PlayerColor color={c} />
|
|
</Box>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Paper>
|
|
)}
|
|
<Paper>
|
|
<List className="PlayerSelector Observers">
|
|
{players
|
|
.filter(p => !p.color || p.color === 'unassigned')
|
|
.map(player => {
|
|
const peerObj = peers[player.session_id] || peers[player.name];
|
|
return (
|
|
<Box
|
|
key={player.session_id}
|
|
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
|
|
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`}
|
|
data-selectable={player.local && gameState === 'lobby'}
|
|
>
|
|
<Box
|
|
style={{
|
|
display: 'flex-wrap',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
|
|
{player.bot_instance_id && <>🤖</>}
|
|
{player.name ? player.name : player.session_id}
|
|
</Box>
|
|
{player.name && !player.live && <Box className="NoNetwork"></Box>}
|
|
</Box>
|
|
|
|
{/* Show media control if available */}
|
|
{player.name &&
|
|
player.live &&
|
|
peerObj &&
|
|
(player.local || player.has_media !== false) && (
|
|
<MediaControl
|
|
key={player.session_id}
|
|
peer={peerObj}
|
|
isSelf={player.local}
|
|
sendJsonMessage={player.local ? sendJsonMessage : undefined}
|
|
remoteAudioMuted={peerObj?.muted}
|
|
remoteVideoOff={peerObj?.video_on === false}
|
|
/>
|
|
)}
|
|
</Box>
|
|
);
|
|
})}
|
|
</List>
|
|
</Paper>
|
|
</Paper>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { PlayerList };
|