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';
|
2026-01-10 20:58:51 +00:00
|
|
|
|
import { Inject, forwardRef } from '@nestjs/common';
|
2026-01-03 14:07:04 +00:00
|
|
|
|
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-08 22:44:20 +00:00
|
|
|
|
import { Prisma } from '@prisma/client';
|
2026-01-03 14:07:04 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
interface PlayerAction {
|
|
|
|
|
|
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
|
|
|
|
|
|
roomId: string;
|
|
|
|
|
|
roomCode: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
// Для revealAnswer:
|
|
|
|
|
|
questionId?: string; // UUID вопроса
|
|
|
|
|
|
answerId?: string; // UUID ответа
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
interface Question {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
text?: string;
|
|
|
|
|
|
answers: Array<{
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
text: string;
|
|
|
|
|
|
points: number;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type RoomWithPack = Prisma.RoomGetPayload<{
|
|
|
|
|
|
include: {
|
|
|
|
|
|
roomPack: true;
|
|
|
|
|
|
participants: true;
|
2026-01-10 15:51:33 +00:00
|
|
|
|
theme: true;
|
2026-01-08 22:44:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
}> & {
|
|
|
|
|
|
currentQuestionId?: string | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
interface RevealedAnswers {
|
|
|
|
|
|
[questionId: string]: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
|
@WebSocketGateway({
|
|
|
|
|
|
cors: {
|
|
|
|
|
|
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(
|
2026-01-10 20:58:51 +00:00
|
|
|
|
@Inject(forwardRef(() => RoomsService))
|
2026-01-07 14:32:51 +00:00
|
|
|
|
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> {
|
2026-01-10 15:51:33 +00:00
|
|
|
|
// Проверяем роль участника (role === 'HOST') для поддержки нескольких хостов
|
|
|
|
|
|
const participant = await this.prisma.participant.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
roomId,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
role: 'HOST',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
2026-01-08 13:18:07 +00:00
|
|
|
|
});
|
2026-01-10 15:51:33 +00:00
|
|
|
|
return !!participant;
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 14:07:04 +00:00
|
|
|
|
@SubscribeMessage('joinRoom')
|
|
|
|
|
|
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
|
|
|
|
|
|
client.join(payload.roomCode);
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-03 14:07:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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');
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
// Инициализировать первый вопрос и игрока
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
2026-01-08 21:44:38 +00:00
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
roomPack: true,
|
|
|
|
|
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
2026-01-08 22:44:20 +00:00
|
|
|
|
} as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
2026-01-03 14:07:04 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (room) {
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const firstQuestion = questions[0];
|
2026-01-08 22:56:11 +00:00
|
|
|
|
|
|
|
|
|
|
// Админ (хост) должен быть первым игроком
|
|
|
|
|
|
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
|
|
|
|
|
|
const firstParticipant = hostParticipant || room.participants[0];
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
// Убеждаемся что firstQuestion.id - строка (UUID)
|
|
|
|
|
|
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
|
|
|
|
|
? firstQuestion.id
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
if (firstQuestionId && firstParticipant) {
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: {
|
2026-01-08 22:44:20 +00:00
|
|
|
|
currentQuestionId: firstQuestionId,
|
|
|
|
|
|
currentQuestionIndex: 0,
|
2026-01-08 21:44:38 +00:00
|
|
|
|
currentPlayerId: firstParticipant.id,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-09 21:36:49 +00:00
|
|
|
|
|
|
|
|
|
|
// Явно отправить событие начала игры для перенаправления всех игроков
|
|
|
|
|
|
this.server.to(payload.roomCode).emit('gameStarted', {
|
|
|
|
|
|
roomId: payload.roomId,
|
|
|
|
|
|
roomCode: payload.roomCode,
|
|
|
|
|
|
status: 'PLAYING'
|
|
|
|
|
|
});
|
2026-01-03 14:07:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
@SubscribeMessage('playerAction')
|
|
|
|
|
|
async handlePlayerAction(client: Socket, payload: PlayerAction) {
|
|
|
|
|
|
// Получаем комнату с данными
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
2026-01-08 21:44:38 +00:00
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
roomPack: true,
|
|
|
|
|
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
2026-01-08 22:44:20 +00:00
|
|
|
|
} as unknown as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
if (!room) {
|
|
|
|
|
|
client.emit('error', { message: 'Room not found' });
|
2026-01-08 20:14:58 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 15:51:33 +00:00
|
|
|
|
// Получаем участника для проверки роли
|
|
|
|
|
|
const participant = room.participants.find(p => p.id === payload.participantId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!participant) {
|
|
|
|
|
|
client.emit('error', { message: 'Participant not found' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем роль участника - зрители не могут выполнять действия игрока
|
|
|
|
|
|
if (participant.role === 'SPECTATOR') {
|
|
|
|
|
|
client.emit('error', { message: 'Spectators cannot perform player actions' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
// Проверяем права
|
2026-01-10 15:51:33 +00:00
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const isCurrentPlayer = room.currentPlayerId === payload.participantId;
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (!isHost && !isCurrentPlayer) {
|
|
|
|
|
|
client.emit('error', { message: 'Not your turn!' });
|
2026-01-08 20:14:58 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
// Выполняем действие
|
|
|
|
|
|
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' });
|
|
|
|
|
|
}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
private async handleRevealAnswerAction(payload: PlayerAction, room: any) {
|
2026-01-08 21:58:00 +00:00
|
|
|
|
if (!payload.questionId || !payload.answerId) {
|
|
|
|
|
|
console.error('Missing questionId or answerId in payload');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const question = questions.find(q => q.id === payload.questionId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!question) {
|
|
|
|
|
|
console.error('Question not found:', payload.questionId);
|
2026-01-08 20:14:58 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const answer = question.answers?.find((a: any) => a.id === payload.answerId);
|
|
|
|
|
|
if (!answer) {
|
|
|
|
|
|
console.error('Answer not found:', payload.answerId);
|
2026-01-08 13:18:07 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
// Обновляем revealedAnswers
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
|
|
|
|
|
|
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
|
2026-01-03 14:07:04 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (!currentRevealed.includes(payload.answerId)) {
|
|
|
|
|
|
currentRevealed.push(payload.answerId);
|
|
|
|
|
|
revealed[payload.questionId] = currentRevealed;
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
2026-01-10 20:49:42 +00:00
|
|
|
|
// Начисляем очки текущему игроку (не тому, кто открыл ответ)
|
|
|
|
|
|
if (room.currentPlayerId) {
|
|
|
|
|
|
await this.prisma.participant.update({
|
|
|
|
|
|
where: { id: room.currentPlayerId },
|
|
|
|
|
|
data: { score: { increment: answer.points } }
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
// Сохраняем revealedAnswers
|
2026-01-08 20:14:58 +00:00
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
2026-01-08 22:44:20 +00:00
|
|
|
|
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
|
2026-01-08 20:14:58 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-10 20:49:42 +00:00
|
|
|
|
// Определяем следующего игрока на основе текущего игрока
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const participants = room.participants;
|
2026-01-10 20:49:42 +00:00
|
|
|
|
const currentIdx = participants.findIndex((p) => p.id === room.currentPlayerId);
|
2026-01-08 21:44:38 +00:00
|
|
|
|
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 }
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
private async handleNextQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
|
|
|
|
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
|
|
|
|
|
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (currentIdx < questions.length - 1) {
|
|
|
|
|
|
const nextQuestion = questions[currentIdx + 1];
|
2026-01-08 20:14:58 +00:00
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
2026-01-08 21:44:38 +00:00
|
|
|
|
data: {
|
|
|
|
|
|
currentQuestionId: nextQuestion.id,
|
2026-01-08 22:44:20 +00:00
|
|
|
|
currentQuestionIndex: currentIdx + 1
|
2026-01-08 21:44:38 +00:00
|
|
|
|
}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
});
|
2026-01-08 21:44:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
private async handlePrevQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
|
|
|
|
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
|
|
|
|
|
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (currentIdx > 0) {
|
|
|
|
|
|
const prevQuestion = questions[currentIdx - 1];
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
currentQuestionId: prevQuestion.id,
|
2026-01-08 22:44:20 +00:00
|
|
|
|
currentQuestionIndex: currentIdx - 1
|
2026-01-08 21:44:38 +00:00
|
|
|
|
}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-03 14:07:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
|
2026-01-10 20:49:42 +00:00
|
|
|
|
public async broadcastFullState(roomCode: string) {
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
2026-01-08 21:44:38 +00:00
|
|
|
|
where: { code: roomCode },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
participants: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
orderBy: { joinedAt: 'asc' }
|
|
|
|
|
|
},
|
|
|
|
|
|
roomPack: true,
|
2026-01-10 15:51:33 +00:00
|
|
|
|
host: { select: { id: true, name: true } },
|
|
|
|
|
|
theme: true
|
2026-01-08 22:44:20 +00:00
|
|
|
|
} as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (!room) return;
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
2026-01-08 23:05:03 +00:00
|
|
|
|
// Извлекаем вопросы из roomPack.questions (JSON поле)
|
|
|
|
|
|
const roomPackQuestions = (room.roomPack as unknown as { questions?: any } | null)?.questions;
|
|
|
|
|
|
let questions: Question[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (roomPackQuestions) {
|
|
|
|
|
|
// Если это уже массив, используем как есть
|
|
|
|
|
|
if (Array.isArray(roomPackQuestions)) {
|
|
|
|
|
|
questions = roomPackQuestions as Question[];
|
|
|
|
|
|
} else if (typeof roomPackQuestions === 'string') {
|
|
|
|
|
|
// Если это строка, парсим JSON
|
|
|
|
|
|
try {
|
|
|
|
|
|
questions = JSON.parse(roomPackQuestions) as Question[];
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Error parsing roomPack.questions:', e);
|
|
|
|
|
|
questions = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📋 Room ${roomCode}: Found ${questions.length} questions`);
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
// Инициализация currentQuestionId если не установлен или невалиден
|
|
|
|
|
|
let currentQuestionId = (room.currentQuestionId as string | null) || null;
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем, что currentQuestionId валиден (существует в вопросах)
|
|
|
|
|
|
if (currentQuestionId) {
|
|
|
|
|
|
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
|
|
|
|
|
if (!questionExists) {
|
|
|
|
|
|
currentQuestionId = null; // Сбрасываем если вопрос удален
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Устанавливаем первый вопрос если нет текущего
|
2026-01-08 21:44:38 +00:00
|
|
|
|
if (!currentQuestionId && questions.length > 0) {
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const firstQuestion = questions[0];
|
|
|
|
|
|
// Убеждаемся что id - строка (UUID)
|
|
|
|
|
|
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
|
|
|
|
|
|
currentQuestionId = firstQuestion.id;
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: room.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
currentQuestionId: currentQuestionId,
|
|
|
|
|
|
currentQuestionIndex: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:56:11 +00:00
|
|
|
|
// Инициализация currentPlayerId если не установлен
|
|
|
|
|
|
let currentPlayerId = room.currentPlayerId;
|
|
|
|
|
|
if (!currentPlayerId && room.participants.length > 0) {
|
|
|
|
|
|
// Админ (хост) должен быть первым игроком
|
|
|
|
|
|
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
|
|
|
|
|
|
const firstParticipant = hostParticipant || room.participants[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (firstParticipant) {
|
|
|
|
|
|
currentPlayerId = firstParticipant.id;
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: room.id },
|
|
|
|
|
|
data: { currentPlayerId: currentPlayerId }
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const fullState = {
|
|
|
|
|
|
roomId: room.id,
|
|
|
|
|
|
roomCode: room.code,
|
|
|
|
|
|
status: room.status,
|
|
|
|
|
|
currentQuestionId: currentQuestionId,
|
2026-01-08 22:56:11 +00:00
|
|
|
|
currentPlayerId: currentPlayerId,
|
2026-01-08 22:44:20 +00:00
|
|
|
|
revealedAnswers: room.revealedAnswers as RevealedAnswers,
|
2026-01-08 21:44:38 +00:00
|
|
|
|
isGameOver: room.isGameOver,
|
|
|
|
|
|
hostId: room.hostId,
|
2026-01-10 15:51:33 +00:00
|
|
|
|
themeId: (room as any).themeId || null,
|
|
|
|
|
|
particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null,
|
2026-01-10 16:44:06 +00:00
|
|
|
|
maxPlayers: (room as any).maxPlayers || 10,
|
2026-01-08 22:44:20 +00:00
|
|
|
|
participants: room.participants.map((p) => ({
|
2026-01-08 21:44:38 +00:00
|
|
|
|
id: p.id,
|
|
|
|
|
|
userId: p.userId,
|
|
|
|
|
|
name: p.name,
|
|
|
|
|
|
role: p.role,
|
|
|
|
|
|
score: p.score
|
|
|
|
|
|
})),
|
2026-01-08 23:05:03 +00:00
|
|
|
|
questions: questions.map((q: any) => {
|
|
|
|
|
|
// Убеждаемся, что у вопроса есть id
|
|
|
|
|
|
const questionId = q.id || (typeof q === 'object' && 'id' in q ? q.id : null);
|
|
|
|
|
|
if (!questionId) {
|
|
|
|
|
|
console.warn('⚠️ Question without ID:', q);
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: questionId || `temp-${Math.random()}`,
|
2026-01-10 18:01:01 +00:00
|
|
|
|
text: q.text || '',
|
2026-01-08 23:05:03 +00:00
|
|
|
|
answers: (q.answers || []).map((a: any) => ({
|
|
|
|
|
|
id: a.id || `answer-${Math.random()}`,
|
|
|
|
|
|
text: a.text || '',
|
|
|
|
|
|
points: a.points || 0
|
|
|
|
|
|
}))
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
2026-01-08 21:44:38 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.server.to(roomCode).emit('gameStateUpdated', fullState);
|
|
|
|
|
|
}
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
@SubscribeMessage('requestFullState')
|
|
|
|
|
|
async handleRequestFullState(client: Socket, payload: { roomCode: string }) {
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
@SubscribeMessage('endGame')
|
|
|
|
|
|
async handleEndGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) {
|
2026-01-08 13:18:07 +00:00
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
2026-01-08 21:44:38 +00:00
|
|
|
|
client.emit('error', { message: 'Only the host can end the game' });
|
2026-01-08 13:18:07 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED');
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
2026-01-08 21:44:38 +00:00
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
roomPack: true,
|
|
|
|
|
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
2026-01-08 22:44:20 +00:00
|
|
|
|
} as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
if (room) {
|
|
|
|
|
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
|
|
|
|
|
const firstQuestion = questions[0];
|
2026-01-08 22:56:11 +00:00
|
|
|
|
|
|
|
|
|
|
// Админ (хост) должен быть первым игроком
|
|
|
|
|
|
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
|
|
|
|
|
|
const firstParticipant = hostParticipant || room.participants[0];
|
2026-01-08 21:44:38 +00:00
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
// Убеждаемся что firstQuestion.id - строка (UUID)
|
|
|
|
|
|
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
|
|
|
|
|
? firstQuestion.id
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
status: 'WAITING',
|
|
|
|
|
|
currentQuestionId: firstQuestionId,
|
|
|
|
|
|
currentQuestionIndex: 0,
|
|
|
|
|
|
revealedAnswers: {} as Prisma.InputJsonValue,
|
|
|
|
|
|
currentPlayerId: firstParticipant?.id || null,
|
|
|
|
|
|
isGameOver: false,
|
|
|
|
|
|
answeredQuestions: 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-08 13:18:07 +00:00
|
|
|
|
|
|
|
|
|
|
await this.prisma.participant.updateMany({
|
|
|
|
|
|
where: { roomId: payload.roomId },
|
|
|
|
|
|
data: { score: 0 },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-08 17:56:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:56:11 +00:00
|
|
|
|
@SubscribeMessage('setCurrentPlayer')
|
|
|
|
|
|
async handleSetCurrentPlayer(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 set current player' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем, что участник существует и активен
|
|
|
|
|
|
const participant = await this.prisma.participant.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: payload.participantId,
|
|
|
|
|
|
roomId: payload.roomId,
|
|
|
|
|
|
isActive: true
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!participant) {
|
|
|
|
|
|
client.emit('error', { message: 'Participant not found' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: { currentPlayerId: payload.participantId }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 15:51:33 +00:00
|
|
|
|
@SubscribeMessage('changeRoomTheme')
|
|
|
|
|
|
async handleChangeRoomTheme(client: Socket, payload: { roomId: string; roomCode: string; userId: string; themeId: string | null }) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only the host can change room theme' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: { themeId: payload.themeId } as Prisma.RoomUpdateInput
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('toggleParticles')
|
|
|
|
|
|
async handleToggleParticles(client: Socket, payload: { roomId: string; roomCode: string; userId: string; particlesEnabled: boolean }) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only the host can toggle particles' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: { particlesEnabled: payload.particlesEnabled } as any
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 16:44:06 +00:00
|
|
|
|
@SubscribeMessage('addPlayer')
|
|
|
|
|
|
async handleAddPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; playerName: string; role?: 'PLAYER' | 'SPECTATOR' }) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only the host can add players' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.roomsService.addPlayerManually(
|
|
|
|
|
|
payload.roomId,
|
|
|
|
|
|
payload.userId,
|
|
|
|
|
|
payload.playerName,
|
|
|
|
|
|
payload.role || 'PLAYER',
|
|
|
|
|
|
);
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
client.emit('error', { message: error.message || 'Failed to add player' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 17:56:00 +00:00
|
|
|
|
@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-09 21:36:49 +00:00
|
|
|
|
// Проверить uiControls
|
|
|
|
|
|
const roomForControls = await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
}) as any;
|
|
|
|
|
|
|
|
|
|
|
|
const uiControls = roomForControls?.uiControls as { allowPackChange?: boolean } | null;
|
|
|
|
|
|
if (uiControls && uiControls.allowPackChange === false) {
|
|
|
|
|
|
client.emit('error', { message: 'Pack editing is disabled for this room' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 22:44:20 +00:00
|
|
|
|
// Обновляем вопросы через service (который добавит UUID если нужно)
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
2026-01-08 22:44:20 +00:00
|
|
|
|
|
|
|
|
|
|
// После обновления вопросов проверяем и обновляем currentQuestionId
|
|
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
include: { roomPack: true } as unknown as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
|
|
|
|
|
|
|
|
|
|
|
if (room) {
|
|
|
|
|
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
|
|
|
|
|
const currentQuestionId = (room.currentQuestionId as string | null) || null;
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем, что currentQuestionId все еще валиден
|
|
|
|
|
|
let validQuestionId = currentQuestionId;
|
|
|
|
|
|
if (currentQuestionId) {
|
|
|
|
|
|
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
|
|
|
|
|
if (!questionExists) {
|
|
|
|
|
|
// Текущий вопрос был удален, устанавливаем первый
|
|
|
|
|
|
validQuestionId = questions[0]?.id && typeof questions[0].id === 'string'
|
|
|
|
|
|
? questions[0].id
|
|
|
|
|
|
: null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (questions.length > 0) {
|
|
|
|
|
|
// Если нет currentQuestionId, устанавливаем первый
|
|
|
|
|
|
validQuestionId = questions[0].id && typeof questions[0].id === 'string'
|
|
|
|
|
|
? questions[0].id
|
|
|
|
|
|
: null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем currentQuestionId если изменился
|
|
|
|
|
|
if (validQuestionId !== currentQuestionId) {
|
|
|
|
|
|
const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId);
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
currentQuestionId: validQuestionId,
|
|
|
|
|
|
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-08 17:56:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
// Проверить uiControls
|
|
|
|
|
|
const roomForControls = await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
}) as any;
|
|
|
|
|
|
|
|
|
|
|
|
const uiControls = roomForControls?.uiControls as { allowPackChange?: boolean } | null;
|
|
|
|
|
|
if (uiControls && uiControls.allowPackChange === false) {
|
|
|
|
|
|
client.emit('error', { message: 'Pack editing is disabled for this room' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 17:56:00 +00:00
|
|
|
|
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices);
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 00:18:08 +00:00
|
|
|
|
// Получаем комнату с участниками
|
|
|
|
|
|
const room = (await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
|
|
|
|
|
} as Prisma.RoomInclude,
|
|
|
|
|
|
})) as unknown as RoomWithPack | null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!room) {
|
|
|
|
|
|
client.emit('error', { message: 'Room not found' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем существование и активность участника
|
|
|
|
|
|
const participant = await this.prisma.participant.findUnique({
|
|
|
|
|
|
where: { id: payload.participantId },
|
|
|
|
|
|
include: { user: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!participant || participant.roomId !== payload.roomId) {
|
|
|
|
|
|
client.emit('error', { message: 'Participant not found' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!participant.isActive) {
|
|
|
|
|
|
client.emit('error', { message: 'Participant is already inactive' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Запрещаем удаление хоста
|
2026-01-10 15:51:33 +00:00
|
|
|
|
if (participant.role === 'HOST') {
|
2026-01-10 00:18:08 +00:00
|
|
|
|
client.emit('error', { message: 'Cannot kick the host' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Если удаляемый участник - текущий игрок, выбираем следующего
|
|
|
|
|
|
let newCurrentPlayerId = room.currentPlayerId;
|
|
|
|
|
|
if (room.currentPlayerId === payload.participantId) {
|
|
|
|
|
|
const activeParticipants = room.participants.filter(p => p.id !== payload.participantId);
|
|
|
|
|
|
if (activeParticipants.length > 0) {
|
|
|
|
|
|
// Выбираем первого активного участника после удаляемого
|
|
|
|
|
|
newCurrentPlayerId = activeParticipants[0].id;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newCurrentPlayerId = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Деактивируем участника
|
2026-01-08 13:18:07 +00:00
|
|
|
|
await this.prisma.participant.update({
|
|
|
|
|
|
where: { id: payload.participantId },
|
|
|
|
|
|
data: { isActive: false },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-10 00:18:08 +00:00
|
|
|
|
// Обновляем currentPlayerId если нужно
|
|
|
|
|
|
if (newCurrentPlayerId !== room.currentPlayerId) {
|
|
|
|
|
|
await this.prisma.room.update({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
data: { currentPlayerId: newCurrentPlayerId },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Отправляем событие об удалении
|
|
|
|
|
|
this.roomEventsService.emitPlayerKicked(payload.roomCode, {
|
|
|
|
|
|
participantId: payload.participantId,
|
2026-01-10 16:44:06 +00:00
|
|
|
|
userId: participant.userId || null,
|
2026-01-10 00:18:08 +00:00
|
|
|
|
participantName: participant.name,
|
|
|
|
|
|
newCurrentPlayerId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Отправляем обновленное состояние
|
2026-01-08 21:44:38 +00:00
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
2026-01-08 13:18:07 +00:00
|
|
|
|
}
|
2026-01-09 16:44:33 +00:00
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('updatePlayerName')
|
|
|
|
|
|
async handleUpdatePlayerName(client: Socket, payload: {
|
|
|
|
|
|
roomId: string;
|
|
|
|
|
|
roomCode: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
newName: string;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only the host can update player names' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
// Проверить uiControls
|
|
|
|
|
|
const roomForControls = await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
}) as any;
|
|
|
|
|
|
|
|
|
|
|
|
const uiControls = roomForControls?.uiControls as { allowNameChange?: boolean } | null;
|
|
|
|
|
|
if (uiControls && uiControls.allowNameChange === false) {
|
|
|
|
|
|
client.emit('error', { message: 'Name editing is disabled for this room' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 16:44:33 +00:00
|
|
|
|
if (!payload.newName || payload.newName.trim().length === 0) {
|
|
|
|
|
|
client.emit('error', { message: 'Name cannot be empty' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.newName.trim().length > 50) {
|
|
|
|
|
|
client.emit('error', { message: 'Name is too long (max 50 characters)' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.participant.update({
|
|
|
|
|
|
where: { id: payload.participantId },
|
|
|
|
|
|
data: { name: payload.newName.trim() }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('updatePlayerScore')
|
|
|
|
|
|
async handleUpdatePlayerScore(client: Socket, payload: {
|
|
|
|
|
|
roomId: string;
|
|
|
|
|
|
roomCode: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
newScore: number;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only the host can update scores' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
// Проверить uiControls
|
|
|
|
|
|
const roomForControls = await this.prisma.room.findUnique({
|
|
|
|
|
|
where: { id: payload.roomId },
|
|
|
|
|
|
}) as any;
|
|
|
|
|
|
|
|
|
|
|
|
const uiControls = roomForControls?.uiControls as { allowScoreEdit?: boolean } | null;
|
|
|
|
|
|
if (uiControls && uiControls.allowScoreEdit === false) {
|
|
|
|
|
|
client.emit('error', { message: 'Score editing is disabled for this room' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 16:44:33 +00:00
|
|
|
|
if (typeof payload.newScore !== 'number' || isNaN(payload.newScore)) {
|
|
|
|
|
|
client.emit('error', { message: 'Invalid score value' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.participant.update({
|
|
|
|
|
|
where: { id: payload.participantId },
|
|
|
|
|
|
data: { score: Math.round(payload.newScore) }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
}
|
2026-01-10 15:51:33 +00:00
|
|
|
|
|
|
|
|
|
|
@SubscribeMessage('changeParticipantRole')
|
|
|
|
|
|
async handleChangeParticipantRole(client: Socket, payload: {
|
|
|
|
|
|
roomId: string;
|
|
|
|
|
|
roomCode: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
newRole: 'HOST' | 'PLAYER' | 'SPECTATOR';
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
|
|
|
|
|
if (!isHost) {
|
|
|
|
|
|
client.emit('error', { message: 'Only hosts can change participant roles' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const room = await this.roomsService.updateParticipantRole(
|
|
|
|
|
|
payload.roomId,
|
|
|
|
|
|
payload.participantId,
|
|
|
|
|
|
payload.newRole,
|
|
|
|
|
|
payload.userId,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await this.broadcastFullState(payload.roomCode);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error changing participant role:', error);
|
|
|
|
|
|
client.emit('error', {
|
|
|
|
|
|
message: error.message || 'Failed to change participant role'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-03 14:07:04 +00:00
|
|
|
|
}
|