2026-01-03 14:07:04 +00:00
|
|
|
import {
|
|
|
|
|
WebSocketGateway,
|
|
|
|
|
WebSocketServer,
|
|
|
|
|
SubscribeMessage,
|
|
|
|
|
OnGatewayConnection,
|
|
|
|
|
OnGatewayDisconnect,
|
2026-01-07 14:32:51 +00:00
|
|
|
OnGatewayInit,
|
2026-01-03 14:07:04 +00:00
|
|
|
} from '@nestjs/websockets';
|
|
|
|
|
import { Server, Socket } from 'socket.io';
|
|
|
|
|
import { RoomsService } from '../rooms/rooms.service';
|
2026-01-07 14:32:51 +00:00
|
|
|
import { RoomEventsService } from './room-events.service';
|
2026-01-08 13:18:07 +00:00
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
2026-01-08 17:56:00 +00:00
|
|
|
import { RoomPackService } from '../room-pack/room-pack.service';
|
2026-01-03 14:07:04 +00:00
|
|
|
|
|
|
|
|
@WebSocketGateway({
|
|
|
|
|
cors: {
|
2026-01-04 21:48:55 +00:00
|
|
|
// Примечание: декоратор выполняется на этапе инициализации,
|
|
|
|
|
// ConfigModule.forRoot() уже загружает переменные в process.env
|
2026-01-03 14:07:04 +00:00
|
|
|
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
|
|
|
|
credentials: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-01-07 14:32:51 +00:00
|
|
|
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
|
2026-01-03 14:07:04 +00:00
|
|
|
@WebSocketServer()
|
|
|
|
|
server: Server;
|
|
|
|
|
|
2026-01-07 14:32:51 +00:00
|
|
|
constructor(
|
|
|
|
|
private roomsService: RoomsService,
|
|
|
|
|
private roomEventsService: RoomEventsService,
|
2026-01-08 13:18:07 +00:00
|
|
|
private prisma: PrismaService,
|
2026-01-08 17:56:00 +00:00
|
|
|
private roomPackService: RoomPackService,
|
2026-01-07 14:32:51 +00:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
afterInit(server: Server) {
|
|
|
|
|
this.roomEventsService.setServer(server);
|
|
|
|
|
}
|
2026-01-03 14:07:04 +00:00
|
|
|
|
|
|
|
|
handleConnection(client: Socket) {
|
|
|
|
|
console.log(`Client connected: ${client.id}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleDisconnect(client: Socket) {
|
|
|
|
|
console.log(`Client disconnected: ${client.id}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 13:18:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
@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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('startGame')
|
2026-01-08 13:18:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
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')
|
2026-01-08 20:14:58 +00:00
|
|
|
async handleRevealAnswer(client: Socket, payload: { roomCode: string; answerIndex: number; userId: string; roomId: string; questionIndex?: number }) {
|
2026-01-08 13:18:07 +00:00
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
client.emit('error', { message: 'Only the host can reveal answers' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
this.server.to(payload.roomCode).emit('answerRevealed', payload);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:14:58 +00:00
|
|
|
@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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
@SubscribeMessage('updateScore')
|
2026-01-08 13:18:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
await this.roomsService.updateParticipantScore(payload.participantId, payload.score);
|
|
|
|
|
this.server.to(payload.roomCode).emit('scoreUpdated', payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('nextQuestion')
|
2026-01-08 13:18:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:14:58 +00:00
|
|
|
const room = await this.prisma.room.findUnique({
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
select: { currentQuestionIndex: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (room) {
|
|
|
|
|
const newIndex = (room.currentQuestionIndex || 0) + 1;
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
data: { currentQuestionIndex: newIndex },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.server.to(payload.roomCode).emit('questionChanged', {
|
|
|
|
|
...payload,
|
|
|
|
|
questionIndex: newIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const room = await this.prisma.room.findUnique({
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
select: { currentQuestionIndex: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (room && room.currentQuestionIndex > 0) {
|
|
|
|
|
const newIndex = room.currentQuestionIndex - 1;
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
data: { currentQuestionIndex: newIndex },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.server.to(payload.roomCode).emit('questionChanged', {
|
|
|
|
|
...payload,
|
|
|
|
|
questionIndex: newIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-03 14:07:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('endGame')
|
2026-01-08 13:18:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED');
|
|
|
|
|
this.server.to(payload.roomCode).emit('gameEnded', payload);
|
|
|
|
|
}
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
|
|
|
@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 }) {
|
2026-01-08 17:56:00 +00:00
|
|
|
// DEPRECATED: Use updateRoomPack instead
|
|
|
|
|
return this.handleUpdateRoomPack(client, payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('updateRoomPack')
|
|
|
|
|
async handleUpdateRoomPack(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any[] }) {
|
2026-01-08 13:18:07 +00:00
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
client.emit('error', { message: 'Only the host can update questions' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 17:56:00 +00:00
|
|
|
const room = await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
|
|
|
|
this.server.to(payload.roomCode).emit('roomPackUpdated', room);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('importQuestions')
|
|
|
|
|
async handleImportQuestions(client: Socket, payload: {
|
|
|
|
|
roomId: string;
|
|
|
|
|
roomCode: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
sourcePackId: string;
|
|
|
|
|
questionIndices: number[];
|
|
|
|
|
}) {
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
client.emit('error', { message: 'Only the host can import questions' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices);
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
|
|
|
const room = await this.roomsService.getRoomByCode(payload.roomCode);
|
2026-01-08 17:56:00 +00:00
|
|
|
this.server.to(payload.roomCode).emit('roomPackUpdated', room);
|
2026-01-08 13:18:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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 });
|
|
|
|
|
}
|
2026-01-03 14:07:04 +00:00
|
|
|
}
|