From d6471d60c4abed6dc706197deaa37d03c4f3f3ad Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 9 Jan 2026 00:44:38 +0300 Subject: [PATCH] stuff --- .cursor/rules/rule.mdc | 7 + backend/prisma/schema.prisma | 3 +- backend/src/game/game.gateway.ts | 421 ++++++++++++------- src/components/Answer.css | 70 +++- src/components/Answer.jsx | 36 +- src/components/Game.jsx | 418 ++----------------- src/components/GameManagementModal.jsx | 14 +- src/components/Question.css | 3 +- src/components/Question.jsx | 22 +- src/hooks/useRoom.js | 67 +--- src/pages/GamePage.jsx | 532 +++++++++++-------------- src/services/socket.js | 16 +- 12 files changed, 640 insertions(+), 969 deletions(-) create mode 100644 .cursor/rules/rule.mdc diff --git a/.cursor/rules/rule.mdc b/.cursor/rules/rule.mdc new file mode 100644 index 0000000..ed732b2 --- /dev/null +++ b/.cursor/rules/rule.mdc @@ -0,0 +1,7 @@ +--- +alwaysApply: true +--- +App runs on a dedicated server with coolify and docker. +Migrations are run via docker on the server. +Keep code clean, never add todos and stubs outside tests. +Keep the final review short. \ No newline at end of file diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 55782b6..1faefd5 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -48,7 +48,8 @@ model Room { // Состояние игры currentQuestionIndex Int @default(0) - revealedAnswers Json @default("{}") + currentQuestionId String? // UUID текущего вопроса + revealedAnswers Json @default("{}") // {"questionUuid": ["answerUuid1", "answerUuid2"]} currentPlayerId String? isGameOver Boolean @default(false) diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts index a19c66f..52ab0aa 100644 --- a/backend/src/game/game.gateway.ts +++ b/backend/src/game/game.gateway.ts @@ -12,10 +12,19 @@ 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: { - // Примечание: декоратор выполняется на этапе инициализации, - // ConfigModule.forRoot() уже загружает переменные в process.env origin: process.env.CORS_ORIGIN || 'http://localhost:5173', credentials: true, }, @@ -51,11 +60,18 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On 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); - const room = await this.roomsService.getRoomByCode(payload.roomCode); - this.server.to(payload.roomCode).emit('roomUpdate', room); + await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('startGame') @@ -67,122 +83,253 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On } await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING'); - const room = await this.roomsService.getRoomByCode(payload.roomCode); - if (room) { - this.server.to(room.code).emit('gameStarted', room); - } - } - - @SubscribeMessage('revealAnswer') - async handleRevealAnswer(client: Socket, payload: { roomCode: string; answerIndex: number; userId: string; roomId: string; questionIndex?: number }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can reveal answers' }); - return; - } - - this.server.to(payload.roomCode).emit('answerRevealed', payload); - } - - @SubscribeMessage('hideAnswer') - async handleHideAnswer(client: Socket, payload: { roomCode: string; answerIndex: number; userId: string; roomId: string; questionIndex?: number }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can hide answers' }); - return; - } - - this.server.to(payload.roomCode).emit('answerHidden', payload); - } - - @SubscribeMessage('showAllAnswers') - async handleShowAllAnswers(client: Socket, payload: { roomCode: string; userId: string; roomId: string; questionIndex?: number }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can show all answers' }); - return; - } - - this.server.to(payload.roomCode).emit('allAnswersShown', payload); - } - - @SubscribeMessage('hideAllAnswers') - async handleHideAllAnswers(client: Socket, payload: { roomCode: string; userId: string; roomId: string; questionIndex?: number }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can hide all answers' }); - return; - } - - this.server.to(payload.roomCode).emit('allAnswersHidden', payload); - } - - @SubscribeMessage('updateScore') - async handleUpdateScore(client: Socket, payload: { participantId: string; score: number; roomCode: string; userId: string; roomId: string }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can update scores' }); - return; - } - - await this.roomsService.updateParticipantScore(payload.participantId, payload.score); - this.server.to(payload.roomCode).emit('scoreUpdated', payload); - } - - @SubscribeMessage('nextQuestion') - async handleNextQuestion(client: Socket, payload: { roomCode: string; userId: string; roomId: string }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can change questions' }); - return; - } - + + // Инициализировать первый вопрос и игрока const room = await this.prisma.room.findUnique({ where: { id: payload.roomId }, - select: { currentQuestionIndex: true }, + include: { + roomPack: true, + participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } } + } }); if (room) { - const newIndex = (room.currentQuestionIndex || 0) + 1; - await this.prisma.room.update({ - where: { id: payload.roomId }, - data: { currentQuestionIndex: newIndex }, + 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) { + 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 } } }); - this.server.to(payload.roomCode).emit('questionChanged', { - ...payload, - questionIndex: newIndex, + // Сохраняем 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 // Для совместимости + } }); } } - @SubscribeMessage('previousQuestion') - async handlePreviousQuestion(client: Socket, payload: { roomCode: string; userId: string; roomId: string }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can change questions' }); - return; - } + private async handlePrevQuestionAction(payload: PlayerAction, room: any) { + const questions = room.roomPack?.questions as any[] || []; + const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId); - const room = await this.prisma.room.findUnique({ - where: { id: payload.roomId }, - select: { currentQuestionIndex: true }, - }); - - if (room && room.currentQuestionIndex > 0) { - const newIndex = room.currentQuestionIndex - 1; + if (currentIdx > 0) { + const prevQuestion = questions[currentIdx - 1]; await this.prisma.room.update({ where: { id: payload.roomId }, - data: { currentQuestionIndex: newIndex }, - }); - - this.server.to(payload.roomCode).emit('questionChanged', { - ...payload, - questionIndex: newIndex, + 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); @@ -192,40 +339,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On } await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED'); - this.server.to(payload.roomCode).emit('gameEnded', payload); - } - - @SubscribeMessage('setCurrentPlayer') - async handleSetCurrentPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; playerId: string }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can select the current player' }); - return; - } - - await this.prisma.room.update({ - where: { id: payload.roomId }, - data: { currentPlayerId: payload.playerId }, - }); - - this.server.to(payload.roomCode).emit('currentPlayerChanged', { playerId: payload.playerId }); - } - - @SubscribeMessage('updateRoomSettings') - async handleUpdateRoomSettings(client: Socket, payload: { roomId: string; roomCode: string; userId: string; settings: any }) { - const isHost = await this.isHost(payload.roomId, payload.userId); - if (!isHost) { - client.emit('error', { message: 'Only the host can update room settings' }); - return; - } - - await this.prisma.room.update({ - where: { id: payload.roomId }, - data: payload.settings, - }); - - const room = await this.roomsService.getRoomByCode(payload.roomCode); - this.server.to(payload.roomCode).emit('roomUpdate', room); + await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('restartGame') @@ -236,13 +350,26 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On 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: null, + currentPlayerId: firstParticipant?.id || null, isGameOver: false, answeredQuestions: 0, }, @@ -253,14 +380,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On data: { score: 0 }, }); - const room = await this.roomsService.getRoomByCode(payload.roomCode); - this.server.to(payload.roomCode).emit('gameRestarted', room); - } - - @SubscribeMessage('updateCustomQuestions') - async handleUpdateCustomQuestions(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any }) { - // DEPRECATED: Use updateRoomPack instead - return this.handleUpdateRoomPack(client, payload); + await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('updateRoomPack') @@ -271,8 +391,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On return; } - const room = await this.roomsService.updateRoomPack(payload.roomId, payload.questions); - this.server.to(payload.roomCode).emit('roomPackUpdated', room); + await this.roomsService.updateRoomPack(payload.roomId, payload.questions); + await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('importQuestions') @@ -290,9 +410,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On } await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices); - - const room = await this.roomsService.getRoomByCode(payload.roomCode); - this.server.to(payload.roomCode).emit('roomPackUpdated', room); + await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('kickPlayer') @@ -308,7 +426,6 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On data: { isActive: false }, }); - const room = await this.roomsService.getRoomByCode(payload.roomCode); - this.server.to(payload.roomCode).emit('playerKicked', { participantId: payload.participantId, room }); + await this.broadcastFullState(payload.roomCode); } } diff --git a/src/components/Answer.css b/src/components/Answer.css index 656d4bf..e368d3b 100644 --- a/src/components/Answer.css +++ b/src/components/Answer.css @@ -11,9 +11,13 @@ align-items: center; justify-content: center; min-height: 0; - height: 100%; position: relative; - overflow: hidden; + overflow-y: auto; + max-height: clamp(120px, 20vh, 250px); + width: 100%; + /* Firefox scrollbar */ + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.05); } /* Горизонтальный layout для узких кнопок */ @@ -23,6 +27,7 @@ align-items: center; justify-content: space-between; gap: clamp(8px, 1.5vw, 15px); + max-height: clamp(100px, 30vh, 200px); } } @@ -95,29 +100,53 @@ transform: scale(1.1); } +.answer-revealed-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: clamp(8px, 1.5vh, 12px); + width: 100%; + flex: 1; + min-height: 0; +} + +.answer-revealed-footer { + display: flex; + align-items: center; + gap: clamp(8px, 1.5vw, 12px); + flex-shrink: 0; +} + .answer-text { font-size: clamp(0.9rem, 1.8vw, 1.4rem); color: #fff; font-weight: bold; - margin-bottom: clamp(4px, 1vh, 8px); text-align: center; text-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; + word-wrap: break-word; + overflow-wrap: break-word; flex-shrink: 1; min-height: 0; flex: 1; + width: 100%; } @media (max-width: 1000px) { + .answer-revealed-content { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: clamp(8px, 1.5vw, 15px); + } + + .answer-revealed-footer { + flex-shrink: 0; + } + .answer-text { - margin-bottom: 0; - margin-right: clamp(8px, 1.5vw, 15px); + margin-right: 0; text-align: left; - -webkit-line-clamp: 2; flex: 1; min-width: 0; } @@ -137,4 +166,23 @@ } } +/* Кастомный скроллбар для кнопок ответов */ +.answer-button::-webkit-scrollbar { + width: 6px; +} + +.answer-button::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +.answer-button::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; + transition: background 0.3s ease; +} + +.answer-button::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); +} diff --git a/src/components/Answer.jsx b/src/components/Answer.jsx index a73a050..67cee4e 100644 --- a/src/components/Answer.jsx +++ b/src/components/Answer.jsx @@ -1,7 +1,7 @@ import VoicePlayer from './VoicePlayer' import './Answer.css' -const Answer = ({ answer, index, onClick, isRevealed, roomId, questionId }) => { +const Answer = ({ answer, onClick, isRevealed, roomId, questionId }) => { const getAnswerClass = () => { if (!isRevealed) return 'answer-hidden' return 'answer-revealed' @@ -32,23 +32,25 @@ const Answer = ({ answer, index, onClick, isRevealed, roomId, questionId }) => { } > {isRevealed ? ( -
+
{answer.text} - - {answer.points} - - {roomId && questionId && answer.id && ( - - )} +
+ + {answer.points} + + {roomId && questionId && answer.id && ( + + )} +
) : ( { - const { playEffect } = useVoice(); + // Нет локального state - всё из props! + // Нет useEffect - нет синхронизации! + // Нет cookies - не нужны! - // Для локальной игры используем cookies, для онлайн - props - const [players, setPlayers] = useState(() => { - if (isOnlineMode && roomParticipants) { - return roomParticipants.map(p => ({ - id: p.id, - name: p.name, - })) - } - const savedPlayers = getCookie('gamePlayers') - return savedPlayers || [] - }) + const players = roomParticipants; - const [currentPlayerId, setCurrentPlayerId] = useState(() => { - if (isOnlineMode && roomParticipants && roomParticipants.length > 0) { - return roomParticipants[0].id - } - const savedId = getCookie('gameCurrentPlayerId') - return savedId !== null ? savedId : null - }) - const [playerScores, setPlayerScores] = useState(() => { - const savedScores = getCookie('gamePlayerScores') - return savedScores || {} - }) - const [gameOver, setGameOver] = useState(() => { - const savedGameOver = getCookie('gameOver') - return savedGameOver === true - }) - const [revealedAnswers, setRevealedAnswers] = useState(() => { - const savedAnswers = getCookie('gameRevealedAnswers') - return savedAnswers || {} - }) + const handleAnswerClick = (answerId, points) => { + if (!currentQuestion) return; + if (revealedAnswers.includes(answerId)) return; // Проверка по UUID + if (!currentPlayerId) return; + if (!onAnswerClick) return; - // Получаем открытые ответы для текущего вопроса - const getCurrentRevealedAnswers = () => { - return revealedAnswers[currentQuestionIndex] || [] - } - - // Обновляем открытые ответы для текущего вопроса - const updateRevealedAnswers = (newAnswers) => { - setRevealedAnswers({ - ...revealedAnswers, - [currentQuestionIndex]: newAnswers, - }) - } - const [isPlayersModalOpen, setIsPlayersModalOpen] = useState(false) - const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false) - - // Сохраняем состояние в cookies при изменении - useEffect(() => { - if (players.length > 0) { - setCookie('gamePlayers', players) - } else { - deleteCookie('gamePlayers') - } - }, [players]) - - useEffect(() => { - if (currentPlayerId !== null) { - setCookie('gameCurrentPlayerId', currentPlayerId) - } else { - deleteCookie('gameCurrentPlayerId') - } - }, [currentPlayerId]) - - useEffect(() => { - if (Object.keys(playerScores).length > 0) { - setCookie('gamePlayerScores', playerScores) - } else { - deleteCookie('gamePlayerScores') - } - }, [playerScores]) - - useEffect(() => { - setCookie('gameRevealedAnswers', revealedAnswers) - // Уведомляем родительский компонент об изменении состояния открытых ответов - if (onQuestionIndexChange) { - // Это вызовет перерендер в App, который обновит состояние кнопки - } - }, [revealedAnswers, currentQuestionIndex]) - - useEffect(() => { - setCookie('gameOver', gameOver) - }, [gameOver]) - - // Обновляем игроков при изменении roomParticipants (для онлайн режима) - useEffect(() => { - if (isOnlineMode && roomParticipants) { - const updatedPlayers = roomParticipants.map(p => ({ - id: p.id, - name: p.name, - })) - setPlayers(updatedPlayers) - - // Устанавливаем текущего игрока, если его нет - if (!currentPlayerId && updatedPlayers.length > 0) { - setCurrentPlayerId(updatedPlayers[0].id) - } - - // Обновляем scores для новых игроков - setPlayerScores(prev => { - const newScores = { ...prev } - updatedPlayers.forEach(player => { - if (!(player.id in newScores)) { - newScores[player.id] = 0 - } - }) - return newScores - }) - } - }, [isOnlineMode, roomParticipants, currentPlayerId]) - - // Устанавливаем первого игрока текущим, если есть игроки, но нет текущего игрока (для локальной игры) - useEffect(() => { - if (!isOnlineMode && players.length > 0 && !currentPlayerId) { - setCurrentPlayerId(players[0].id) - } - }, [isOnlineMode, players, currentPlayerId]) - - const currentQuestion = questions[currentQuestionIndex] - const isLastQuestion = currentQuestionIndex === questions.length - 1 - - const handleShowAllAnswers = () => { - if (!currentQuestion) return - const currentRevealed = getCurrentRevealedAnswers() - const allAnswersRevealed = currentRevealed.length === currentQuestion.answers.length - - if (allAnswersRevealed) { - // Если все открыты - скрываем все - updateRevealedAnswers([]) - } else { - // Если не все открыты - открываем все - const allAnswerIndices = currentQuestion.answers.map((_, index) => index) - updateRevealedAnswers(allAnswerIndices) - } - } - - const hasRevealedAnswers = () => { - const currentRevealed = getCurrentRevealedAnswers() - return currentRevealed.length > 0 - } - - const areAllAnswersRevealed = () => { - if (!currentQuestion) return false - const currentRevealed = getCurrentRevealedAnswers() - return currentRevealed.length === currentQuestion.answers.length - } + onAnswerClick(answerId, points); // Передаем UUID + }; + // Expose methods для внешних компонентов (если нужно) useImperativeHandle(ref, () => ({ - openPlayersModal: () => setIsPlayersModalOpen(true), - openQuestionsModal: () => setIsQuestionsModalOpen(true), - newGame: () => { - setPlayers([]) - setCurrentPlayerId(null) - setPlayerScores({}) - setGameOver(false) - setRevealedAnswers({}) - }, - showAllAnswers: handleShowAllAnswers, - areAllAnswersRevealed: areAllAnswersRevealed, - })) - - const handleAddPlayer = (name) => { - const newPlayer = { - id: Date.now(), - name: name, - } - const updatedPlayers = [...players, newPlayer] - setPlayers(updatedPlayers) - - // Если это первый участник, делаем его текущим - if (updatedPlayers.length === 1) { - setCurrentPlayerId(newPlayer.id) - setPlayerScores({ [newPlayer.id]: 0 }) - } else { - setPlayerScores({ ...playerScores, [newPlayer.id]: 0 }) - } - } - - const handleSelectPlayer = (playerId) => { - setCurrentPlayerId(playerId) - } - - const handleRemovePlayer = (playerId) => { - const updatedPlayers = players.filter(p => p.id !== playerId) - setPlayers(updatedPlayers) - - const updatedScores = { ...playerScores } - delete updatedScores[playerId] - setPlayerScores(updatedScores) - - // Если удалили текущего участника, выбираем другого - if (currentPlayerId === playerId) { - if (updatedPlayers.length > 0) { - setCurrentPlayerId(updatedPlayers[0].id) - } else { - setCurrentPlayerId(null) - } - } - } - - const getNextPlayerId = () => { - if (players.length === 0) return null - if (players.length === 1) return currentPlayerId - - const currentIndex = players.findIndex(p => p.id === currentPlayerId) - const nextIndex = (currentIndex + 1) % players.length - return players[nextIndex].id - } - - const handleAnswerClick = (answerIndex, points) => { - const currentRevealed = getCurrentRevealedAnswers() - if (currentRevealed.includes(answerIndex)) return - if (!currentPlayerId) return - if (!currentQuestion) return - - const isLastAnswer = currentRevealed.length === currentQuestion.answers.length - 1 - - updateRevealedAnswers([...currentRevealed, answerIndex]) - - // Добавляем очки текущему участнику - setPlayerScores({ - ...playerScores, - [currentPlayerId]: (playerScores[currentPlayerId] || 0) + points, - }) - - // Play correct answer sound - playEffect('correct') - - // Переходим к следующему участнику только если это не последний ответ - if (!isLastAnswer) { - const nextPlayerId = getNextPlayerId() - if (nextPlayerId) { - setTimeout(() => { - setCurrentPlayerId(nextPlayerId) - }, 500) - } - } else { - // Если это последний ответ, переходим к следующему участнику перед следующим вопросом - setTimeout(() => { - const nextPlayerId = getNextPlayerId() - if (nextPlayerId) { - setCurrentPlayerId(nextPlayerId) - } - - if (isLastQuestion) { - setGameOver(true) - } else { - setTimeout(() => { - nextQuestion() - }, 500) - } - }, 2000) - } - } - - const nextQuestion = () => { - if (onQuestionIndexChange) { - onQuestionIndexChange(currentQuestionIndex + 1) - } - // Не сбрасываем открытые ответы - они сохраняются для каждого вопроса отдельно - } - - const restartGame = () => { - if (onQuestionIndexChange) { - onQuestionIndexChange(0) - } - setGameOver(false) - setRevealedAnswers({}) - const initialScores = {} - players.forEach(player => { - initialScores[player.id] = 0 - }) - setPlayerScores(initialScores) - if (players.length > 0) { - setCurrentPlayerId(players[0].id) - } - } - - const newGame = () => { - setPlayers([]) - setCurrentPlayerId(null) - setPlayerScores({}) - setGameOver(false) - setRevealedAnswers({}) - if (onQuestionIndexChange) { - onQuestionIndexChange(0) - } - } - - const handlePreviousQuestion = () => { - if (currentQuestionIndex > 0 && onQuestionIndexChange) { - onQuestionIndexChange(currentQuestionIndex - 1) - // Открытые ответы сохраняются для каждого вопроса отдельно - } - } - - const handleNextQuestion = () => { - if (currentQuestionIndex < questions.length - 1 && onQuestionIndexChange) { - onQuestionIndexChange(currentQuestionIndex + 1) - // Открытые ответы сохраняются для каждого вопроса отдельно - } - } - - if (gameOver) { - // Находим победителя(ей) - const scores = Object.values(playerScores) - const maxScore = scores.length > 0 ? Math.max(...scores) : 0 - const winners = players.filter(p => playerScores[p.id] === maxScore) - - // Play victory sound - useEffect(() => { - playEffect('victory') - }, []) - - return ( -
-
-

🎉 Игра окончена! 🎉

-
-

Итоговые результаты:

- {players - .sort((a, b) => (playerScores[b.id] || 0) - (playerScores[a.id] || 0)) - .map((player) => ( -
- {player.name} - - {playerScores[player.id] || 0} очков - -
- ))} -
- -
-
- ) - } + // Можно добавить методы если понадобится + })); return (
- {!isOnlineMode && ( - setIsPlayersModalOpen(false)} - players={players} - onAddPlayer={handleAddPlayer} - onRemovePlayer={handleRemovePlayer} - /> - )} - - {!isOnlineMode && ( - setIsQuestionsModalOpen(false)} - questions={questions} - onUpdateQuestions={onQuestionsChange} - /> - )} -
{players.length > 0 && ( )}
- + {players.length > 0 && currentPlayerId ? (
- {questions.length === 0 ? ( -
-

Добавьте вопросы, чтобы начать игру

-
- ) : currentQuestion ? ( + {currentQuestion ? ( 0} - canGoNext={currentQuestionIndex < questions.length - 1} + revealedAnswers={revealedAnswers} + onPreviousQuestion={onPreviousQuestion} + onNextQuestion={onNextQuestion} roomId={roomId} /> ) : (
-

Ошибка: вопрос не найден

+

Загрузка вопроса...

)}
) : (
-

Добавьте участников, чтобы начать игру

+

Ожидание игроков...

)}
@@ -430,4 +75,3 @@ const Game = forwardRef(({ Game.displayName = 'Game' export default Game - diff --git a/src/components/GameManagementModal.jsx b/src/components/GameManagementModal.jsx index 26a7a91..3aed55d 100644 --- a/src/components/GameManagementModal.jsx +++ b/src/components/GameManagementModal.jsx @@ -62,11 +62,11 @@ const GameManagementModal = ({ if (e.target === e.currentTarget) onClose() } - const handleRevealAnswer = (index) => { - if (revealedAnswers.includes(index)) { - onHideAnswer(index) + const handleRevealAnswer = (answerId) => { + if (revealedAnswers.includes(answerId)) { + onHideAnswer(answerId) } else { - onRevealAnswer(index) + onRevealAnswer(answerId) } } @@ -506,11 +506,11 @@ const GameManagementModal = ({
{currentQuestion.answers.map((answer, index) => (
- {canGoNext && onNextQuestion && ( + {onNextQuestion && (
- {question.answers.map((answer, index) => ( + {question.answers.map((answer) => ( onAnswerClick(index, answer.points)} - isRevealed={revealedAnswers.includes(index)} + onClick={() => onAnswerClick(answer.id, answer.points)} + isRevealed={revealedAnswers.includes(answer.id)} roomId={roomId} questionId={question.id} /> diff --git a/src/hooks/useRoom.js b/src/hooks/useRoom.js index a5fccff..1647a2c 100644 --- a/src/hooks/useRoom.js +++ b/src/hooks/useRoom.js @@ -53,51 +53,23 @@ export const useRoom = (roomCode, onGameStarted = null) => { } }; - const handleAnswerRevealed = (data) => { - console.log('Answer revealed:', data); - }; - - const handleScoreUpdated = (data) => { - console.log('Score updated:', data); - setParticipants((prev) => - prev.map((p) => - p.id === data.participantId ? { ...p, score: data.score } : p - ) - ); - }; - - const handleQuestionChanged = (data) => { - console.log('Question changed:', data); - }; - - const handleGameEnded = (data) => { - console.log('Game ended:', data); - setRoom((prevRoom) => - prevRoom ? { ...prevRoom, status: 'FINISHED' } : null - ); - }; - - const handleRoomPackUpdated = (updatedRoom) => { - console.log('Room pack updated:', updatedRoom); - setRoom(updatedRoom); + // Используем новое событие gameStateUpdated если нужно обновлять состояние + const handleGameStateUpdated = (state) => { + // Обновляем только базовую информацию о комнате + // Полное состояние игры управляется в GamePage + if (state.participants) { + setParticipants(state.participants); + } }; socketService.on('roomUpdate', handleRoomUpdate); socketService.on('gameStarted', handleGameStarted); - socketService.on('answerRevealed', handleAnswerRevealed); - socketService.on('scoreUpdated', handleScoreUpdated); - socketService.on('questionChanged', handleQuestionChanged); - socketService.on('gameEnded', handleGameEnded); - socketService.on('roomPackUpdated', handleRoomPackUpdated); + socketService.on('gameStateUpdated', handleGameStateUpdated); return () => { socketService.off('roomUpdate', handleRoomUpdate); socketService.off('gameStarted', handleGameStarted); - socketService.off('answerRevealed', handleAnswerRevealed); - socketService.off('scoreUpdated', handleScoreUpdated); - socketService.off('questionChanged', handleQuestionChanged); - socketService.off('gameEnded', handleGameEnded); - socketService.off('roomPackUpdated', handleRoomPackUpdated); + socketService.off('gameStateUpdated', handleGameStateUpdated); }; }, [roomCode, onGameStarted, user?.id]); @@ -128,24 +100,6 @@ export const useRoom = (roomCode, onGameStarted = null) => { } }, [room, user]); - const revealAnswer = useCallback((answerIndex) => { - if (room && user) { - socketService.revealAnswer(room.code, room.id, user.id, answerIndex); - } - }, [room, user]); - - const updateScore = useCallback((participantId, score) => { - if (room && user) { - socketService.updateScore(participantId, score, room.code, room.id, user.id); - } - }, [room, user]); - - const nextQuestion = useCallback(() => { - if (room && user) { - socketService.nextQuestion(room.code, room.id, user.id); - } - }, [room, user]); - const endGame = useCallback(() => { if (room && user) { socketService.endGame(room.id, room.code, user.id); @@ -176,9 +130,6 @@ export const useRoom = (roomCode, onGameStarted = null) => { createRoom, joinRoom, startGame, - revealAnswer, - updateScore, - nextQuestion, endGame, updateQuestionPack, }; diff --git a/src/pages/GamePage.jsx b/src/pages/GamePage.jsx index 863542d..01b6b41 100644 --- a/src/pages/GamePage.jsx +++ b/src/pages/GamePage.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; -import { useRoom } from '../hooks/useRoom'; import { questionsApi, roomsApi } from '../services/api'; import QRCode from 'qrcode'; import socketService from '../services/socket'; @@ -16,72 +15,62 @@ const GamePage = () => { const { roomCode } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); - const { - room, - participants, - loading, - error, - updateQuestionPack, - startGame, - endGame, - nextQuestion, - revealAnswer, - updateScore, - } = useRoom(roomCode); - const [questions, setQuestions] = useState([]); - const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); - const [loadingQuestions, setLoadingQuestions] = useState(true); - const [questionPacks, setQuestionPacks] = useState([]); - const [selectedPackId, setSelectedPackId] = useState(''); - const [updatingPack, setUpdatingPack] = useState(false); + // ВСЁ состояние игры в одном объекте + const [gameState, setGameState] = useState({ + roomId: null, + status: 'WAITING', + currentQuestionId: null, + currentPlayerId: null, + revealedAnswers: {}, + participants: [], + questions: [], + hostId: null, + roomCode: null, + }); + + const [loading, setLoading] = useState(true); const [isGameManagementModalOpen, setIsGameManagementModalOpen] = useState(false); const [isQRModalOpen, setIsQRModalOpen] = useState(false); const [qrCode, setQrCode] = useState(''); - const [revealedAnswers, setRevealedAnswers] = useState([]); + const [questionPacks, setQuestionPacks] = useState([]); + // ЕДИНСТВЕННЫЙ обработчик состояния игры useEffect(() => { - const loadQuestions = async () => { - if (!room) return; + if (!roomCode) return; - setLoadingQuestions(true); - try { - // Load from roomPack (always exists now) - if (room.roomPack) { - const questions = room.roomPack.questions; - setQuestions(Array.isArray(questions) ? questions : []); - } else { - // Fallback for legacy rooms without roomPack - if (room.questionPackId) { - if (room.questionPack && room.questionPack.questions) { - const packQuestions = room.questionPack.questions; - setQuestions(Array.isArray(packQuestions) ? packQuestions : []); - } else { - const response = await questionsApi.getPack(room.questionPackId); - setQuestions( - response.data?.questions && Array.isArray(response.data.questions) - ? response.data.questions - : [] - ); - } - } else { - setQuestions([]); - } - } - } catch (error) { - console.error('Error loading questions:', error); - setQuestions([]); - } finally { - setLoadingQuestions(false); + const handleGameStateUpdated = (state) => { + console.log('📦 Game state updated:', state); + setGameState(state); + setLoading(false); + }; + + socketService.connect(); + socketService.joinRoom(roomCode, user?.id); + socketService.on('gameStateUpdated', handleGameStateUpdated); + + return () => { + socketService.off('gameStateUpdated', handleGameStateUpdated); + }; + }, [roomCode, user?.id]); + + // Переподключение - автоматически получаем состояние + useEffect(() => { + const handleReconnect = () => { + console.log('🔄 Reconnected, requesting state...'); + if (roomCode) { + socketService.emit('requestFullState', { roomCode }); } }; - loadQuestions(); - }, [room]); + socketService.onReconnect(handleReconnect); + return () => socketService.offReconnect(handleReconnect); + }, [roomCode]); + // Загрузка доступных паков для хоста useEffect(() => { const fetchPacks = async () => { - if (user && room && room.hostId === user.id) { + if (user && gameState.hostId === user.id) { try { const response = await questionsApi.getPacks(user.id); setQuestionPacks(response.data); @@ -92,17 +81,9 @@ const GamePage = () => { }; fetchPacks(); - }, [user, room]); + }, [user, gameState.hostId]); - useEffect(() => { - if (room && room.questionPackId) { - setSelectedPackId(room.questionPackId); - } else { - setSelectedPackId(''); - } - }, [room]); - - // Generate QR code for room + // Генерация QR кода useEffect(() => { const generateQR = async () => { try { @@ -126,120 +107,176 @@ const GamePage = () => { } }, [roomCode]); - // Listen for socket events - useEffect(() => { - if (!room) return; + // === Handlers для действий игрока === - const handleAnswerRevealed = (data) => { - if (data.questionIndex === currentQuestionIndex) { - setRevealedAnswers((prev) => { - if (!prev.includes(data.answerIndex)) { - return [...prev, data.answerIndex]; - } - return prev; - }); - } - }; + const handleAnswerClick = (answerId, points) => { + if (!gameState.roomId || !user) return; - const handleAnswerHidden = (data) => { - if (data.questionIndex === currentQuestionIndex) { - setRevealedAnswers((prev) => - prev.filter((idx) => idx !== data.answerIndex) - ); - } - }; + const myParticipant = gameState.participants.find(p => p.userId === user.id); + if (!myParticipant) return; - const handleAllAnswersShown = (data) => { - if (data.questionIndex === currentQuestionIndex && questions[currentQuestionIndex]) { - setRevealedAnswers( - Array.from({ length: questions[currentQuestionIndex].answers.length }, (_, i) => i) - ); - } - }; - - const handleAllAnswersHidden = (data) => { - if (data.questionIndex === currentQuestionIndex) { - setRevealedAnswers([]); - } - }; - - const handleQuestionChanged = (data) => { - if (data.questionIndex !== undefined) { - setCurrentQuestionIndex(data.questionIndex); - // Reset revealed answers when question changes - setRevealedAnswers([]); - } - }; - - socketService.on('answerRevealed', handleAnswerRevealed); - socketService.on('answerHidden', handleAnswerHidden); - socketService.on('allAnswersShown', handleAllAnswersShown); - socketService.on('allAnswersHidden', handleAllAnswersHidden); - socketService.on('questionChanged', handleQuestionChanged); - - return () => { - socketService.off('answerRevealed', handleAnswerRevealed); - socketService.off('answerHidden', handleAnswerHidden); - socketService.off('allAnswersShown', handleAllAnswersShown); - socketService.off('allAnswersHidden', handleAllAnswersHidden); - socketService.off('questionChanged', handleQuestionChanged); - }; - }, [room, currentQuestionIndex, questions]); - - // Reset revealed answers when question changes - useEffect(() => { - setRevealedAnswers([]); - }, [currentQuestionIndex]); - - const handleUpdateQuestionPack = async () => { - if (!selectedPackId) { - alert('Выберите пак вопросов'); + // Проверка очереди + if (gameState.currentPlayerId !== myParticipant.id) { + alert('Сейчас не ваша очередь!'); return; } + // Отправляем действие + socketService.emit('playerAction', { + action: 'revealAnswer', + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id, + participantId: myParticipant.id, + questionId: gameState.currentQuestionId, + answerId: answerId + }); + }; + + const handleNextQuestion = () => { + if (!gameState.roomId || !user) return; + + const myParticipant = gameState.participants.find(p => p.userId === user.id); + if (!myParticipant) return; + + socketService.emit('playerAction', { + action: 'nextQuestion', + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id, + participantId: myParticipant.id + }); + }; + + const handlePrevQuestion = () => { + if (!gameState.roomId || !user) return; + + const myParticipant = gameState.participants.find(p => p.userId === user.id); + if (!myParticipant) return; + + socketService.emit('playerAction', { + action: 'prevQuestion', + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id, + participantId: myParticipant.id + }); + }; + + const handleStartGame = () => { + if (!gameState.roomId || !user) return; + + socketService.emit('startGame', { + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id + }); + }; + + const handleEndGame = () => { + if (window.confirm('Завершить игру?')) { + socketService.emit('endGame', { + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id + }); + } + }; + + const handleRestartGame = () => { + if (window.confirm('Начать игру заново? Все очки будут сброшены.')) { + socketService.emit('restartGame', { + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id + }); + } + }; + + const handleUpdateRoomQuestions = async (newQuestions) => { + if (!gameState.roomId) return; + try { - setUpdatingPack(true); - await updateQuestionPack(selectedPackId); - // Перезагружаем вопросы после обновления пака - const response = await questionsApi.getPack(selectedPackId); - if (response.data && response.data.questions) { - setQuestions( - Array.isArray(response.data.questions) - ? response.data.questions - : [], - ); - setCurrentQuestionIndex(0); - } + socketService.emit('updateRoomPack', { + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id, + questions: newQuestions + }); } catch (error) { - console.error('Error updating question pack:', error); - alert('Ошибка при обновлении пака вопросов'); - } finally { - setUpdatingPack(false); + console.error('Error updating room pack:', error); + alert('Ошибка при сохранении вопросов'); } }; - const handleQuestionsChange = (newQuestions) => { - setQuestions(newQuestions); - if (currentQuestionIndex >= newQuestions.length) { - setCurrentQuestionIndex(0); - } + // Handlers для GameManagementModal (работают с UUID) + const handleRevealAnswer = (answerId) => { + if (!gameState.roomId || !user) return; + const myParticipant = gameState.participants.find(p => p.userId === user.id); + if (!myParticipant) return; + + socketService.emit('playerAction', { + action: 'revealAnswer', + roomId: gameState.roomId, + roomCode: gameState.roomCode, + userId: user.id, + participantId: myParticipant.id, + questionId: gameState.currentQuestionId, + answerId: answerId + }); }; - if (loading || loadingQuestions) { + const handleHideAnswer = (answerId) => { + // TODO: В новой архитектуре нет hideAnswer + // Ответы можно только открывать через playerAction + console.warn('hideAnswer not implemented in new architecture'); + }; + + const handleShowAllAnswers = () => { + // TODO: Открыть все ответы через playerAction + console.warn('showAllAnswers not fully implemented'); + // Можно сделать цикл по всем ответам и открыть их + }; + + const handleHideAllAnswers = () => { + // TODO: В новой архитектуре нет hideAllAnswers + console.warn('hideAllAnswers not implemented in new architecture'); + }; + + const handleAwardPoints = (participantId, points) => { + // TODO: Начисление очков вручную + // Нужен отдельный backend endpoint или событие + console.warn('Manual point award not implemented yet'); + }; + + // === Вычисляемые значения === + + const currentQuestion = gameState.questions.find( + q => q.id === gameState.currentQuestionId + ); + + const currentQuestionIndex = gameState.questions.findIndex( + q => q.id === gameState.currentQuestionId + ); + + const revealedForCurrentQ = gameState.revealedAnswers[gameState.currentQuestionId] || []; + + const playerScores = gameState.participants.reduce( + (acc, p) => ({ ...acc, [p.id]: p.score }), + {} + ); + + const isHost = user && gameState.hostId === user.id; + const canGoPrev = currentQuestionIndex > 0; + const canGoNext = currentQuestionIndex < gameState.questions.length - 1; + + // === Render === + + if (loading) { return
Загрузка игры...
; } - if (error) { - return ( -
-

Ошибка

-

{error}

- -
- ); - } - - if (!room) { + if (!gameState.roomId) { return (

Комната не найдена

@@ -248,135 +285,9 @@ const GamePage = () => { ); } - const isHost = user && room.hostId === user.id; - - const handleUpdateRoomQuestions = async (newQuestions) => { - setQuestions(newQuestions); - if (room) { - try { - await roomsApi.updateRoomPack(room.id, newQuestions); - } catch (error) { - console.error('Error updating room pack:', error); - alert('Ошибка при сохранении вопросов'); - } - } - }; - - // Game control handlers - const handleStartGame = () => { - startGame(); - }; - - const handleEndGame = () => { - if (window.confirm('Завершить игру? Весь прогресс будет сохранен.')) { - endGame(); - } - }; - - const handleNextQuestion = () => { - if (currentQuestionIndex < questions.length - 1) { - nextQuestion(); - // The question index will be updated via socket event - } - }; - - const handlePreviousQuestion = () => { - if (room && user && currentQuestionIndex > 0) { - socketService.emit('previousQuestion', { - roomCode: room.code, - roomId: room.id, - userId: user.id, - }); - // The question index will be updated via socket event (questionChanged) - } - }; - - const handleRevealAnswer = (answerIndex) => { - if (room && user) { - socketService.emit('revealAnswer', { - roomCode: room.code, - roomId: room.id, - userId: user.id, - answerIndex, - questionIndex: currentQuestionIndex, - }); - // Also update local state immediately for better UX - setRevealedAnswers((prev) => { - if (!prev.includes(answerIndex)) { - return [...prev, answerIndex]; - } - return prev; - }); - } - }; - - const handleHideAnswer = (answerIndex) => { - if (room && user) { - socketService.emit('hideAnswer', { - roomCode: room.code, - roomId: room.id, - userId: user.id, - answerIndex, - questionIndex: currentQuestionIndex, - }); - // Also update local state immediately for better UX - setRevealedAnswers((prev) => - prev.filter((idx) => idx !== answerIndex) - ); - } - }; - - const handleShowAllAnswers = () => { - if (room && user && questions[currentQuestionIndex]) { - socketService.emit('showAllAnswers', { - roomCode: room.code, - roomId: room.id, - userId: user.id, - questionIndex: currentQuestionIndex, - }); - // Also update local state immediately for better UX - setRevealedAnswers( - Array.from({ length: questions[currentQuestionIndex].answers.length }, (_, i) => i) - ); - } - }; - - const handleHideAllAnswers = () => { - if (room && user) { - socketService.emit('hideAllAnswers', { - roomCode: room.code, - roomId: room.id, - userId: user.id, - questionIndex: currentQuestionIndex, - }); - // Also update local state immediately for better UX - setRevealedAnswers([]); - } - }; - - const handleAwardPoints = (participantId, points) => { - if (room && user) { - const participant = participants.find((p) => p.id === participantId); - if (participant) { - const newScore = (participant.score || 0) + points; - updateScore(participantId, newScore); - } - } - }; - - const handlePenalty = (participantId) => { - if (room && user) { - const participant = participants.find((p) => p.id === participantId); - if (participant) { - const newScore = Math.max(0, (participant.score || 0) - 10); - updateScore(participantId, newScore); - } - } - }; - return (
- {/* Control bar - only for host */} + {/* Control bar - только для хоста */} {isHost && (
@@ -399,9 +310,9 @@ const GamePage = () => {
- {questions.length > 0 && ( + {gameState.questions.length > 0 && (
- {currentQuestionIndex + 1}/{questions.length} + {currentQuestionIndex + 1}/{gameState.questions.length}
)}
@@ -409,7 +320,7 @@ const GamePage = () => { )}
- {questions.length === 0 && ( + {gameState.questions.length === 0 && (

Вопросы не загружены. @@ -421,13 +332,15 @@ const GamePage = () => { )}

@@ -444,25 +357,30 @@ const GamePage = () => { setIsGameManagementModalOpen(false)} - room={room} - participants={participants} - currentQuestion={questions[currentQuestionIndex]} + room={{ + id: gameState.roomId, + code: gameState.roomCode, + status: gameState.status, + hostId: gameState.hostId + }} + participants={gameState.participants} + currentQuestion={currentQuestion} currentQuestionIndex={currentQuestionIndex} - totalQuestions={questions.length} - revealedAnswers={revealedAnswers} - questions={questions} + totalQuestions={gameState.questions.length} + revealedAnswers={revealedForCurrentQ} + questions={gameState.questions} onUpdateQuestions={handleUpdateRoomQuestions} availablePacks={questionPacks} onStartGame={handleStartGame} onEndGame={handleEndGame} + onRestartGame={handleRestartGame} onNextQuestion={handleNextQuestion} - onPreviousQuestion={handlePreviousQuestion} + onPreviousQuestion={handlePrevQuestion} onRevealAnswer={handleRevealAnswer} onHideAnswer={handleHideAnswer} onShowAllAnswers={handleShowAllAnswers} onHideAllAnswers={handleHideAllAnswers} onAwardPoints={handleAwardPoints} - onPenalty={handlePenalty} /> )} diff --git a/src/services/socket.js b/src/services/socket.js index e28161d..dee4985 100644 --- a/src/services/socket.js +++ b/src/services/socket.js @@ -82,22 +82,14 @@ class SocketService { this.emit('startGame', { roomId, roomCode, userId }); } - revealAnswer(roomCode, roomId, userId, answerIndex) { - this.emit('revealAnswer', { roomCode, roomId, userId, answerIndex }); - } - - updateScore(participantId, score, roomCode, roomId, userId) { - this.emit('updateScore', { participantId, score, roomCode, roomId, userId }); - } - - nextQuestion(roomCode, roomId, userId) { - this.emit('nextQuestion', { roomCode, roomId, userId }); - } - endGame(roomId, roomCode, userId) { this.emit('endGame', { roomId, roomCode, userId }); } + // Note: Game actions now use 'playerAction' event with UUID + // Direct emit is preferred over these helper methods + // Example: socketService.emit('playerAction', { action: 'revealAnswer', ... }) + updateRoomPack(roomId, roomCode, userId, questions) { this.emit('updateRoomPack', { roomId, roomCode, userId, questions }); }