import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { RoomsService } from '../rooms/rooms.service'; import { RoomEventsService } from './room-events.service'; import { PrismaService } from '../prisma/prisma.service'; import { RoomPackService } from '../room-pack/room-pack.service'; interface PlayerAction { action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion'; roomId: string; roomCode: string; userId: string; participantId: string; // Для revealAnswer: questionId?: string; // UUID вопроса answerId?: string; // UUID ответа } @WebSocketGateway({ cors: { origin: process.env.CORS_ORIGIN || 'http://localhost:5173', credentials: true, }, }) export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { @WebSocketServer() server: Server; constructor( private roomsService: RoomsService, private roomEventsService: RoomEventsService, private prisma: PrismaService, private roomPackService: RoomPackService, ) {} afterInit(server: Server) { this.roomEventsService.setServer(server); } handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } private async isHost(roomId: string, userId: string): Promise { const room = await this.prisma.room.findUnique({ where: { id: roomId }, select: { hostId: true }, }); return room?.hostId === userId; } private async isCurrentPlayer(roomId: string, participantId: string): Promise { const room = await this.prisma.room.findUnique({ where: { id: roomId }, select: { currentPlayerId: true }, }); return room?.currentPlayerId === participantId; } @SubscribeMessage('joinRoom') async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) { client.join(payload.roomCode); await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('startGame') async handleStartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can start the game' }); return; } await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING'); // Инициализировать первый вопрос и игрока const room = await this.prisma.room.findUnique({ where: { id: payload.roomId }, include: { roomPack: true, participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } } } }); if (room) { const questions = room.roomPack?.questions as any[] || []; const firstQuestion = questions[0]; const firstParticipant = room.participants[0]; if (firstQuestion && firstParticipant) { await this.prisma.room.update({ where: { id: payload.roomId }, data: { currentQuestionId: firstQuestion.id, currentPlayerId: firstParticipant.id, } }); } } await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('playerAction') async handlePlayerAction(client: Socket, payload: PlayerAction) { // Получаем комнату с данными const room = await this.prisma.room.findUnique({ where: { id: payload.roomId }, include: { roomPack: true, participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } } }, }); if (!room) { client.emit('error', { message: 'Room not found' }); return; } // Проверяем права const isHost = room.hostId === payload.userId; const isCurrentPlayer = room.currentPlayerId === payload.participantId; if (!isHost && !isCurrentPlayer) { client.emit('error', { message: 'Not your turn!' }); return; } // Выполняем действие try { switch (payload.action) { case 'revealAnswer': await this.handleRevealAnswerAction(payload, room); break; case 'nextQuestion': await this.handleNextQuestionAction(payload, room); break; case 'prevQuestion': await this.handlePrevQuestionAction(payload, room); break; } // Отправляем полное состояние всем await this.broadcastFullState(payload.roomCode); } catch (error) { console.error('Error handling player action:', error); client.emit('error', { message: 'Failed to process action' }); } } private async handleRevealAnswerAction(payload: PlayerAction, room: any) { if (!payload.questionId || !payload.answerId) { console.error('Missing questionId or answerId in payload'); return; } const questions = room.roomPack?.questions as any[] || []; const question = questions.find(q => q.id === payload.questionId); if (!question) { console.error('Question not found:', payload.questionId); return; } const answer = question.answers?.find((a: any) => a.id === payload.answerId); if (!answer) { console.error('Answer not found:', payload.answerId); return; } // Обновляем revealedAnswers const revealed = (room.revealedAnswers as any) || {}; const currentRevealed: string[] = revealed[payload.questionId] || []; if (!currentRevealed.includes(payload.answerId)) { currentRevealed.push(payload.answerId); revealed[payload.questionId] = currentRevealed; // Начисляем очки await this.prisma.participant.update({ where: { id: payload.participantId }, data: { score: { increment: answer.points } } }); // Сохраняем revealedAnswers await this.prisma.room.update({ where: { id: payload.roomId }, data: { revealedAnswers: revealed } }); // Определяем следующего игрока const participants = room.participants; const currentIdx = participants.findIndex((p: any) => p.id === payload.participantId); const nextIdx = (currentIdx + 1) % participants.length; const nextPlayerId = participants[nextIdx]?.id; // Проверяем, это последний ответ? const isLastAnswer = currentRevealed.length >= question.answers.length; if (!isLastAnswer) { // Меняем игрока await this.prisma.room.update({ where: { id: payload.roomId }, data: { currentPlayerId: nextPlayerId } }); } else { // Последний ответ - проверяем, последний ли вопрос const currentQuestionIndex = questions.findIndex((q: any) => q.id === payload.questionId); const isLastQuestion = currentQuestionIndex >= questions.length - 1; if (isLastQuestion) { // Конец игры await this.prisma.room.update({ where: { id: payload.roomId }, data: { status: 'FINISHED', isGameOver: true, finishedAt: new Date() } }); } else { // Меняем игрока для следующего вопроса await this.prisma.room.update({ where: { id: payload.roomId }, data: { currentPlayerId: nextPlayerId } }); } } } } private async handleNextQuestionAction(payload: PlayerAction, room: any) { const questions = room.roomPack?.questions as any[] || []; const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId); if (currentIdx < questions.length - 1) { const nextQuestion = questions[currentIdx + 1]; await this.prisma.room.update({ where: { id: payload.roomId }, data: { currentQuestionId: nextQuestion.id, currentQuestionIndex: currentIdx + 1 // Для совместимости } }); } } private async handlePrevQuestionAction(payload: PlayerAction, room: any) { const questions = room.roomPack?.questions as any[] || []; const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId); if (currentIdx > 0) { const prevQuestion = questions[currentIdx - 1]; await this.prisma.room.update({ where: { id: payload.roomId }, data: { currentQuestionId: prevQuestion.id, currentQuestionIndex: currentIdx - 1 // Для совместимости } }); } } // КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния private async broadcastFullState(roomCode: string) { const room = await this.prisma.room.findUnique({ where: { code: roomCode }, include: { participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }, roomPack: true, host: { select: { id: true, name: true } } } }); if (!room) return; const questions = room.roomPack?.questions as any[] || []; // Инициализация currentQuestionId если не установлен let currentQuestionId = room.currentQuestionId; if (!currentQuestionId && questions.length > 0) { currentQuestionId = questions[0].id; await this.prisma.room.update({ where: { id: room.id }, data: { currentQuestionId } }); } const fullState = { roomId: room.id, roomCode: room.code, status: room.status, currentQuestionId: currentQuestionId, currentPlayerId: room.currentPlayerId, revealedAnswers: room.revealedAnswers, isGameOver: room.isGameOver, hostId: room.hostId, participants: room.participants.map(p => ({ id: p.id, userId: p.userId, name: p.name, role: p.role, score: p.score })), questions: questions.map((q: any) => ({ id: q.id, text: q.text || q.question, answers: (q.answers || []).map((a: any) => ({ id: a.id, text: a.text, points: a.points })) })) }; this.server.to(roomCode).emit('gameStateUpdated', fullState); } @SubscribeMessage('requestFullState') async handleRequestFullState(client: Socket, payload: { roomCode: string }) { await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('endGame') async handleEndGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can end the game' }); return; } await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED'); await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('restartGame') async handleRestartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can restart the game' }); return; } const room = await this.prisma.room.findUnique({ where: { id: payload.roomId }, include: { roomPack: true, participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } } } }); const questions = room?.roomPack?.questions as any[] || []; const firstQuestion = questions[0]; const firstParticipant = room?.participants[0]; await this.prisma.room.update({ where: { id: payload.roomId }, data: { status: 'WAITING', currentQuestionIndex: 0, currentQuestionId: firstQuestion?.id || null, revealedAnswers: {}, currentPlayerId: firstParticipant?.id || null, isGameOver: false, answeredQuestions: 0, }, }); await this.prisma.participant.updateMany({ where: { roomId: payload.roomId }, data: { score: 0 }, }); await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('updateRoomPack') async handleUpdateRoomPack(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any[] }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can update questions' }); return; } await this.roomsService.updateRoomPack(payload.roomId, payload.questions); await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('importQuestions') async handleImportQuestions(client: Socket, payload: { roomId: string; roomCode: string; userId: string; sourcePackId: string; questionIndices: number[]; }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can import questions' }); return; } await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices); await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('kickPlayer') async handleKickPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; participantId: string }) { const isHost = await this.isHost(payload.roomId, payload.userId); if (!isHost) { client.emit('error', { message: 'Only the host can kick players' }); return; } await this.prisma.participant.update({ where: { id: payload.participantId }, data: { isActive: false }, }); await this.broadcastFullState(payload.roomCode); } }