multiplayer

This commit is contained in:
Dmitry 2026-01-08 16:18:07 +03:00
parent bfd202bf68
commit 3c612f4491
8 changed files with 356 additions and 11 deletions

View file

@ -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[]

View file

@ -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<boolean> {
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 });
}
}

View file

@ -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);
}
}
}

View file

@ -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() {

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -73,3 +73,5 @@ const PlayersModal = ({ isOpen, onClose, players, onAddPlayer, onRemovePlayer })
export default PlayersModal

View file

@ -331,3 +331,5 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
export default QuestionsModal