multiplayer
This commit is contained in:
parent
bfd202bf68
commit
3c612f4491
8 changed files with 356 additions and 11 deletions
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,3 +73,5 @@ const PlayersModal = ({ isOpen, onClose, players, onAddPlayer, onRemovePlayer })
|
|||
export default PlayersModal
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -331,3 +331,5 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
|
|||
export default QuestionsModal
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue