diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts index 6ef639b..dac100f 100644 --- a/backend/src/game/game.gateway.ts +++ b/backend/src/game/game.gateway.ts @@ -110,6 +110,14 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On console.log(`πŸ‘₯ Clients in room ${payload.roomCode}: ${roomClients?.size || 0}`, Array.from(roomClients || [])); + // Π’ΠΠ–ΠΠž: ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ + client.emit('roomJoined', { + roomCode: payload.roomCode, + success: true, + clientsInRoom: roomClients?.size || 0 + }); + console.log(`βœ… Sent roomJoined acknowledgement to client ${client.id}`); + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΠΎΠ»Π½ΠΎΠ΅ состояниС для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ ΠΏΡ€ΠΈΡΠΎΠ΅Π΄ΠΈΠ½ΠΈΠ²ΡˆΠ΅ΠΌΡƒΡΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ const room = (await this.prisma.room.findUnique({ where: { code: payload.roomCode }, @@ -1035,4 +1043,42 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On }); } } + + @SubscribeMessage('joinAsParticipant') + async handleJoinAsParticipant( + client: Socket, + payload: { + roomId: string; + roomCode: string; + userId: string; + name: string; + role: 'PLAYER' | 'SPECTATOR'; + } + ) { + try { + console.log(`🎭 Client ${client.id} joining as participant: ${payload.name} (${payload.role})`); + + // Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ сСрвис для создания участника + const participant = await this.roomsService.joinRoom( + payload.roomId, + payload.userId, + payload.name, + payload.role + ); + + console.log(`βœ… Participant created: ${participant.id}, broadcasting to room ${payload.roomCode}`); + + // broadcastFullState ΡƒΠΆΠ΅ Π²Ρ‹Π·Π²Π°Π½ Π²Π½ΡƒΡ‚Ρ€ΠΈ roomsService.joinRoom + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ + client.emit('participantJoined', { + success: true, + participantId: participant.id + }); + } catch (error) { + console.error('❌ Error joining as participant:', error); + client.emit('error', { + message: error.message || 'Failed to join as participant' + }); + } + } } diff --git a/src/hooks/useRoom.js b/src/hooks/useRoom.js index 97d2aa4..7c43694 100644 --- a/src/hooks/useRoom.js +++ b/src/hooks/useRoom.js @@ -161,28 +161,31 @@ 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)); - } + console.log('πŸš€ Starting join process...'); - // Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ REST API для создания участника - const response = await roomsApi.join(roomId, userId, name, role); + // Π¨Π°Π³ 1: ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌΡΡ ΠΊ WebSocket ΠΊΠΎΠΌΠ½Π°Ρ‚Π΅ ΠΈ Π–Π”Π•Πœ подтвСрТдСния + socketService.connect(); + console.log('⏳ Waiting for WebSocket room join confirmation...'); + const joinAck = await socketService.joinRoomWithAck(roomCode, userId); + console.log('βœ… WebSocket connected to room:', joinAck); - // ПослС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ присоСдинСния Π·Π°ΠΏΡ€Π°ΡˆΠΈΠ²Π°Π΅ΠΌ ΠΏΠΎΠ»Π½ΠΎΠ΅ состояниС Ρ‡Π΅Ρ€Π΅Π· WebSocket - // (Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ гарантия получСния Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ состояния) - if (roomCode) { - setTimeout(() => { - socketService.emit('requestFullState', { roomCode }); - }, 50); - } + // Π¨Π°Π³ 2: Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π“ΠΠ ΠΠΠ’Π˜Π ΠžΠ’ΠΠΠΠž ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹, создаСм участника Ρ‡Π΅Ρ€Π΅Π· WebSocket + console.log('⏳ Creating participant in database...'); + const participantAck = await socketService.joinAsParticipant( + roomId, + roomCode, + userId, + name, + role + ); + console.log('βœ… Participant created:', participantAck); - return response.data; + // broadcastFullState ΡƒΠΆΠ΅ Π²Ρ‹Π·Π²Π°Π½ backend, события gameStateUpdated ΠΏΡ€ΠΈΠ΄ΡƒΡ‚ автоматичСски + // ВсС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρ‹ (Π²ΠΊΠ»ΡŽΡ‡Π°Ρ хоста) ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ + + return { success: true, participantId: participantAck.participantId }; } catch (err) { + console.error('❌ Join error:', err); setError(err.message); throw err; } diff --git a/src/pages/CreateRoom.jsx b/src/pages/CreateRoom.jsx index 87e2e56..e08ed7d 100644 --- a/src/pages/CreateRoom.jsx +++ b/src/pages/CreateRoom.jsx @@ -1,11 +1,12 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { useRoom } from '../hooks/useRoom'; +import NameInputModal from '../components/NameInputModal'; const CreateRoom = () => { const navigate = useNavigate(); - const { user } = useAuth(); + const { user, loading: authLoading, loginAnonymous } = useAuth(); const { createRoom, loading: roomLoading } = useRoom(); const [settings, setSettings] = useState({ @@ -13,11 +14,32 @@ const CreateRoom = () => { allowSpectators: true, password: '', }); + const [isNameModalOpen, setIsNameModalOpen] = useState(false); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΈ ΠΏΠΎΠΊΠ°Π· модального ΠΎΠΊΠ½Π° для Π²Π²ΠΎΠ΄Π° ΠΈΠΌΠ΅Π½ΠΈ + // Волько Ссли Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π° ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π΅Ρ‚ + useEffect(() => { + if (!authLoading && !user) { + setIsNameModalOpen(true); + } else if (user) { + setIsNameModalOpen(false); + } + }, [authLoading, user]); + + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π²Π²ΠΎΠ΄Π° ΠΈΠΌΠ΅Π½ΠΈ ΠΈ авторизация + const handleNameSubmit = async (name) => { + try { + await loginAnonymous(name); + setIsNameModalOpen(false); + } catch (error) { + console.error('Login error:', error); + alert('Ошибка ΠΏΡ€ΠΈ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ Π΅Ρ‰Π΅ Ρ€Π°Π·.'); + } + }; const handleCreateRoom = async () => { if (!user) { - alert('ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π·Π°Π΄Π°ΠΉΡ‚Π΅ имя Π½Π° Π³Π»Π°Π²Π½ΠΎΠΌ экранС'); - navigate('/'); + setIsNameModalOpen(true); return; } @@ -92,7 +114,7 @@ const CreateRoom = () => {
+ + navigate('/')} + /> ); }; diff --git a/src/pages/RoomPage.jsx b/src/pages/RoomPage.jsx index 868908e..c7adf0a 100644 --- a/src/pages/RoomPage.jsx +++ b/src/pages/RoomPage.jsx @@ -7,6 +7,7 @@ import { questionsApi } from '../services/api'; import QRCode from 'qrcode'; import socketService from '../services/socket'; import QRModal from '../components/QRModal'; +import NameInputModal from '../components/NameInputModal'; import PasswordModal from '../components/PasswordModal'; import RoleSelectionModal from '../components/RoleSelectionModal'; import GameManagementModal from '../components/GameManagementModal'; @@ -14,7 +15,7 @@ import GameManagementModal from '../components/GameManagementModal'; const RoomPage = () => { const { roomCode } = useParams(); const navigate = useNavigate(); - const { user, loading: authLoading } = useAuth(); + const { user, loading: authLoading, loginAnonymous } = useAuth(); const { changeTheme } = useTheme(); // Π₯Ρ€Π°Π½ΠΈΠΌ ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰ΠΈΠΉ themeId ΠΊΠΎΠΌΠ½Π°Ρ‚Ρ‹ для отслСТивания ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ @@ -40,6 +41,7 @@ const RoomPage = () => { const [qrCode, setQrCode] = useState(''); const [joined, setJoined] = useState(false); const [isQRModalOpen, setIsQRModalOpen] = useState(false); + const [isNameModalOpen, setIsNameModalOpen] = useState(false); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false); const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false); @@ -87,6 +89,16 @@ const RoomPage = () => { } }, [requiresPassword, isPasswordModalOpen, loading]); + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΈΠΌΠ΅Π½ΠΈ: ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ модальноС ΠΎΠΊΠ½ΠΎ для Π²Π²ΠΎΠ΄Π° ΠΈΠΌΠ΅Π½ΠΈ + // Волько Ссли Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°, ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π΅Ρ‚, ΠΊΠΎΠΌΠ½Π°Ρ‚Π° Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½Π° ΠΈ ΠΏΠ°Ρ€ΠΎΠ»ΡŒ Π½Π΅ трСбуСтся + useEffect(() => { + if (!authLoading && !user && room && !loading && !requiresPassword) { + setIsNameModalOpen(true); + } else if (user) { + setIsNameModalOpen(false); + } + }, [authLoading, user, room, loading, requiresPassword]); + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π²Π²ΠΎΠ΄Π° пароля const handlePasswordSubmit = async (enteredPassword) => { try { @@ -104,6 +116,17 @@ const RoomPage = () => { } }; + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π²Π²ΠΎΠ΄Π° ΠΈΠΌΠ΅Π½ΠΈ ΠΈ авторизация + const handleNameSubmit = async (name) => { + try { + await loginAnonymous(name); + setIsNameModalOpen(false); + } catch (error) { + console.error('Login error:', error); + alert('Ошибка ΠΏΡ€ΠΈ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ Π΅Ρ‰Π΅ Ρ€Π°Π·.'); + } + }; + // Единая Π»ΠΎΠ³ΠΈΠΊΠ° присоСдинСния ΠΊ ΠΊΠΎΠΌΠ½Π°Ρ‚Π΅ useEffect(() => { const handleJoin = async () => { @@ -112,10 +135,8 @@ const RoomPage = () => { return; } - // ΠŸΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Π½Π° Π³Π»Π°Π²Π½Ρ‹ΠΉ экран, Ссли Π½Π΅Ρ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + // Если ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π΅Ρ‚ - модальноС ΠΎΠΊΠ½ΠΎ ΠΈΠΌΠ΅Π½ΠΈ покаТСтся Ρ‡Π΅Ρ€Π΅Π· Π΄Ρ€ΡƒΠ³ΠΎΠΉ useEffect if (!user) { - alert('ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π·Π°Π΄Π°ΠΉΡ‚Π΅ имя Π½Π° Π³Π»Π°Π²Π½ΠΎΠΌ экранС'); - navigate('/'); return; } @@ -429,6 +450,12 @@ const RoomPage = () => { roomCode={roomCode} /> + navigate('/')} + /> + { + const timeout = setTimeout(() => { + reject(new Error('WebSocket join timeout - room not joined within 5 seconds')); + }, 5000); // 5 сСкунд максимум + + // Π‘Π»ΡƒΡˆΠ°Π΅ΠΌ ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· + this.socket.once('roomJoined', (data) => { + clearTimeout(timeout); + console.log('βœ… WebSocket room joined:', data); + resolve(data); + }); + + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ запрос Π½Π° присоСдинСниС + console.log(`πŸ“€ Requesting to join WebSocket room: ${roomCode}`); + this.emit('joinRoom', { roomCode, userId }); + }); + } + + // ΠŸΡ€ΠΈΡΠΎΠ΅Π΄ΠΈΠ½Π΅Π½ΠΈΠ΅ ΠΊΠ°ΠΊ участник (созданиС записи Π² Π‘Π”) Ρ‡Π΅Ρ€Π΅Π· WebSocket + joinAsParticipant(roomId, roomCode, userId, name, role) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Participant join timeout - not created within 5 seconds')); + }, 5000); + + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ присоСдинСния + const handleSuccess = (data) => { + clearTimeout(timeout); + this.socket.off('error', handleError); // Π£Π±ΠΈΡ€Π°Π΅ΠΌ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ ошибки + console.log('βœ… Participant joined successfully:', data); + resolve(data); + }; + + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ ошибки + const handleError = (error) => { + clearTimeout(timeout); + this.socket.off('participantJoined', handleSuccess); // Π£Π±ΠΈΡ€Π°Π΅ΠΌ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ успСха + console.error('❌ Participant join error:', error); + reject(new Error(error.message || 'Failed to join as participant')); + }; + + this.socket.once('participantJoined', handleSuccess); + this.socket.once('error', handleError); + + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ запрос + console.log(`πŸ“€ Requesting to join as participant: ${name} (${role})`); + this.emit('joinAsParticipant', { + roomId, + roomCode, + userId, + name, + role + }); + }); + } + startGame(roomId, roomCode, userId) { this.emit('startGame', { roomId, roomCode, userId }); }