// /server/bc.js - Главный файл сервера Battle Club const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); // Импорт серверных модулей из их новых местоположений const authService = require('./auth/authService'); // Сервис аутентификации const GameManager = require('./game/GameManager'); // Менеджер игр const db = require('./core/db'); // Модуль базы данных (для инициализации) const GAME_CONFIG = require('./core/config'); // Глобальный конфиг игры // data.js (теперь data/index.js) и gameLogic.js (теперь game/logic/index.js) // импортируются внутри GameManager и GameInstance или их компонентов. const app = express(); const server = http.createServer(app); // Настройка Socket.IO const io = new Server(server, { cors: { origin: "https://pavel-chagovsky.com:3200", // Для разработки. В продакшене укажите домен клиента. methods: ["GET", "POST"] }, // Можно настроить pingInterval и pingTimeout для более быстрого обнаружения дисконнектов // pingInterval: 10000, // 10 секунд // pingTimeout: 5000, // 5 секунд (клиент должен ответить в течение этого времени) }); // Раздача статических файлов из папки 'public' // __dirname будет указывать на папку server/, поэтому нужно подняться на уровень выше app.use(express.static(path.join(__dirname, '..', 'public'))); // Создаем экземпляр GameManager const gameManager = new GameManager(io); // Хранилище информации о залогиненных пользователях по socket.id // (Временное решение, в продакшене лучше использовать Redis или БД для сессий) const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } // Обработка подключений Socket.IO io.on('connection', (socket) => { console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`); // Привязываем user data к сокету (пока пустые, заполняются при логине) socket.userData = null; // { userId: ..., username: ... } // --- Обработчики событий Аутентификации --- socket.on('register', async (data) => { console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`); const result = await authService.registerUser(data?.username, data?.password); if (result.success) { console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`); } else { console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`); } socket.emit('registerResponse', result); }); socket.on('login', async (data) => { console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`); const result = await authService.loginUser(data?.username, data?.password); if (result.success && result.userId && result.username) { // Убедимся, что userId и username есть console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`); socket.userData = { userId: result.userId, username: result.username }; loggedInUsers[socket.id] = socket.userData; // Сохраняем для быстрого доступа, если нужно // После успешного логина, просим GameManager проверить, не был ли этот пользователь в игре if (gameManager && typeof gameManager.handleRequestGameState === 'function') { gameManager.handleRequestGameState(socket, result.userId); } } else { console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`); socket.userData = null; if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; } socket.emit('loginResponse', result); // Отправляем результат клиенту }); socket.on('logout', () => { const username = socket.userData?.username || 'UnknownUser'; const userId = socket.userData?.userId; console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`); if (gameManager && typeof gameManager.handleDisconnect === 'function' && userId) { // Уведомляем GameManager о "дисконнекте" этого пользователя из его игры, если он там был. // handleDisconnect использует identifier (userId в данном случае) для поиска игры. // Передаем socket.id на случай, если игра была AI и identifier был socket.id (хотя при logout должен быть userId). gameManager.handleDisconnect(socket.id, userId); } if (loggedInUsers[socket.id]) { delete loggedInUsers[socket.id]; } socket.userData = null; // Клиент сам обработает UI после logout (например, покажет экран логина) // Можно отправить подтверждение, но обычно не требуется: socket.emit('logoutResponse', { success: true }); console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out.`); }); // --- Обработчики событий Управления Играми --- // Все эти события делегируются в GameManager socket.on('createGame', (data) => { const identifier = socket.userData?.userId || socket.id; // userId для залогиненных, socket.id для гостей (AI игра) const mode = data?.mode || 'ai'; if (mode === 'pvp' && !socket.userData) { socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' }); return; } console.log(`[Socket.IO] Create Game from ${socket.userData?.username || socket.id} (ID: ${identifier}). Mode: ${mode}, Char: ${data?.characterKey}`); gameManager.createGame(socket, mode, data?.characterKey, identifier); }); socket.on('joinGame', (data) => { if (!socket.userData?.userId) { socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' }); return; } console.log(`[Socket.IO] Join Game from ${socket.userData.username} (ID: ${socket.userData.userId}). GameID: ${data?.gameId}`); gameManager.joinGame(socket, data?.gameId, socket.userData.userId); }); socket.on('findRandomGame', (data) => { if (!socket.userData?.userId) { socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' }); return; } console.log(`[Socket.IO] Find Random Game from ${socket.userData.username} (ID: ${socket.userData.userId}). PrefChar: ${data?.characterKey}`); gameManager.findAndJoinRandomPvPGame(socket, data?.characterKey, socket.userData.userId); }); socket.on('requestPvPGameList', () => { // console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`); const availableGames = gameManager.getAvailablePvPGamesListForClient(); socket.emit('availablePvPGamesList', availableGames); }); socket.on('requestGameState', () => { if (!socket.userData?.userId) { // console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`); socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' }); return; } // console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (ID: ${socket.userData.userId}).`); gameManager.handleRequestGameState(socket, socket.userData.userId); }); // --- Обработчик события Игрового Действия --- socket.on('playerAction', (actionData) => { const identifier = socket.userData?.userId || socket.id; // Идентификатор для GameManager // console.log(`[Socket.IO] Player Action from ${identifier} (socket ${socket.id}):`, actionData); gameManager.handlePlayerAction(identifier, actionData); }); // --- Обработчик отключения сокета --- socket.on('disconnect', (reason) => { const identifier = socket.userData?.userId || socket.id; console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`); gameManager.handleDisconnect(socket.id, identifier); // Передаем и socketId, и identifier if (loggedInUsers[socket.id]) { delete loggedInUsers[socket.id]; } // socket.userData очистится автоматически при уничтожении объекта socket }); }); // Запуск HTTP сервера const PORT = process.env.BC_INTERNAL_PORT || 3200; // Внутренний порт для bc.js const HOSTNAME = '127.0.0.1'; // Слушать ТОЛЬКО на localhost server.listen(PORT, HOSTNAME, () => { // Явно указываем HOSTNAME console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`); console.log(`This server should only be accessed locally by the reverse proxy.`); console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); }); // Обработка необработанных промис-ошибок process.on('unhandledRejection', (reason, promise) => { console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason); // В продакшене здесь может быть более сложная логика или перезапуск процесса // process.exit(1); }); process.on('uncaughtException', (err) => { console.error('[Server FATAL] Uncaught Exception:', err); // Критическая ошибка, обычно требует перезапуска приложения process.exit(1); // Аварийное завершение процесса });