stuff
This commit is contained in:
parent
0c86868e0f
commit
d6471d60c4
12 changed files with 640 additions and 969 deletions
7
.cursor/rules/rule.mdc
Normal file
7
.cursor/rules/rule.mdc
Normal file
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', width: '100%' }}>
|
||||
<div className="answer-revealed-content">
|
||||
<span className="answer-text">{answer.text}</span>
|
||||
<span
|
||||
className="answer-points"
|
||||
style={{ color: getPointsColor(answer.points) }}
|
||||
>
|
||||
{answer.points}
|
||||
</span>
|
||||
{roomId && questionId && answer.id && (
|
||||
<VoicePlayer
|
||||
roomId={roomId}
|
||||
questionId={questionId}
|
||||
contentType="answer"
|
||||
answerId={answer.id}
|
||||
showButton={true}
|
||||
/>
|
||||
)}
|
||||
<div className="answer-revealed-footer">
|
||||
<span
|
||||
className="answer-points"
|
||||
style={{ color: getPointsColor(answer.points) }}
|
||||
>
|
||||
{answer.points}
|
||||
</span>
|
||||
{roomId && questionId && answer.id && (
|
||||
<VoicePlayer
|
||||
roomId={roomId}
|
||||
questionId={questionId}
|
||||
contentType="answer"
|
||||
answerId={answer.id}
|
||||
showButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1,426 +1,71 @@
|
|||
import { useState, useImperativeHandle, forwardRef, useEffect } from 'react'
|
||||
import { forwardRef, useImperativeHandle } from 'react'
|
||||
import Question from './Question'
|
||||
import Players from './Players'
|
||||
import PlayersModal from './PlayersModal'
|
||||
import QuestionsModal from './QuestionsModal'
|
||||
import { getCookie, setCookie, deleteCookie } from '../utils/cookies'
|
||||
import { useVoice } from '../hooks/useVoice'
|
||||
import './Game.css'
|
||||
|
||||
const Game = forwardRef(({
|
||||
questions = [],
|
||||
currentQuestionIndex = 0,
|
||||
onQuestionIndexChange,
|
||||
onQuestionsChange,
|
||||
roomParticipants = null, // Участники для онлайн игры
|
||||
isOnlineMode = false, // Флаг онлайн режима
|
||||
roomId = null, // Room ID для TTS (только для онлайн режима)
|
||||
currentQuestion = null,
|
||||
roomParticipants = [],
|
||||
roomId = null,
|
||||
revealedAnswers = [], // Массив UUID открытых ответов
|
||||
playerScores = {}, // Объект {playerId: score}
|
||||
currentPlayerId = null,
|
||||
onAnswerClick = null,
|
||||
onPreviousQuestion = null,
|
||||
onNextQuestion = null,
|
||||
}, ref) => {
|
||||
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 (
|
||||
<div className="game-over">
|
||||
<div className="game-over-content">
|
||||
<h2 className="game-over-title">🎉 Игра окончена! 🎉</h2>
|
||||
<div className="final-scores">
|
||||
<h3 className="final-scores-title">Итоговые результаты:</h3>
|
||||
{players
|
||||
.sort((a, b) => (playerScores[b.id] || 0) - (playerScores[a.id] || 0))
|
||||
.map((player) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className={`final-score-item ${
|
||||
winners.includes(player) ? 'final-score-winner' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="final-score-name">{player.name}</span>
|
||||
<span className="final-score-value">
|
||||
{playerScores[player.id] || 0} очков
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="restart-button" onClick={restartGame}>
|
||||
Играть снова
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Можно добавить методы если понадобится
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="game">
|
||||
{!isOnlineMode && (
|
||||
<PlayersModal
|
||||
isOpen={isPlayersModalOpen}
|
||||
onClose={() => setIsPlayersModalOpen(false)}
|
||||
players={players}
|
||||
onAddPlayer={handleAddPlayer}
|
||||
onRemovePlayer={handleRemovePlayer}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isOnlineMode && (
|
||||
<QuestionsModal
|
||||
isOpen={isQuestionsModalOpen}
|
||||
onClose={() => setIsQuestionsModalOpen(false)}
|
||||
questions={questions}
|
||||
onUpdateQuestions={onQuestionsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="game-header">
|
||||
{players.length > 0 && (
|
||||
<Players
|
||||
players={players}
|
||||
currentPlayerId={currentPlayerId}
|
||||
playerScores={playerScores}
|
||||
onSelectPlayer={handleSelectPlayer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{players.length > 0 && currentPlayerId ? (
|
||||
<div className="game-content">
|
||||
{questions.length === 0 ? (
|
||||
<div className="no-players-message">
|
||||
<p>Добавьте вопросы, чтобы начать игру</p>
|
||||
</div>
|
||||
) : currentQuestion ? (
|
||||
{currentQuestion ? (
|
||||
<Question
|
||||
question={currentQuestion}
|
||||
questionNumber={currentQuestionIndex + 1}
|
||||
onAnswerClick={handleAnswerClick}
|
||||
revealedAnswers={getCurrentRevealedAnswers()}
|
||||
onPreviousQuestion={handlePreviousQuestion}
|
||||
onNextQuestion={handleNextQuestion}
|
||||
canGoPrevious={currentQuestionIndex > 0}
|
||||
canGoNext={currentQuestionIndex < questions.length - 1}
|
||||
revealedAnswers={revealedAnswers}
|
||||
onPreviousQuestion={onPreviousQuestion}
|
||||
onNextQuestion={onNextQuestion}
|
||||
roomId={roomId}
|
||||
/>
|
||||
) : (
|
||||
<div className="no-players-message">
|
||||
<p>Ошибка: вопрос не найден</p>
|
||||
<p>Загрузка вопроса...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-players-message">
|
||||
<p>Добавьте участников, чтобы начать игру</p>
|
||||
<p>Ожидание игроков...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -430,4 +75,3 @@ const Game = forwardRef(({
|
|||
Game.displayName = 'Game'
|
||||
|
||||
export default Game
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
<div className="answers-grid">
|
||||
{currentQuestion.answers.map((answer, index) => (
|
||||
<button
|
||||
key={index}
|
||||
key={answer.id || index}
|
||||
className={`answer-button ${
|
||||
revealedAnswers.includes(index) ? 'revealed' : 'hidden'
|
||||
revealedAnswers.includes(answer.id) ? 'revealed' : 'hidden'
|
||||
}`}
|
||||
onClick={() => handleRevealAnswer(index)}
|
||||
onClick={() => handleRevealAnswer(answer.id)}
|
||||
>
|
||||
<span className="answer-num">{index + 1}</span>
|
||||
<span className="answer-txt">{answer.text}</span>
|
||||
|
|
|
|||
|
|
@ -127,11 +127,10 @@
|
|||
.answers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
gap: clamp(6px, 1.2vw, 12px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
|
|||
|
|
@ -4,24 +4,17 @@ import './Question.css'
|
|||
|
||||
const Question = ({
|
||||
question,
|
||||
questionNumber,
|
||||
onAnswerClick,
|
||||
revealedAnswers,
|
||||
onShowAll,
|
||||
revealedAnswers, // Массив UUID открытых ответов
|
||||
onPreviousQuestion,
|
||||
onNextQuestion,
|
||||
canGoPrevious,
|
||||
canGoNext,
|
||||
roomId,
|
||||
}) => {
|
||||
const allAnswersRevealed = question.answers.every((_, index) => revealedAnswers.includes(index))
|
||||
const hasUnrevealedAnswers = revealedAnswers.length < question.answers.length
|
||||
|
||||
return (
|
||||
<div className="question-container">
|
||||
<div className="question-box">
|
||||
<div className="question-navigation">
|
||||
{canGoPrevious && onPreviousQuestion && (
|
||||
{onPreviousQuestion && (
|
||||
<button
|
||||
className="question-nav-button question-nav-button-prev"
|
||||
onClick={onPreviousQuestion}
|
||||
|
|
@ -40,7 +33,7 @@ const Question = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{canGoNext && onNextQuestion && (
|
||||
{onNextQuestion && (
|
||||
<button
|
||||
className="question-nav-button question-nav-button-next"
|
||||
onClick={onNextQuestion}
|
||||
|
|
@ -52,13 +45,12 @@ const Question = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="answers-grid">
|
||||
{question.answers.map((answer, index) => (
|
||||
{question.answers.map((answer) => (
|
||||
<Answer
|
||||
key={answer.id || index}
|
||||
key={answer.id}
|
||||
answer={answer}
|
||||
index={index}
|
||||
onClick={() => onAnswerClick(index, answer.points)}
|
||||
isRevealed={revealedAnswers.includes(index)}
|
||||
onClick={() => onAnswerClick(answer.id, answer.points)}
|
||||
isRevealed={revealedAnswers.includes(answer.id)}
|
||||
roomId={roomId}
|
||||
questionId={question.id}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 <div className="loading">Загрузка игры...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-page">
|
||||
<h1>Ошибка</h1>
|
||||
<p>{error}</p>
|
||||
<button onClick={() => navigate('/')}>На главную</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
if (!gameState.roomId) {
|
||||
return (
|
||||
<div className="error-page">
|
||||
<h1>Комната не найдена</h1>
|
||||
|
|
@ -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 (
|
||||
<div className="game-page">
|
||||
{/* Control bar - only for host */}
|
||||
{/* Control bar - только для хоста */}
|
||||
{isHost && (
|
||||
<div className="game-control-bar">
|
||||
<div className="game-control-left">
|
||||
|
|
@ -399,9 +310,9 @@ const GamePage = () => {
|
|||
</div>
|
||||
|
||||
<div className="game-control-right">
|
||||
{questions.length > 0 && (
|
||||
{gameState.questions.length > 0 && (
|
||||
<div className="question-counter">
|
||||
{currentQuestionIndex + 1}/{questions.length}
|
||||
{currentQuestionIndex + 1}/{gameState.questions.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -409,7 +320,7 @@ const GamePage = () => {
|
|||
)}
|
||||
|
||||
<div className="game-container">
|
||||
{questions.length === 0 && (
|
||||
{gameState.questions.length === 0 && (
|
||||
<div className="no-questions-banner">
|
||||
<p>
|
||||
Вопросы не загружены.
|
||||
|
|
@ -421,13 +332,15 @@ const GamePage = () => {
|
|||
)}
|
||||
|
||||
<Game
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
onQuestionIndexChange={setCurrentQuestionIndex}
|
||||
onQuestionsChange={handleQuestionsChange}
|
||||
roomParticipants={participants}
|
||||
isOnlineMode={true}
|
||||
roomId={room?.id}
|
||||
currentQuestion={currentQuestion}
|
||||
roomParticipants={gameState.participants}
|
||||
roomId={gameState.roomId}
|
||||
revealedAnswers={revealedForCurrentQ}
|
||||
playerScores={playerScores}
|
||||
currentPlayerId={gameState.currentPlayerId}
|
||||
onAnswerClick={handleAnswerClick}
|
||||
onPreviousQuestion={canGoPrev ? handlePrevQuestion : null}
|
||||
onNextQuestion={canGoNext ? handleNextQuestion : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -444,25 +357,30 @@ const GamePage = () => {
|
|||
<GameManagementModal
|
||||
isOpen={isGameManagementModalOpen}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue