diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts index fac12a2..6ef639b 100644 --- a/backend/src/game/game.gateway.ts +++ b/backend/src/game/game.gateway.ts @@ -105,6 +105,11 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On console.log(`🔌 Client ${client.id} joining WebSocket room ${payload.roomCode}, userId: ${payload.userId}`); client.join(payload.roomCode); + // Логируем всех клиентов в комнате + const roomClients = this.server.sockets.adapter.rooms.get(payload.roomCode); + console.log(`👥 Clients in room ${payload.roomCode}: ${roomClients?.size || 0}`, + Array.from(roomClients || [])); + // Получаем полное состояние для отправки присоединившемуся клиенту const room = (await this.prisma.room.findUnique({ where: { code: payload.roomCode }, @@ -574,7 +579,9 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On }) }; + const roomClients = this.server.sockets.adapter.rooms.get(roomCode); console.log(`📡 Broadcasting gameStateUpdated to room ${roomCode} with ${room.participants.length} participants`); + console.log(`📡 Target clients: ${roomClients?.size || 0}`, Array.from(roomClients || [])); this.server.to(roomCode).emit('gameStateUpdated', fullState); } diff --git a/backend/src/rooms/rooms.service.ts b/backend/src/rooms/rooms.service.ts index 1910c1e..90db513 100644 --- a/backend/src/rooms/rooms.service.ts +++ b/backend/src/rooms/rooms.service.ts @@ -153,10 +153,6 @@ export class RoomsService { // Отправляем событие roomUpdate всем клиентам в комнате if (updatedRoom) { - // Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате - // WebSocket joinRoom может выполняться параллельно с REST API joinRoom - await new Promise(resolve => setTimeout(resolve, 50)); - console.log(`📤 Broadcasting room update for ${updatedRoom.code} with ${updatedRoom.participants.length} participants`); this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom); // Также отправляем gameStateUpdated через broadcastFullState @@ -248,9 +244,6 @@ export class RoomsService { // Отправляем событие roomUpdate всем клиентам в комнате if (updatedRoom) { - // Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате - await new Promise(resolve => setTimeout(resolve, 50)); - this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom); // Также отправляем gameStateUpdated через broadcastFullState await this.gameGateway.broadcastFullState(updatedRoom.code); diff --git a/src/hooks/useRoom.js b/src/hooks/useRoom.js index 93884e7..12106ec 100644 --- a/src/hooks/useRoom.js +++ b/src/hooks/useRoom.js @@ -11,6 +11,12 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => { const [participants, setParticipants] = useState([]); const [requiresPassword, setRequiresPassword] = useState(false); + // Используем ref для onGameStarted, чтобы не пересоздавать обработчики при его изменении + const onGameStartedRef = useRef(onGameStarted); + useEffect(() => { + onGameStartedRef.current = onGameStarted; + }, [onGameStarted]); + // Обработчики событий вынесены наружу useEffect, чтобы они регистрировались только один раз // и не зависели от изменений зависимостей const handleRoomUpdate = useCallback((updatedRoom) => { @@ -22,11 +28,11 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => { const handleGameStarted = useCallback((updatedRoom) => { console.log('🎮 gameStarted received'); setRoom(updatedRoom); - // Вызываем callback для навигации на страницу игры - if (onGameStarted) { - onGameStarted(updatedRoom); + // Вызываем callback через ref + if (onGameStartedRef.current) { + onGameStartedRef.current(updatedRoom); } - }, [onGameStarted]); + }, []); // Теперь НЕТ зависимости от onGameStarted! const handleGameStateUpdated = useCallback((state) => { console.log('🔄 gameStateUpdated received:', state.participants?.length, 'participants'); @@ -57,11 +63,11 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => { return updatedRoom; }); - // Если игра началась, вызываем callback - if (state.status === 'PLAYING' && onGameStarted) { - onGameStarted(state); + // Если игра началась, вызываем callback через ref + if (state.status === 'PLAYING' && onGameStartedRef.current) { + onGameStartedRef.current(state); } - }, [onGameStarted]); + }, []); // Теперь НЕТ зависимости от onGameStarted! const handleRoomPackUpdated = useCallback((updatedRoom) => { setRoom(updatedRoom); @@ -140,7 +146,7 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => { socketService.off('gameStateUpdated', handleGameStateUpdated); socketService.off('roomPackUpdated', handleRoomPackUpdated); }; - }, [roomCode, password, user?.id, authLoading, handleRoomUpdate, handleGameStarted, handleGameStateUpdated, handleRoomPackUpdated]); + }, [roomCode, password, user?.id, authLoading]); // Убрали обработчики из зависимостей! const createRoom = useCallback(async (hostId, questionPackId, settings = {}, hostName) => { try { @@ -155,16 +161,24 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => { const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => { try { + // ВАЖНО: Сначала подключаемся к WebSocket комнате + // Это гарантирует, что мы получим broadcast от backend после создания участника + if (roomCode) { + socketService.connect(); + socketService.joinRoom(roomCode, userId); + // Даем время на установку WebSocket соединения + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Теперь вызываем REST API для создания участника const response = await roomsApi.join(roomId, userId, name, role); // После успешного присоединения запрашиваем полное состояние через WebSocket - // Это гарантирует получение актуального состояния от сервера (список игроков, тема, voiceMode) - // НЕ обновляем состояние локально - полагаемся только на серверные обновления для консистентности + // (дополнительная гарантия получения актуального состояния) if (roomCode) { - // Небольшая задержка, чтобы убедиться, что WebSocket подключен setTimeout(() => { socketService.emit('requestFullState', { roomCode }); - }, 100); + }, 50); } return response.data; diff --git a/src/pages/RoomPage.jsx b/src/pages/RoomPage.jsx index 823cd86..868908e 100644 --- a/src/pages/RoomPage.jsx +++ b/src/pages/RoomPage.jsx @@ -14,7 +14,7 @@ import GameManagementModal from '../components/GameManagementModal'; const RoomPage = () => { const { roomCode } = useParams(); const navigate = useNavigate(); - const { user } = useAuth(); + const { user, loading: authLoading } = useAuth(); const { changeTheme } = useTheme(); // Храним предыдущий themeId комнаты для отслеживания изменений @@ -107,6 +107,11 @@ const RoomPage = () => { // Единая логика присоединения к комнате useEffect(() => { const handleJoin = async () => { + // Ждем загрузки пользователя из куки + if (authLoading) { + return; + } + // Перенаправляем на главный экран, если нет пользователя if (!user) { alert('Пожалуйста, задайте имя на главном экране'); @@ -156,7 +161,7 @@ const RoomPage = () => { }; handleJoin(); - }, [room, user, joined, participants, joinRoom, navigate, isRoleSelectionModalOpen]); + }, [room, user, joined, participants, joinRoom, navigate, isRoleSelectionModalOpen, authLoading]); // Обработка выбора роли // Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED)