sto-k-odnomu/backend/src/game/game.gateway.ts
2026-01-11 08:36:50 +03:00

1031 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
} from '@nestjs/websockets';
import { Inject, forwardRef } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { RoomsService } from '../rooms/rooms.service';
import { RoomEventsService } from './room-events.service';
import { PrismaService } from '../prisma/prisma.service';
import { RoomPackService } from '../room-pack/room-pack.service';
import { Prisma } from '@prisma/client';
interface PlayerAction {
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
roomId: string;
roomCode: string;
userId: string;
participantId: string;
// Для revealAnswer:
questionId?: string; // UUID вопроса
answerId?: string; // UUID ответа
}
interface Question {
id: string;
text?: string;
answers: Array<{
id: string;
text: string;
points: number;
}>;
}
type RoomWithPack = Prisma.RoomGetPayload<{
include: {
roomPack: true;
participants: true;
theme: true;
}
}> & {
currentQuestionId?: string | null;
};
interface RevealedAnswers {
[questionId: string]: string[];
}
@WebSocketGateway({
cors: {
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
},
})
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
@WebSocketServer()
server: Server;
constructor(
@Inject(forwardRef(() => RoomsService))
private roomsService: RoomsService,
private roomEventsService: RoomEventsService,
private prisma: PrismaService,
private roomPackService: RoomPackService,
) {}
afterInit(server: Server) {
this.roomEventsService.setServer(server);
}
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
private async isHost(roomId: string, userId: string): Promise<boolean> {
// Проверяем роль участника (role === 'HOST') для поддержки нескольких хостов
const participant = await this.prisma.participant.findFirst({
where: {
roomId,
userId,
role: 'HOST',
isActive: true,
},
});
return !!participant;
}
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;
}
@SubscribeMessage('joinRoom')
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
console.log(`🔌 Client ${client.id} joining WebSocket room ${payload.roomCode}, userId: ${payload.userId}`);
client.join(payload.roomCode);
// Получаем полное состояние для отправки присоединившемуся клиенту
const room = (await this.prisma.room.findUnique({
where: { code: payload.roomCode },
include: {
participants: {
where: { isActive: true },
orderBy: { joinedAt: 'asc' }
},
roomPack: true,
host: { select: { id: true, name: true } },
theme: true
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
// Используем тот же метод, что и в broadcastFullState, но отправляем напрямую клиенту
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') {
try {
questions = JSON.parse(roomPackQuestions) as Question[];
} catch (e) {
console.error('Error parsing roomPack.questions:', e);
questions = [];
}
}
}
let currentQuestionId = (room.currentQuestionId as string | null) || null;
if (currentQuestionId) {
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
if (!questionExists) {
currentQuestionId = null;
}
}
if (!currentQuestionId && questions.length > 0) {
const firstQuestion = questions[0];
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
currentQuestionId = firstQuestion.id;
await this.prisma.room.update({
where: { id: room.id },
data: {
currentQuestionId: currentQuestionId,
currentQuestionIndex: 0
}
});
}
}
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 }
});
}
}
const fullState = {
roomId: room.id,
roomCode: room.code,
status: room.status,
currentQuestionId: currentQuestionId,
currentPlayerId: currentPlayerId,
revealedAnswers: room.revealedAnswers as RevealedAnswers,
isGameOver: room.isGameOver,
hostId: room.hostId,
themeId: (room as any).themeId || null,
voiceMode: (room as any).voiceMode !== undefined ? (room as any).voiceMode : false,
particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null,
maxPlayers: (room as any).maxPlayers || 10,
participants: room.participants.map((p) => ({
id: p.id,
userId: p.userId,
name: p.name,
role: p.role,
score: p.score
})),
questions: questions.map((q: any) => {
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()}`,
text: q.text || '',
answers: (q.answers || []).map((a: any) => ({
id: a.id || `answer-${Math.random()}`,
text: a.text || '',
points: a.points || 0
}))
};
})
};
// Отправляем состояние напрямую присоединившемуся клиенту
client.emit('gameStateUpdated', fullState);
}
// Также отправляем всем остальным в комнате (broadcast)
await this.broadcastFullState(payload.roomCode);
}
@SubscribeMessage('startGame')
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.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const firstQuestion = questions[0];
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
// Убеждаемся что firstQuestion.id - строка (UUID)
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
? firstQuestion.id
: null;
if (firstQuestionId && firstParticipant) {
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: firstQuestionId,
currentQuestionIndex: 0,
currentPlayerId: firstParticipant.id,
}
});
}
}
await this.broadcastFullState(payload.roomCode);
// Явно отправить событие начала игры для перенаправления всех игроков
this.server.to(payload.roomCode).emit('gameStarted', {
roomId: payload.roomId,
roomCode: payload.roomCode,
status: 'PLAYING'
});
}
@SubscribeMessage('playerAction')
async handlePlayerAction(client: Socket, payload: PlayerAction) {
// Получаем комнату с данными
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (!room) {
client.emit('error', { message: 'Room not found' });
return;
}
// Получаем участника для проверки роли
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;
}
// Проверяем права
const isHost = await this.isHost(payload.roomId, payload.userId);
const isCurrentPlayer = room.currentPlayerId === payload.participantId;
// Только хост может открывать ответы
if (payload.action === 'revealAnswer' && !isHost) {
client.emit('error', { message: 'Only the host can reveal answers' });
return;
}
// Для других действий проверяем права (хост или текущий игрок)
if (payload.action !== 'revealAnswer' && !isHost && !isCurrentPlayer) {
client.emit('error', { message: 'Not your turn!' });
return;
}
// Выполняем действие
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' });
}
}
private async handleRevealAnswerAction(payload: PlayerAction, room: any) {
if (!payload.questionId || !payload.answerId) {
console.error('Missing questionId or answerId in payload');
return;
}
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
const question = questions.find(q => q.id === payload.questionId);
if (!question) {
console.error('Question not found:', payload.questionId);
return;
}
const answer = question.answers?.find((a: any) => a.id === payload.answerId);
if (!answer) {
console.error('Answer not found:', payload.answerId);
return;
}
// Обновляем revealedAnswers
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
if (!currentRevealed.includes(payload.answerId)) {
currentRevealed.push(payload.answerId);
revealed[payload.questionId] = currentRevealed;
// Начисляем очки текущему игроку (не тому, кто открыл ответ)
if (room.currentPlayerId) {
await this.prisma.participant.update({
where: { id: room.currentPlayerId },
data: { score: { increment: answer.points } }
});
}
// Сохраняем revealedAnswers
await this.prisma.room.update({
where: { id: payload.roomId },
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
});
// Определяем следующего игрока на основе текущего игрока
const participants = room.participants;
const currentIdx = participants.findIndex((p) => p.id === room.currentPlayerId);
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 }
});
}
}
}
}
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));
if (currentIdx < questions.length - 1) {
const nextQuestion = questions[currentIdx + 1];
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: nextQuestion.id,
currentQuestionIndex: currentIdx + 1
}
});
}
}
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));
if (currentIdx > 0) {
const prevQuestion = questions[currentIdx - 1];
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: prevQuestion.id,
currentQuestionIndex: currentIdx - 1
}
});
}
}
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
public async broadcastFullState(roomCode: string) {
const room = (await this.prisma.room.findUnique({
where: { code: roomCode },
include: {
participants: {
where: { isActive: true },
orderBy: { joinedAt: 'asc' }
},
roomPack: true,
host: { select: { id: true, name: true } },
theme: true
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (!room) return;
// Извлекаем вопросы из 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`);
// Инициализация 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; // Сбрасываем если вопрос удален
}
}
// Устанавливаем первый вопрос если нет текущего
if (!currentQuestionId && questions.length > 0) {
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
}
});
}
}
// Инициализация 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 }
});
}
}
const fullState = {
roomId: room.id,
roomCode: room.code,
status: room.status,
currentQuestionId: currentQuestionId,
currentPlayerId: currentPlayerId,
revealedAnswers: room.revealedAnswers as RevealedAnswers,
isGameOver: room.isGameOver,
hostId: room.hostId,
themeId: (room as any).themeId || null,
voiceMode: (room as any).voiceMode !== undefined ? (room as any).voiceMode : false,
particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null,
maxPlayers: (room as any).maxPlayers || 10,
participants: room.participants.map((p) => ({
id: p.id,
userId: p.userId,
name: p.name,
role: p.role,
score: p.score
})),
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()}`,
text: q.text || '',
answers: (q.answers || []).map((a: any) => ({
id: a.id || `answer-${Math.random()}`,
text: a.text || '',
points: a.points || 0
}))
};
})
};
console.log(`📡 Broadcasting gameStateUpdated to room ${roomCode} with ${room.participants.length} participants`);
this.server.to(roomCode).emit('gameStateUpdated', fullState);
}
@SubscribeMessage('requestFullState')
async handleRequestFullState(client: Socket, payload: { roomCode: string }) {
await this.broadcastFullState(payload.roomCode);
}
@SubscribeMessage('endGame')
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');
await this.broadcastFullState(payload.roomCode);
}
@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;
}
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const firstQuestion = questions[0];
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
// Убеждаемся что 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,
}
});
}
await this.prisma.participant.updateMany({
where: { roomId: payload.roomId },
data: { score: 0 },
});
await this.broadcastFullState(payload.roomCode);
}
@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);
}
@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);
}
@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' });
}
}
@SubscribeMessage('updateRoomPack')
async handleUpdateRoomPack(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;
}
// Проверить 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;
}
try {
// Убеждаемся, что questions - это массив
const questionsArray = Array.isArray(payload.questions) ? payload.questions : [];
// Обновляем вопросы через service (который добавит UUID если нужно)
await this.roomsService.updateRoomPack(payload.roomId, questionsArray);
// После обновления вопросов проверяем и обновляем 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
}
});
}
}
await this.broadcastFullState(payload.roomCode);
} catch (error: any) {
console.error('Error updating room pack:', error);
client.emit('error', { message: error.message || 'Failed to update questions' });
}
}
@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;
}
// Проверить 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;
}
try {
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices);
await this.broadcastFullState(payload.roomCode);
} catch (error: any) {
console.error('Error importing questions:', error);
client.emit('error', { message: error.message || 'Failed to import questions' });
}
}
@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;
}
// Получаем комнату с участниками
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;
}
// Запрещаем удаление хоста
if (participant.role === 'HOST') {
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;
}
}
// Деактивируем участника
await this.prisma.participant.update({
where: { id: payload.participantId },
data: { isActive: false },
});
// Обновляем 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,
userId: participant.userId || null,
participantName: participant.name,
newCurrentPlayerId,
});
// Отправляем обновленное состояние
await this.broadcastFullState(payload.roomCode);
}
@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;
}
// Проверить 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;
}
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;
}
// Проверить 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;
}
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);
}
@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'
});
}
}
}