diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b321752..36d46fa 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -7,6 +7,7 @@ generator client { datasource db { provider = "postgresql" + url = env("DATABASE_URL") } model User { @@ -44,6 +45,7 @@ model Room { timerDuration Int @default(30) questionPackId String? autoAdvance Boolean @default(false) + voiceMode Boolean @default(false) // Голосовой режим // Состояние игры currentQuestionIndex Int @default(0) @@ -57,6 +59,9 @@ model Room { startedAt DateTime? finishedAt DateTime? + // Временный пак для комнаты (если хост редактирует вопросы) + customQuestions Json? // Кастомные вопросы для этой комнаты + // Связи host User @relation("HostedRooms", fields: [hostId], references: [id]) participants Participant[] diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts index af0758e..532ee20 100644 --- a/backend/src/game/game.gateway.ts +++ b/backend/src/game/game.gateway.ts @@ -9,6 +9,7 @@ import { import { Server, Socket } from 'socket.io'; import { RoomsService } from '../rooms/rooms.service'; import { RoomEventsService } from './room-events.service'; +import { PrismaService } from '../prisma/prisma.service'; @WebSocketGateway({ cors: { @@ -25,6 +26,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On constructor( private roomsService: RoomsService, private roomEventsService: RoomEventsService, + private prisma: PrismaService, ) {} afterInit(server: Server) { @@ -39,6 +41,14 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On console.log(`Client disconnected: ${client.id}`); } + private async isHost(roomId: string, userId: string): Promise { + const room = await this.prisma.room.findUnique({ + where: { id: roomId }, + select: { hostId: true }, + }); + return room?.hostId === userId; + } + @SubscribeMessage('joinRoom') async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) { client.join(payload.roomCode); @@ -47,7 +57,13 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On } @SubscribeMessage('startGame') - async handleStartGame(client: Socket, payload: { roomId: string; roomCode: string }) { + async handleStartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { + const isHost = await this.isHost(payload.roomId, payload.userId); + if (!isHost) { + client.emit('error', { message: 'Only the host can start the game' }); + return; + } + await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING'); const room = await this.roomsService.getRoomByCode(payload.roomCode); if (room) { @@ -56,24 +72,144 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On } @SubscribeMessage('revealAnswer') - handleRevealAnswer(client: Socket, payload: { roomCode: string; answerIndex: number }) { + async handleRevealAnswer(client: Socket, payload: { roomCode: string; answerIndex: number; userId: string; roomId: string }) { + 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('updateScore') - async handleUpdateScore(client: Socket, payload: { participantId: string; score: number; roomCode: string }) { + 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') - handleNextQuestion(client: Socket, payload: { roomCode: string }) { + 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; + } + this.server.to(payload.roomCode).emit('questionChanged', payload); } @SubscribeMessage('endGame') - async handleEndGame(client: Socket, payload: { roomId: string; roomCode: string }) { + async handleEndGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { + const isHost = await this.isHost(payload.roomId, payload.userId); + if (!isHost) { + client.emit('error', { message: 'Only the host can end the game' }); + return; + } + await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED'); 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); + } + + @SubscribeMessage('restartGame') + async handleRestartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) { + const isHost = await this.isHost(payload.roomId, payload.userId); + if (!isHost) { + client.emit('error', { message: 'Only the host can restart the game' }); + return; + } + + await this.prisma.room.update({ + where: { id: payload.roomId }, + data: { + status: 'WAITING', + currentQuestionIndex: 0, + revealedAnswers: {}, + currentPlayerId: null, + isGameOver: false, + answeredQuestions: 0, + }, + }); + + await this.prisma.participant.updateMany({ + where: { roomId: payload.roomId }, + 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 }) { + const isHost = await this.isHost(payload.roomId, payload.userId); + if (!isHost) { + client.emit('error', { message: 'Only the host can update questions' }); + return; + } + + await this.prisma.room.update({ + where: { id: payload.roomId }, + data: { customQuestions: payload.questions }, + }); + + const room = await this.roomsService.getRoomByCode(payload.roomCode); + this.server.to(payload.roomCode).emit('customQuestionsUpdated', room); + } + + @SubscribeMessage('kickPlayer') + async handleKickPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; participantId: string }) { + const isHost = await this.isHost(payload.roomId, payload.userId); + if (!isHost) { + client.emit('error', { message: 'Only the host can kick players' }); + return; + } + + await this.prisma.participant.update({ + where: { id: payload.participantId }, + data: { isActive: false }, + }); + + const room = await this.roomsService.getRoomByCode(payload.roomCode); + this.server.to(payload.roomCode).emit('playerKicked', { participantId: payload.participantId, room }); + } } diff --git a/backend/src/game/room-events.service.ts b/backend/src/game/room-events.service.ts index dc48261..8117701 100644 --- a/backend/src/game/room-events.service.ts +++ b/backend/src/game/room-events.service.ts @@ -44,4 +44,28 @@ export class RoomEventsService { this.server.to(roomCode).emit('gameEnded', data); } } + + emitCurrentPlayerChanged(roomCode: string, data: any) { + if (this.server) { + this.server.to(roomCode).emit('currentPlayerChanged', data); + } + } + + emitGameRestarted(roomCode: string, data: any) { + if (this.server) { + this.server.to(roomCode).emit('gameRestarted', data); + } + } + + emitCustomQuestionsUpdated(roomCode: string, data: any) { + if (this.server) { + this.server.to(roomCode).emit('customQuestionsUpdated', data); + } + } + + emitPlayerKicked(roomCode: string, data: any) { + if (this.server) { + this.server.to(roomCode).emit('playerKicked', data); + } + } } diff --git a/backend/src/prisma/prisma.service.ts b/backend/src/prisma/prisma.service.ts index 33716a0..3395a6f 100644 --- a/backend/src/prisma/prisma.service.ts +++ b/backend/src/prisma/prisma.service.ts @@ -1,14 +1,10 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; -import { PrismaPg } from '@prisma/adapter-pg'; -import { Pool } from 'pg'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { constructor() { - const pool = new Pool({ connectionString: process.env.DATABASE_URL }); - const adapter = new PrismaPg(pool); - super({ adapter }); + super(); } async onModuleInit() { diff --git a/backend/src/rooms/rooms.controller.ts b/backend/src/rooms/rooms.controller.ts index 4c8f30e..60fa9aa 100644 --- a/backend/src/rooms/rooms.controller.ts +++ b/backend/src/rooms/rooms.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Get, Body, Param, Patch } from '@nestjs/common'; +import { Controller, Post, Get, Body, Param, Patch, Put } from '@nestjs/common'; import { RoomsService } from './rooms.service'; @Controller('rooms') @@ -30,4 +30,46 @@ export class RoomsController { ) { return this.roomsService.updateQuestionPack(roomId, dto.questionPackId); } + + @Patch(':roomId/custom-questions') + async updateCustomQuestions( + @Param('roomId') roomId: string, + @Body() dto: { questions: any } + ) { + return this.roomsService.updateCustomQuestions(roomId, dto.questions); + } + + @Get(':roomId/questions') + async getEffectiveQuestions(@Param('roomId') roomId: string) { + return this.roomsService.getEffectiveQuestions(roomId); + } + + @Patch(':roomId/settings') + async updateRoomSettings( + @Param('roomId') roomId: string, + @Body() dto: { settings: any } + ) { + return this.roomsService.updateRoomSettings(roomId, dto.settings); + } + + @Post(':roomId/restart') + async restartGame(@Param('roomId') roomId: string) { + return this.roomsService.restartGame(roomId); + } + + @Patch(':roomId/current-player') + async setCurrentPlayer( + @Param('roomId') roomId: string, + @Body() dto: { playerId: string } + ) { + return this.roomsService.setCurrentPlayer(roomId, dto.playerId); + } + + @Post(':roomId/kick/:participantId') + async kickPlayer( + @Param('roomId') roomId: string, + @Param('participantId') participantId: string + ) { + return this.roomsService.kickPlayer(roomId, participantId); + } } diff --git a/backend/src/rooms/rooms.service.ts b/backend/src/rooms/rooms.service.ts index 6bb7269..d745a1b 100644 --- a/backend/src/rooms/rooms.service.ts +++ b/backend/src/rooms/rooms.service.ts @@ -123,4 +123,142 @@ export class RoomsService { }, }); } + + async updateCustomQuestions(roomId: string, questions: any) { + const room = await this.prisma.room.update({ + where: { id: roomId }, + data: { + customQuestions: questions, + currentQuestionIndex: 0, + revealedAnswers: {}, + }, + include: { + host: true, + participants: { + include: { user: true }, + }, + questionPack: true, + }, + }); + + this.roomEventsService.emitCustomQuestionsUpdated(room.code, room); + return room; + } + + async getEffectiveQuestions(roomId: string) { + const room = await this.prisma.room.findUnique({ + where: { id: roomId }, + include: { questionPack: true }, + }); + + if (!room) { + return null; + } + + // Если есть кастомные вопросы, используем их + if (room.customQuestions) { + return room.customQuestions; + } + + // Иначе используем вопросы из пака + if (room.questionPack) { + return room.questionPack.questions; + } + + return null; + } + + async updateRoomSettings(roomId: string, settings: any) { + const room = await this.prisma.room.update({ + where: { id: roomId }, + data: settings, + include: { + host: true, + participants: { + include: { user: true }, + }, + questionPack: true, + }, + }); + + this.roomEventsService.emitRoomUpdate(room.code, room); + return room; + } + + async restartGame(roomId: string) { + await this.prisma.room.update({ + where: { id: roomId }, + data: { + status: 'WAITING', + currentQuestionIndex: 0, + revealedAnswers: {}, + currentPlayerId: null, + isGameOver: false, + answeredQuestions: 0, + }, + }); + + await this.prisma.participant.updateMany({ + where: { roomId }, + data: { score: 0 }, + }); + + const room = await this.prisma.room.findUnique({ + where: { id: roomId }, + include: { + host: true, + participants: { + include: { user: true }, + }, + questionPack: true, + }, + }); + + if (room) { + this.roomEventsService.emitGameRestarted(room.code, room); + } + + return room; + } + + async setCurrentPlayer(roomId: string, playerId: string) { + const room = await this.prisma.room.update({ + where: { id: roomId }, + data: { currentPlayerId: playerId }, + include: { + host: true, + participants: { + include: { user: true }, + }, + questionPack: true, + }, + }); + + this.roomEventsService.emitCurrentPlayerChanged(room.code, { playerId }); + return room; + } + + async kickPlayer(roomId: string, participantId: string) { + await this.prisma.participant.update({ + where: { id: participantId }, + data: { isActive: false }, + }); + + const room = await this.prisma.room.findUnique({ + where: { id: roomId }, + include: { + host: true, + participants: { + include: { user: true }, + }, + questionPack: true, + }, + }); + + if (room) { + this.roomEventsService.emitPlayerKicked(room.code, { participantId, room }); + } + + return room; + } } diff --git a/src/components/PlayersModal.jsx b/src/components/PlayersModal.jsx index c9a58ed..b77f90f 100644 --- a/src/components/PlayersModal.jsx +++ b/src/components/PlayersModal.jsx @@ -73,3 +73,5 @@ const PlayersModal = ({ isOpen, onClose, players, onAddPlayer, onRemovePlayer }) export default PlayersModal + + diff --git a/src/components/QuestionsModal.jsx b/src/components/QuestionsModal.jsx index efdfb5e..de091c7 100644 --- a/src/components/QuestionsModal.jsx +++ b/src/components/QuestionsModal.jsx @@ -331,3 +331,5 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => { export default QuestionsModal + +