Политика CORS не была исправлена, так что скорее всего игра вызовет ошибки на сервере.
Поправлены насмешки и обеспечен корректный выход из игры.
This commit is contained in:
parent
e1cb3f33b5
commit
cbe25878a2
@ -5,9 +5,13 @@ export function initAuth(dependencies) {
|
|||||||
const { socket, clientState, ui } = dependencies;
|
const { socket, clientState, ui } = dependencies;
|
||||||
const { loginForm, registerForm, logoutButton } = ui.elements; // Получаем нужные DOM элементы
|
const { loginForm, registerForm, logoutButton } = ui.elements; // Получаем нужные DOM элементы
|
||||||
|
|
||||||
// URL вашего API сервера. Лучше вынести в конфигурацию или передавать.
|
// URL вашего API сервера. В данной версии main.js не передает API_BASE_URL,
|
||||||
// Для примера захардкодим, но в main.js можно будет это улучшить.
|
// предполагая, что fetch будет использовать относительные пути к текущему домену.
|
||||||
const API_BASE_URL = dependencies.API_BASE_URL || 'http://127.0.0.1:3200'; // Убедитесь, что это ваш URL
|
// Если ваш main.js снова будет передавать API_BASE_URL, раскомментируйте и используйте его.
|
||||||
|
// const API_BASE_URL = dependencies.API_BASE_URL || ''; // Пустая строка заставит fetch использовать относительные пути
|
||||||
|
// Если API на другом домене, API_BASE_URL обязателен. Для относительных путей:
|
||||||
|
const getApiUrl = (path) => `${window.location.origin}${path}`;
|
||||||
|
|
||||||
|
|
||||||
// Название ключа для хранения JWT в localStorage
|
// Название ключа для хранения JWT в localStorage
|
||||||
const JWT_TOKEN_KEY = 'jwtToken';
|
const JWT_TOKEN_KEY = 'jwtToken';
|
||||||
@ -32,11 +36,12 @@ export function initAuth(dependencies) {
|
|||||||
|
|
||||||
// Важно: переподключить сокет с новым токеном
|
// Важно: переподключить сокет с новым токеном
|
||||||
if (socket.connected) {
|
if (socket.connected) {
|
||||||
socket.disconnect();
|
socket.disconnect(); // Отключаемся, чтобы при следующем connect отправился новый токен
|
||||||
}
|
}
|
||||||
// Обновляем auth объект сокета перед подключением
|
// Обновляем auth объект сокета перед подключением
|
||||||
// В main.js при создании сокета, он должен уже брать токен из localStorage
|
// socket.io клиент автоматически подхватит новый токен из localStorage при следующем .connect(),
|
||||||
// Но если сокет уже существует, нужно обновить его auth данные
|
// если он был инициализирован с auth: () => { token: localStorage.getItem(...) }
|
||||||
|
// или мы можем явно установить его здесь:
|
||||||
socket.auth = { token: data.token };
|
socket.auth = { token: data.token };
|
||||||
socket.connect(); // Это вызовет 'connect' и 'requestGameState' в main.js
|
socket.connect(); // Это вызовет 'connect' и 'requestGameState' в main.js
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ export function initAuth(dependencies) {
|
|||||||
// Разблокируем кнопки в любом случае
|
// Разблокируем кнопки в любом случае
|
||||||
if (regButton) regButton.disabled = false;
|
if (regButton) regButton.disabled = false;
|
||||||
if (loginButton) loginButton.disabled = false;
|
if (loginButton) loginButton.disabled = false;
|
||||||
if (logoutButton && clientState.isLoggedIn) logoutButton.disabled = false;
|
// Кнопка logout управляется состоянием isLoggedIn и видимостью экрана
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,23 +82,27 @@ export function initAuth(dependencies) {
|
|||||||
const password = passwordInput.value;
|
const password = passwordInput.value;
|
||||||
|
|
||||||
const regButton = registerForm.querySelector('button');
|
const regButton = registerForm.querySelector('button');
|
||||||
const loginButton = loginForm ? loginForm.querySelector('button') : null;
|
const loginButton = loginForm ? loginForm.querySelector('button') : null; // Может быть null, если форма логина на другой странице
|
||||||
if (regButton) regButton.disabled = true;
|
if (regButton) regButton.disabled = true;
|
||||||
if (loginButton) loginButton.disabled = true;
|
if (loginButton) loginButton.disabled = true;
|
||||||
|
|
||||||
ui.setAuthMessage('Регистрация...');
|
ui.setAuthMessage('Регистрация...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
// Используем относительный путь, если API_BASE_URL не задан или пуст
|
||||||
|
const response = await fetch(getApiUrl('/auth/register'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
await handleAuthResponse(response, 'register');
|
await handleAuthResponse(response, 'register');
|
||||||
if (response.ok && registerForm) registerForm.reset(); // Очищаем форму при успехе
|
if (response.ok && clientState.isLoggedIn && registerForm) { // Проверяем clientState.isLoggedIn для очистки
|
||||||
|
registerForm.reset(); // Очищаем форму при успехе
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Auth] Network error during registration:', error);
|
console.error('[Auth] Network error during registration:', error);
|
||||||
ui.setAuthMessage('Ошибка сети при регистрации. Пожалуйста, проверьте ваше подключение.', true);
|
ui.setAuthMessage('Ошибка сети при регистрации. Пожалуйста, проверьте ваше подключение.', true);
|
||||||
|
// Разблокируем кнопки при ошибке сети, т.к. finally в handleAuthResponse может не сработать
|
||||||
if (regButton) regButton.disabled = false;
|
if (regButton) regButton.disabled = false;
|
||||||
if (loginButton) loginButton.disabled = false;
|
if (loginButton) loginButton.disabled = false;
|
||||||
}
|
}
|
||||||
@ -118,12 +127,14 @@ export function initAuth(dependencies) {
|
|||||||
ui.setAuthMessage('Вход...');
|
ui.setAuthMessage('Вход...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
// Используем относительный путь
|
||||||
|
const response = await fetch(getApiUrl('/auth/login'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
await handleAuthResponse(response, 'login');
|
await handleAuthResponse(response, 'login');
|
||||||
|
// Форма логина обычно не сбрасывается или перенаправляется
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Auth] Network error during login:', error);
|
console.error('[Auth] Network error during login:', error);
|
||||||
ui.setAuthMessage('Ошибка сети при входе. Пожалуйста, проверьте ваше подключение.', true);
|
ui.setAuthMessage('Ошибка сети при входе. Пожалуйста, проверьте ваше подключение.', true);
|
||||||
@ -135,76 +146,67 @@ export function initAuth(dependencies) {
|
|||||||
|
|
||||||
if (logoutButton) {
|
if (logoutButton) {
|
||||||
logoutButton.addEventListener('click', () => {
|
logoutButton.addEventListener('click', () => {
|
||||||
logoutButton.disabled = true;
|
logoutButton.disabled = true; // Блокируем кнопку сразу
|
||||||
|
|
||||||
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
// Проверяем, находится ли игрок в активной игре
|
||||||
// Если игрок в активной PvP игре, отправляем сигнал о сдаче
|
if (clientState.isLoggedIn && clientState.isInGame && clientState.currentGameId) {
|
||||||
if (clientState.isLoggedIn &&
|
// Если это PvP игра и она не закончена
|
||||||
clientState.isInGame &&
|
if (clientState.currentGameState &&
|
||||||
clientState.currentGameId &&
|
clientState.currentGameState.gameMode === 'pvp' &&
|
||||||
clientState.currentGameState && // Убедимся, что gameState существует
|
!clientState.currentGameState.isGameOver) {
|
||||||
clientState.currentGameState.gameMode === 'pvp' && // Проверяем режим игры
|
console.log('[Auth] Player is in an active PvP game. Emitting playerSurrender.');
|
||||||
!clientState.currentGameState.isGameOver) { // Только если игра еще не закончена
|
socket.emit('playerSurrender');
|
||||||
|
// Не ждем ответа от сервера здесь, так как logout - это безусловное действие на клиенте.
|
||||||
console.log('[Auth] Player is in an active PvP game. Emitting playerSurrender.');
|
}
|
||||||
socket.emit('playerSurrender');
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ДЛЯ ВАРИАНТА А ---
|
||||||
// Не ждем ответа от сервера здесь, так как logout - это безусловное действие на клиенте.
|
else if (clientState.currentGameState &&
|
||||||
// Сервер обработает 'playerSurrender' и соответствующим образом завершит игру.
|
clientState.currentGameState.gameMode === 'ai' &&
|
||||||
|
!clientState.currentGameState.isGameOver) {
|
||||||
|
console.log('[Auth] Player is in an active AI game. Emitting leaveAiGame.');
|
||||||
|
socket.emit('leaveAiGame');
|
||||||
|
// Сервер должен обработать это и завершить AI игру.
|
||||||
|
}
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ДЛЯ ВАРИАНТА А ---
|
||||||
}
|
}
|
||||||
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
|
||||||
|
|
||||||
|
|
||||||
// Серверный эндпоинт для логаута не обязателен для JWT,
|
// Серверный эндпоинт для логаута не обязателен для JWT,
|
||||||
// если нет необходимости аннулировать токен на сервере (что сложно с JWT).
|
// если нет необходимости аннулировать токен на сервере (что сложно с JWT).
|
||||||
// Основное действие - удаление токена на клиенте.
|
// Основное действие - удаление токена на клиенте.
|
||||||
// socket.emit('logout'); // Можно оставить, если на сервере есть логика для этого (например, GameManager.handleDisconnect)
|
// socket.emit('logout'); // Клиент сам инициирует разрыв и новое подключение без токена.
|
||||||
|
// Это событие из bc.js скорее для очистки серверной сессии сокета.
|
||||||
|
|
||||||
localStorage.removeItem(JWT_TOKEN_KEY); // Удаляем токен
|
localStorage.removeItem(JWT_TOKEN_KEY); // Удаляем токен
|
||||||
|
|
||||||
|
// Сбрасываем состояние клиента
|
||||||
clientState.isLoggedIn = false;
|
clientState.isLoggedIn = false;
|
||||||
clientState.loggedInUsername = '';
|
clientState.loggedInUsername = '';
|
||||||
clientState.myUserId = null;
|
clientState.myUserId = null;
|
||||||
// isInGame и другие игровые переменные сбросятся в ui.showAuthScreen()
|
// clientState.isInGame и другие игровые переменные будут сброшены в ui.showAuthScreen()
|
||||||
// ui.disableGameControls() также будет вызван опосредованно
|
// или ui.resetGameVariables() если вызывается напрямую.
|
||||||
|
|
||||||
ui.showAuthScreen(); // Показываем экран логина
|
ui.showAuthScreen(); // Показываем экран логина (это вызовет resetGameVariables)
|
||||||
ui.setGameStatusMessage("Вы вышли из системы."); // Можно заменить на ui.setAuthMessage, если хотим видеть сообщение на экране логина
|
ui.setAuthMessage("Вы успешно вышли из системы."); // Сообщение на экране логина
|
||||||
|
|
||||||
// Переподключаем сокет без токена
|
// Переподключаем сокет без токена (или он сам переподключится при следующем действии)
|
||||||
if (socket.connected) {
|
if (socket.connected) {
|
||||||
socket.disconnect();
|
socket.disconnect(); // Принудительно отключаемся
|
||||||
}
|
}
|
||||||
socket.auth = { token: null }; // Очищаем токен в auth объекте сокета
|
socket.auth = { token: null }; // Очищаем токен в auth объекте сокета
|
||||||
socket.connect(); // Сокет подключится как неаутентифицированный
|
socket.connect(); // Сокет подключится как неаутентифицированный (или main.js инициирует)
|
||||||
|
// Фактически, connect() будет вызван из main.js при переходе на authScreen
|
||||||
|
// и проверке состояния. Здесь главное - очистить токен.
|
||||||
|
// Но явный connect() после disconnect() более предсказуем.
|
||||||
|
|
||||||
// Кнопка logout будет активирована, когда пользователь снова войдет
|
// Кнопка logoutButton.disabled = true; уже была установлена в showAuthScreen()
|
||||||
// или если она видна только залогиненным пользователям, то исчезнет.
|
|
||||||
// (В showAuthScreen logoutButton.disabled устанавливается в true)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Обработчики событий Socket.IO ---
|
// --- Обработчики событий Socket.IO ---
|
||||||
// Старые 'registerResponse' и 'loginResponse' больше не нужны,
|
// Старые 'registerResponse' и 'loginResponse' (если были через сокет) больше не нужны,
|
||||||
// так как эти ответы приходят через HTTP.
|
// так как эти ответы приходят через HTTP.
|
||||||
|
|
||||||
// Можно добавить обработчик для принудительного разлогинивания от сервера, если такой будет
|
// Логика проверки токена при загрузке страницы (если токен есть в localStorage)
|
||||||
// socket.on('forceLogout', (data) => {
|
// обычно выполняется в main.js до инициализации сокета.
|
||||||
// console.log('[Auth] Forced logout by server:', data.message);
|
// Здесь мы предполагаем, что main.js уже подготовил clientState
|
||||||
// localStorage.removeItem(JWT_TOKEN_KEY);
|
// на основе существующего токена или оставил его пустым.
|
||||||
// clientState.isLoggedIn = false;
|
|
||||||
// clientState.loggedInUsername = '';
|
|
||||||
// clientState.myUserId = null;
|
|
||||||
// ui.showAuthScreen();
|
|
||||||
// ui.setAuthMessage(data.message || "Вы были разлогинены сервером.");
|
|
||||||
// if (socket.connected) socket.disconnect();
|
|
||||||
// socket.auth = { token: null };
|
|
||||||
// socket.connect();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// При загрузке модуля auth.js, проверяем, нет ли уже токена в localStorage
|
|
||||||
// Эта логика лучше всего будет работать в main.js при инициализации сокета,
|
|
||||||
// но здесь можно было бы сделать предварительную проверку и обновление clientState,
|
|
||||||
// если бы это было необходимо до создания сокета.
|
|
||||||
// Однако, поскольку сокет создается в main.js и сразу использует токен из localStorage,
|
|
||||||
// отдельная логика здесь не так критична.
|
|
||||||
}
|
}
|
98
server/bc.js
98
server/bc.js
@ -18,22 +18,19 @@ const app = express();
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
// --- НАСТРОЙКА EXPRESS ---
|
// --- НАСТРОЙКА EXPRESS ---
|
||||||
console.log(`[BC.JS CONFIG] Reading environment variables...`);
|
console.log(`[BC.JS CONFIG] Reading environment variables for Express CORS...`);
|
||||||
console.log(`[BC.JS CONFIG] NODE_ENV: ${process.env.NODE_ENV}`);
|
console.log(`[BC.JS CONFIG] NODE_ENV: ${process.env.NODE_ENV}`);
|
||||||
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_CLIENT: ${process.env.CORS_ORIGIN_CLIENT}`);
|
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_CLIENT: ${process.env.CORS_ORIGIN_CLIENT}`);
|
||||||
|
|
||||||
const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
||||||
// Этот лог покажет, какое значение clientOrigin будет использовано для HTTP CORS
|
|
||||||
console.log(`[BC.JS CONFIG] Effective clientOrigin for HTTP CORS: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET (CORS will likely fail if not development)'}`);
|
console.log(`[BC.JS CONFIG] Effective clientOrigin for HTTP CORS: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET (CORS will likely fail if not development)'}`);
|
||||||
|
|
||||||
if (!clientOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) { // Добавил проверку на undefined NODE_ENV
|
if (!clientOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) {
|
||||||
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_CLIENT is not set for a non-development and non-undefined NODE_ENV. HTTP API requests from browsers might be blocked by CORS.");
|
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_CLIENT is not set for a non-development and non-undefined NODE_ENV. HTTP API requests from browsers might be blocked by CORS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: clientOrigin,
|
origin: clientOrigin,
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true // Важно, если клиент шлет куки или заголовок Authorization
|
credentials: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -41,12 +38,10 @@ const publicPath = path.join(__dirname, '..', 'public');
|
|||||||
console.log(`[BC.JS CONFIG] Serving static files from: ${publicPath}`);
|
console.log(`[BC.JS CONFIG] Serving static files from: ${publicPath}`);
|
||||||
app.use(express.static(publicPath));
|
app.use(express.static(publicPath));
|
||||||
|
|
||||||
|
|
||||||
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
||||||
app.post('/auth/register', async (req, res) => {
|
app.post('/auth/register', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
// Логируем входящий Origin для этого запроса
|
console.log(`[BC HTTP /auth/register] Attempt for username: "${username}" from IP: ${req.ip}. Origin: ${req.headers.origin}`);
|
||||||
console.log(`[BC HTTP /auth/register] Attempt for username: "${username}" from IP: ${req.ip}. Origin header: ${req.headers.origin}`);
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
console.warn('[BC HTTP /auth/register] Bad request: Username or password missing.');
|
console.warn('[BC HTTP /auth/register] Bad request: Username or password missing.');
|
||||||
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
||||||
@ -63,8 +58,7 @@ app.post('/auth/register', async (req, res) => {
|
|||||||
|
|
||||||
app.post('/auth/login', async (req, res) => {
|
app.post('/auth/login', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
// Логируем входящий Origin для этого запроса
|
console.log(`[BC HTTP /auth/login] Attempt for username: "${username}" from IP: ${req.ip}. Origin: ${req.headers.origin}`);
|
||||||
console.log(`[BC HTTP /auth/login] Attempt for username: "${username}" from IP: ${req.ip}. Origin header: ${req.headers.origin}`);
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
console.warn('[BC HTTP /auth/login] Bad request: Username or password missing.');
|
console.warn('[BC HTTP /auth/login] Bad request: Username or password missing.');
|
||||||
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
||||||
@ -80,17 +74,17 @@ app.post('/auth/login', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- НАСТРОЙКА SOCKET.IO ---
|
// --- НАСТРОЙКА SOCKET.IO ---
|
||||||
|
console.log(`[BC.JS CONFIG] Reading environment variables for Socket.IO CORS...`);
|
||||||
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_SOCKET: ${process.env.CORS_ORIGIN_SOCKET}`);
|
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_SOCKET: ${process.env.CORS_ORIGIN_SOCKET}`);
|
||||||
const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
||||||
// Этот лог покажет, какое значение socketCorsOrigin будет использовано для Socket.IO CORS
|
|
||||||
console.log(`[BC.JS CONFIG] Effective socketCorsOrigin for Socket.IO CORS: ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'NOT SET (Socket.IO CORS will likely fail if not development)'}`);
|
console.log(`[BC.JS CONFIG] Effective socketCorsOrigin for Socket.IO CORS: ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'NOT SET (Socket.IO CORS will likely fail if not development)'}`);
|
||||||
|
|
||||||
if (!socketCorsOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) { // Добавил проверку на undefined NODE_ENV
|
if (!socketCorsOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) {
|
||||||
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_SOCKET is not set for a non-development and non-undefined NODE_ENV. Socket.IO connections from browsers might be blocked by CORS.");
|
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_SOCKET is not set for a non-development and non-undefined NODE_ENV. Socket.IO connections from browsers might be blocked by CORS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
path: '/socket.io/', // Убедитесь, что это соответствует настройкам клиента и прокси
|
path: '/socket.io/', // Убедитесь, что это соответствует клиенту и прокси (stripPrefix: false для /socket.io)
|
||||||
cors: {
|
cors: {
|
||||||
origin: socketCorsOrigin,
|
origin: socketCorsOrigin,
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
@ -99,14 +93,12 @@ const io = new Server(server, {
|
|||||||
});
|
});
|
||||||
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
||||||
|
|
||||||
|
|
||||||
const gameManager = new GameManager(io);
|
const gameManager = new GameManager(io);
|
||||||
const loggedInUsers = {};
|
const loggedInUsersBySocketId = {}; // Хранилище для данных пользователя по ID сокета
|
||||||
|
|
||||||
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
const token = socket.handshake.auth.token;
|
const token = socket.handshake.auth.token;
|
||||||
// Пытаемся получить IP клиента, учитывая возможные заголовки от прокси
|
|
||||||
const clientIp = socket.handshake.headers['x-forwarded-for']?.split(',')[0].trim() || socket.handshake.address;
|
const clientIp = socket.handshake.headers['x-forwarded-for']?.split(',')[0].trim() || socket.handshake.address;
|
||||||
const originHeader = socket.handshake.headers.origin;
|
const originHeader = socket.handshake.headers.origin;
|
||||||
const socketPath = socket.nsp.name;
|
const socketPath = socket.nsp.name;
|
||||||
@ -116,16 +108,17 @@ io.use(async (socket, next) => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
socket.userData = { userId: decoded.userId, username: decoded.username };
|
socket.userData = { userId: decoded.userId, username: decoded.username }; // Прикрепляем данные к объекту сокета
|
||||||
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}`);
|
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
|
||||||
|
// Не прерываем соединение, но userData не будет установлен
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
||||||
}
|
}
|
||||||
next();
|
next(); // Разрешаем подключение даже неаутентифицированным
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO ---
|
// --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO ---
|
||||||
@ -136,26 +129,29 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (socket.userData && socket.userData.userId) {
|
if (socket.userData && socket.userData.userId) {
|
||||||
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
||||||
loggedInUsers[socket.id] = socket.userData;
|
loggedInUsersBySocketId[socket.id] = socket.userData; // Сохраняем данные пользователя, связанные с этим сокетом
|
||||||
|
|
||||||
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
||||||
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
} else {
|
} else {
|
||||||
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or handleRequestGameState not available for authenticated user!");
|
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or handleRequestGameState not found for authenticated user!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}. No game state will be restored.`);
|
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (остальные обработчики событий: logout, playerSurrender, createGame, и т.д. как в предыдущей версии) ...
|
socket.on('logout', () => { // Инициируется клиентом ПЕРЕД разрывом соединения и удалением токена
|
||||||
socket.on('logout', () => {
|
const username = socket.userData?.username || 'UnknownUserOnLogout';
|
||||||
const username = socket.userData?.username || 'UnknownUser';
|
|
||||||
const userId = socket.userData?.userId;
|
const userId = socket.userData?.userId;
|
||||||
console.log(`[BC Socket.IO 'logout'] Event from user ${username} (ID: ${userId}, Socket: ${socket.id})`);
|
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}).`);
|
||||||
if (loggedInUsers[socket.id]) {
|
// Логика GameManager.handleDisconnect будет вызвана при фактическом 'disconnect' событии.
|
||||||
delete loggedInUsers[socket.id];
|
// Здесь мы просто очищаем данные, связанные с этим сокетом на сервере,
|
||||||
|
// так как клиент собирается разорвать соединение или переподключиться без токена.
|
||||||
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
socket.userData = null;
|
socket.userData = null; // Очищаем данные на самом объекте сокета
|
||||||
console.log(`[BC Socket.IO 'logout'] User ${username} (Socket: ${socket.id}) session data cleared.`);
|
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('playerSurrender', () => {
|
socket.on('playerSurrender', () => {
|
||||||
@ -175,6 +171,28 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- НАЧАЛО ИЗМЕНЕНИЯ: ОБРАБОТЧИК ДЛЯ ВЫХОДА ИЗ AI-ИГРЫ ---
|
||||||
|
socket.on('leaveAiGame', () => {
|
||||||
|
if (!socket.userData?.userId) {
|
||||||
|
console.warn(`[BC Socket.IO 'leaveAiGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
|
socket.emit('gameError', { message: 'Необходимо войти в систему, чтобы покинуть AI игру.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const identifier = socket.userData.userId;
|
||||||
|
const username = socket.userData.username;
|
||||||
|
console.log(`[BC Socket.IO 'leaveAiGame'] Request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
|
||||||
|
|
||||||
|
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
|
||||||
|
gameManager.handleLeaveAiGame(identifier);
|
||||||
|
// Ответ клиенту не требуется, т.к. он и так выходит и переходит на другой экран.
|
||||||
|
// GameManager._cleanupGame будет вызван изнутри handleLeaveAiGame через GameInstance.
|
||||||
|
} else {
|
||||||
|
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
socket.on('createGame', (data) => {
|
socket.on('createGame', (data) => {
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'createGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'createGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
@ -189,6 +207,7 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('joinGame', (data) => {
|
socket.on('joinGame', (data) => {
|
||||||
|
// ... (код без изменений)
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'joinGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'joinGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
|
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
|
||||||
@ -201,6 +220,7 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('findRandomGame', (data) => {
|
socket.on('findRandomGame', (data) => {
|
||||||
|
// ... (код без изменений)
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'findRandomGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'findRandomGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
|
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
|
||||||
@ -213,12 +233,14 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestPvPGameList', () => {
|
socket.on('requestPvPGameList', () => {
|
||||||
|
// ... (код без изменений)
|
||||||
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
||||||
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
socket.emit('availablePvPGamesList', availableGames);
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestGameState', () => {
|
socket.on('requestGameState', () => {
|
||||||
|
// ... (код без изменений)
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
||||||
@ -230,6 +252,7 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('playerAction', (actionData) => {
|
socket.on('playerAction', (actionData) => {
|
||||||
|
// ... (код без изменений)
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'playerAction'] Denied for unauthenticated socket ${socket.id}. Action: ${actionData?.actionType}`);
|
console.warn(`[BC Socket.IO 'playerAction'] Denied for unauthenticated socket ${socket.id}. Action: ${actionData?.actionType}`);
|
||||||
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
||||||
@ -241,15 +264,17 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
const identifier = socket.userData?.userId;
|
const identifier = socket.userData?.userId; // Берем из userData, если был аутентифицирован
|
||||||
const username = socket.userData?.username || 'UnauthenticatedUser';
|
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
|
||||||
|
|
||||||
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
gameManager.handleDisconnect(socket.id, identifier);
|
gameManager.handleDisconnect(socket.id, identifier);
|
||||||
}
|
}
|
||||||
if (loggedInUsers[socket.id]) {
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
delete loggedInUsers[socket.id];
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
|
// socket.userData автоматически очистится при уничтожении объекта сокета
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -273,7 +298,7 @@ server.listen(PORT, HOSTNAME, () => {
|
|||||||
}
|
}
|
||||||
console.log(`[BC Server Startup] Static files served from: ${publicPath}`);
|
console.log(`[BC Server Startup] Static files served from: ${publicPath}`);
|
||||||
console.log(`[BC.JS Startup] Socket.IO server effective path: ${io.path()}`);
|
console.log(`[BC.JS Startup] Socket.IO server effective path: ${io.path()}`);
|
||||||
console.log(`[BC.JS Startup] HTTP API effective CORS origin: ${app.get('env') === 'development' && !process.env.CORS_ORIGIN_CLIENT ? "'*'" : clientOrigin || 'NOT SET'}`); // Это немного упрощенный вывод, точнее в логах CONFIG
|
console.log(`[BC.JS Startup] HTTP API effective CORS origin: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET'}`);
|
||||||
console.log(`[BC.JS Startup] Socket.IO effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
console.log(`[BC.JS Startup] Socket.IO effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -283,5 +308,6 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
||||||
|
// В продакшене PM2 или другой менеджер процессов должен перезапустить приложение
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
@ -7,444 +7,370 @@ const GAME_CONFIG = require('../core/config');
|
|||||||
class GameManager {
|
class GameManager {
|
||||||
constructor(io) {
|
constructor(io) {
|
||||||
this.io = io;
|
this.io = io;
|
||||||
this.games = {}; // Активные инстансы игр { gameId: GameInstance }
|
this.games = {}; // { gameId: GameInstance }
|
||||||
this.userIdentifierToGameId = {}; // { userId: gameId }
|
this.userIdentifierToGameId = {}; // { userId: gameId }
|
||||||
this.pendingPvPGames = []; // Массив gameId игр, ожидающих второго игрока
|
this.pendingPvPGames = []; // Массив gameId ожидающих PvP игр
|
||||||
console.log("[GameManager] Initialized.");
|
console.log("[GameManager] Initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||||||
console.log(`[GameManager._removePreviousPendingGames] Called for user: ${identifier}, currentSocket: ${currentSocketId}, excludeGameId: ${excludeGameId}`);
|
console.log(`[GameManager._removePreviousPendingGames] User: ${identifier}, Socket: ${currentSocketId}, Exclude: ${excludeGameId}`);
|
||||||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||||||
|
|
||||||
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||||||
const gameToRemove = this.games[oldPendingGameId];
|
const gameToRemove = this.games[oldPendingGameId];
|
||||||
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
// Используем game.playerCount (или аналогичный метод GameInstance, если он инкапсулирует это)
|
||||||
const playerInfo = Object.values(gameToRemove.players).find(p => p.identifier === identifier);
|
if (gameToRemove.mode === 'pvp' &&
|
||||||
if (playerInfo && playerInfo.id === GAME_CONFIG.PLAYER_ID) {
|
gameToRemove.playerCount === 1 && // Предполагаем, GameInstance.playerCount - это активные игроки
|
||||||
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} (socket: ${currentSocketId}) created/joined new game. Removing their previous owned pending PvP game: ${oldPendingGameId}`);
|
gameToRemove.ownerIdentifier === identifier &&
|
||||||
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game_creation_or_join');
|
this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||||
} else {
|
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} creating/joining new. Removing previous pending PvP game: ${oldPendingGameId}`);
|
||||||
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} had pending game ${oldPendingGameId}, but was not the primary player. Not removing.`);
|
this._cleanupGame(oldPendingGameId, 'owner_action_removed_pending_game');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[GameManager._removePreviousPendingGames] No old pending game found for user ${identifier} or conditions not met.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
|
||||||
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey}`);
|
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
|
||||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
|
||||||
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
|
||||||
console.warn(`[GameManager.createGame] User ${identifier} already in game ${this.userIdentifierToGameId[identifier]}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
|
|
||||||
|
|
||||||
// Если игра существует и НЕ завершена
|
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||||
if (!existingGame.gameState?.isGameOver) {
|
if (existingGameId && this.games[existingGameId]) {
|
||||||
|
const existingGame = this.games[existingGameId];
|
||||||
|
// Используем game.playerCount
|
||||||
|
console.warn(`[GameManager.createGame] User ${identifier} already in game ${existingGameId}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
|
||||||
|
|
||||||
|
if (existingGame.gameState && !existingGame.gameState.isGameOver) {
|
||||||
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
||||||
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
||||||
} else {
|
} else {
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||||||
}
|
}
|
||||||
this.handleRequestGameState(socket, identifier); // Попытка восстановить сессию в существующей игре
|
this.handleRequestGameState(socket, identifier);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Игра существует, но завершена. GameManager должен был ее очистить.
|
this._cleanupGame(existingGameId, `stale_finished_on_create_${identifier}`);
|
||||||
// Если мы здесь, значит, что-то пошло не так с очисткой.
|
|
||||||
console.warn(`[GameManager.createGame] User ${identifier} was mapped to an already finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up stale entry before creating new game.`);
|
|
||||||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_create_for_${identifier}`);
|
|
||||||
// this.userIdentifierToGameId[identifier] будет удален в _cleanupGame
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
|
|
||||||
const gameId = uuidv4();
|
const gameId = uuidv4();
|
||||||
console.log(`[GameManager.createGame] Generated new GameID: ${gameId}`);
|
console.log(`[GameManager.createGame] New GameID: ${gameId}`);
|
||||||
const game = new GameInstance(gameId, this.io, mode, this);
|
const game = new GameInstance(gameId, this.io, mode, this);
|
||||||
this.games[gameId] = game;
|
this.games[gameId] = game;
|
||||||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
|
||||||
|
|
||||||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
||||||
|
|
||||||
|
// addPlayer в GameInstance теперь bool, а не объект с результатом
|
||||||
|
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameId;
|
this.userIdentifierToGameId[identifier] = gameId;
|
||||||
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
// Получаем роль и актуальный ключ из GameInstance после добавления
|
||||||
const assignedPlayerId = Object.values(game.players).find(p=>p.identifier === identifier)?.id;
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
const assignedPlayerId = playerInfo?.id;
|
||||||
|
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
||||||
|
|
||||||
if (!assignedPlayerId) {
|
if (!assignedPlayerId || !actualCharacterKey) {
|
||||||
console.error(`[GameManager.createGame] CRITICAL: Failed to assign player role for user ${identifier} in game ${gameId}.`);
|
console.error(`[GameManager.createGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`);
|
||||||
this._cleanupGame(gameId, 'player_add_failed_no_role_assigned');
|
this._cleanupGame(gameId, 'player_info_missing_after_add');
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось присвоить роль).' });
|
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
|
||||||
console.log(`[GameManager.createGame] Emitted 'gameCreated' to ${identifier}. gameId: ${gameId}, yourPlayerId: ${assignedPlayerId}`);
|
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated.`);
|
||||||
|
socket.emit('gameCreated', {
|
||||||
|
gameId: gameId,
|
||||||
|
mode: mode,
|
||||||
|
yourPlayerId: assignedPlayerId,
|
||||||
|
chosenCharacterKey: actualCharacterKey
|
||||||
|
});
|
||||||
|
|
||||||
if (mode === 'ai') {
|
if (mode === 'ai') {
|
||||||
const isInitialized = game.initializeGame();
|
if (game.initializeGame()) {
|
||||||
if (isInitialized) {
|
|
||||||
console.log(`[GameManager.createGame] AI game ${gameId} initialized, starting game...`);
|
|
||||||
game.startGame();
|
game.startGame();
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.createGame] AI game ${gameId} initialization failed. Cleaning up.`);
|
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
||||||
this._cleanupGame(gameId, 'initialization_failed_on_ai_create');
|
|
||||||
}
|
}
|
||||||
} else if (mode === 'pvp') {
|
} else if (mode === 'pvp') {
|
||||||
|
game.initializeGame(); // Инициализирует первого игрока
|
||||||
if (!this.pendingPvPGames.includes(gameId)) {
|
if (!this.pendingPvPGames.includes(gameId)) {
|
||||||
this.pendingPvPGames.push(gameId);
|
this.pendingPvPGames.push(gameId);
|
||||||
console.log(`[GameManager.createGame] PvP game ${gameId} added to pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
|
||||||
}
|
}
|
||||||
game.initializeGame();
|
|
||||||
console.log(`[GameManager.createGame] PvP game ${gameId} initialized (or placeholder). Emitting 'waitingForOpponent'.`);
|
|
||||||
socket.emit('waitingForOpponent');
|
socket.emit('waitingForOpponent');
|
||||||
this.broadcastAvailablePvPGames();
|
this.broadcastAvailablePvPGames();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.createGame] game.addPlayer failed for user ${identifier} in game ${gameId}. Cleaning up.`);
|
console.error(`[GameManager.createGame] game.addPlayer (instance method) failed for ${identifier} in ${gameId}. Cleaning up.`);
|
||||||
this._cleanupGame(gameId, 'player_add_failed_in_instance');
|
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGame(socket, gameIdToJoin, identifier) {
|
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
|
||||||
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join GameID: ${gameIdToJoin}`);
|
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
|
||||||
const game = this.games[gameIdToJoin];
|
const game = this.games[gameIdToJoin];
|
||||||
if (!game) {
|
|
||||||
console.warn(`[GameManager.joinGame] Game ${gameIdToJoin} not found for user ${identifier}.`);
|
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||||||
socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return;
|
if (game.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_${identifier}`); return; }
|
||||||
}
|
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
|
||||||
if (game.gameState?.isGameOver) {
|
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join game ${gameIdToJoin} which is already over.`);
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
if (game.playerCount >= 2 && !playerInfoInGame?.isTemporarilyDisconnected) {
|
||||||
this._cleanupGame(gameIdToJoin, `attempt_to_join_finished_game_${identifier}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (game.mode !== 'pvp') {
|
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join non-PvP game ${gameIdToJoin}. Mode: ${game.mode}`);
|
|
||||||
socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP режим).' }); return;
|
|
||||||
}
|
|
||||||
if (game.playerCount >= 2 && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join full PvP game ${gameIdToJoin}. Players: ${game.playerCount}`);
|
|
||||||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||||||
}
|
}
|
||||||
if (game.ownerIdentifier === identifier && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
if (game.ownerIdentifier === identifier && !playerInfoInGame?.isTemporarilyDisconnected) {
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} (owner) tried to join their own waiting game ${gameIdToJoin} as a new player.`);
|
socket.emit('gameError', { message: 'Вы не можете присоединиться к своей же ожидающей игре как новый игрок.' }); this.handleRequestGameState(socket, identifier); return;
|
||||||
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете, как новый игрок.' });
|
|
||||||
this.handleRequestGameState(socket, identifier); return;
|
|
||||||
}
|
}
|
||||||
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameIdToJoin) {
|
|
||||||
const otherGame = this.games[this.userIdentifierToGameId[identifier]];
|
const existingGameIdOfUser = this.userIdentifierToGameId[identifier];
|
||||||
if (otherGame && !otherGame.gameState?.isGameOver) {
|
if (existingGameIdOfUser && existingGameIdOfUser !== gameIdToJoin) {
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} already in another active game: ${this.userIdentifierToGameId[identifier]}. Cannot join ${gameIdToJoin}.`);
|
const otherGame = this.games[existingGameIdOfUser];
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в другой игре. Сначала завершите или покиньте её.' });
|
if (otherGame && !otherGame.gameState?.isGameOver) { socket.emit('gameError', { message: 'Вы уже в другой активной игре.' }); this.handleRequestGameState(socket, identifier); return; }
|
||||||
this.handleRequestGameState(socket, identifier); return;
|
else if (otherGame?.gameState?.isGameOver) this._cleanupGame(existingGameIdOfUser, `stale_finished_on_join_${identifier}`);
|
||||||
} else if (otherGame && otherGame.gameState?.isGameOver) {
|
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} was mapped to a finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up before join.`);
|
|
||||||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_join_${identifier}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
|
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
|
||||||
|
|
||||||
if (game.addPlayer(socket, null, identifier)) {
|
const charKeyForJoin = chosenCharacterKey || 'elena';
|
||||||
|
if (game.addPlayer(socket, charKeyForJoin, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||||||
console.log(`[GameManager.joinGame] Player ${identifier} successfully added/reconnected to PvP game ${gameIdToJoin}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier); // Получаем инфо после добавления
|
||||||
if (game.playerCount === 2) {
|
|
||||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full with 2 active players. Initializing and starting.`);
|
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
||||||
const isInitialized = game.initializeGame();
|
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}. Cleaning up.`);
|
||||||
if (isInitialized) {
|
// Не вызываем _cleanupGame здесь, т.к. игра могла быть уже с одним игроком.
|
||||||
|
// GameInstance.addPlayer должен был бы вернуть false и не изменить playerCount.
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}.`);
|
||||||
|
socket.emit('gameCreated', {
|
||||||
|
gameId: gameIdToJoin,
|
||||||
|
mode: game.mode,
|
||||||
|
yourPlayerId: joinedPlayerInfo.id,
|
||||||
|
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
|
||||||
|
});
|
||||||
|
|
||||||
|
if (game.playerCount === 2) { // Используем game.playerCount из GameInstance
|
||||||
|
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
|
||||||
|
if (game.initializeGame()) {
|
||||||
game.startGame();
|
game.startGame();
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.joinGame] PvP game ${gameIdToJoin} initialization failed after 2nd player join. Cleaning up.`);
|
this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return;
|
||||||
this._cleanupGame(gameIdToJoin, 'initialization_failed_on_pvp_join'); return;
|
|
||||||
}
|
}
|
||||||
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
|
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
|
||||||
if (idx > -1) {
|
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||||||
this.pendingPvPGames.splice(idx, 1);
|
|
||||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
|
||||||
}
|
|
||||||
this.broadcastAvailablePvPGames();
|
this.broadcastAvailablePvPGames();
|
||||||
} else {
|
|
||||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} has ${game.playerCount} active players after join/reconnect. Waiting for more or game was already running.`);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error(`[GameManager.joinGame] game.addPlayer failed for user ${identifier} trying to join ${gameIdToJoin}.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||||||
console.log(`[GameManager.findAndJoinRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
// ... (Логика findAndJoinRandomPvPGame без изменений, использует game.playerCount)
|
||||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||||||
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||||
if (existingGame && !existingGame.gameState?.isGameOver) {
|
if (existingGameId && this.games[existingGameId]) {
|
||||||
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} already in active game: ${this.userIdentifierToGameId[identifier]}.`);
|
const existingGame = this.games[existingGameId];
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
if (!existingGame.gameState?.isGameOver) {
|
||||||
|
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
|
||||||
this.handleRequestGameState(socket, identifier); return;
|
this.handleRequestGameState(socket, identifier); return;
|
||||||
} else if (existingGame && existingGame.gameState?.isGameOver) {
|
} else {
|
||||||
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} mapped to finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up.`);
|
this._cleanupGame(existingGameId, `stale_finished_on_find_random_${identifier}`);
|
||||||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_find_random_${identifier}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
|
|
||||||
let gameIdToJoin = null;
|
let gameIdToJoin = null;
|
||||||
console.log(`[GameManager.findAndJoinRandomPvPGame] Searching pending games for ${identifier}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
for (const id of [...this.pendingPvPGames]) {
|
||||||
for (const id of [...this.pendingPvPGames]) { // Iterate over a copy in case of modification
|
|
||||||
const pendingGame = this.games[id];
|
const pendingGame = this.games[id];
|
||||||
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
if (pendingGame && pendingGame.mode === 'pvp' &&
|
||||||
if (!pendingGame.gameState || !pendingGame.gameState.isGameOver) {
|
pendingGame.playerCount === 1 &&
|
||||||
gameIdToJoin = id;
|
pendingGame.ownerIdentifier !== identifier &&
|
||||||
console.log(`[GameManager.findAndJoinRandomPvPGame] Found suitable pending game: ${gameIdToJoin} for user ${identifier}.`);
|
!pendingGame.gameState?.isGameOver) {
|
||||||
break;
|
gameIdToJoin = id; break;
|
||||||
} else {
|
} else if (pendingGame?.gameState?.isGameOver) {
|
||||||
console.log(`[GameManager.findAndJoinRandomPvPGame] Pending game ${id} is already over. Skipping and cleaning.`);
|
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
|
||||||
this._cleanupGame(id, `stale_finished_pending_game_during_find_random`); // Clean up stale finished game
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameIdToJoin) {
|
if (gameIdToJoin) {
|
||||||
this.joinGame(socket, gameIdToJoin, identifier);
|
console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`);
|
||||||
|
const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)];
|
||||||
|
this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[GameManager.findAndJoinRandomPvPGame] No suitable pending game found for ${identifier}. Creating a new PvP game.`);
|
console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`);
|
||||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayerAction(identifier, actionData) {
|
handlePlayerAction(identifier, actionData) {
|
||||||
const gameId = this.userIdentifierToGameId[identifier];
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, AbilityID: ${actionData?.abilityId}, GameID from map: ${gameId}`);
|
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, GameID: ${gameId}`);
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} in game ${gameId} attempted action, but game is ALREADY OVER. Action ignored.`);
|
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; // Находим сокет по identifier
|
||||||
game.playerSockets[identifier]?.socket.emit('gameError', {message: "Действие невозможно: игра уже завершена."});
|
if (playerSocket) this.handleRequestGameState(playerSocket, identifier);
|
||||||
// Potentially send gameNotFound or re-send gameOver if client missed it
|
|
||||||
this.handleRequestGameState(game.playerSockets[identifier]?.socket || this._findClientSocketByIdentifier(identifier), identifier);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
game.processPlayerAction(identifier, actionData); // Передаем identifier
|
||||||
if (playerInfo && playerInfo.socket && playerInfo.socket.connected && !playerInfo.isTemporarilyDisconnected) {
|
|
||||||
console.log(`[GameManager.handlePlayerAction] Forwarding action from user ${identifier} (Socket: ${playerInfo.socket.id}) to game ${gameId}.`);
|
|
||||||
game.processPlayerAction(playerInfo.socket.id, actionData);
|
|
||||||
} else if (playerInfo && playerInfo.isTemporarilyDisconnected) {
|
|
||||||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket?.id}) in game ${gameId} attempted action, but is temporarily disconnected. Action ignored.`);
|
|
||||||
playerInfo.socket?.emit('gameError', {message: "Действие невозможно: вы временно отключены."});
|
|
||||||
} else if (playerInfo && playerInfo.socket && !playerInfo.socket.connected) {
|
|
||||||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket.id}) in game ${gameId} attempted action, but socket is reported as disconnected by server. Action ignored. Potential state mismatch.`);
|
|
||||||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
|
||||||
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action for game ${gameId}, but active player info or socket not found in game instance. Removing from user map.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
|
||||||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока при действии).' });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action, but game ${gameId} (from map) not found in this.games. Removing from user map.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена (игра отсутствует).' });
|
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при действии.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayerSurrender(identifier) {
|
handlePlayerSurrender(identifier) {
|
||||||
|
// ... (Логика handlePlayerSurrender без изменений)
|
||||||
const gameId = this.userIdentifierToGameId[identifier];
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`);
|
console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`);
|
||||||
|
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} tried to surrender in game ${gameId} which is ALREADY OVER. Ignoring.`);
|
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
|
||||||
// _cleanupGame should have already run or will run.
|
if (this.userIdentifierToGameId[identifier] === gameId) delete this.userIdentifierToGameId[identifier];
|
||||||
// Ensure map is clear if it's somehow stale.
|
|
||||||
if (this.userIdentifierToGameId[identifier] === gameId) {
|
|
||||||
console.warn(`[GameManager.handlePlayerSurrender] Stale map entry for ${identifier} to finished game ${gameId}. Cleaning.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier]; // Direct cleanup if game is confirmed over.
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
|
||||||
|
else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method"); }
|
||||||
|
} else {
|
||||||
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof game.playerDidSurrender === 'function') {
|
handleLeaveAiGame(identifier) {
|
||||||
console.log(`[GameManager.handlePlayerSurrender] Forwarding surrender from user ${identifier} to game ${gameId}.`);
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
game.playerDidSurrender(identifier); // This method will call _cleanupGame internally
|
console.log(`[GameManager.handleLeaveAiGame] User: ${identifier} leaving AI game. GameID from map: ${gameId}`);
|
||||||
} else {
|
const game = this.games[gameId];
|
||||||
console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} is missing playerDidSurrender method! Attempting fallback cleanup for PvP.`);
|
if (game) {
|
||||||
if (game.mode === 'pvp' && game.gameState && !game.gameState.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
const surrenderedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} game ${gameId} leaving, but game ALREADY OVER.`);
|
||||||
if (surrenderedPlayerInfo) {
|
return;
|
||||||
const opponentInfo = Object.values(game.players).find(p => p.identifier !== identifier && !p.isTemporarilyDisconnected);
|
}
|
||||||
const winnerRole = opponentInfo ? opponentInfo.id : null;
|
if (game.mode === 'ai') {
|
||||||
if (typeof game.endGameDueToDisconnect === 'function') {
|
if (typeof game.playerExplicitlyLeftAiGame === 'function') {
|
||||||
game.endGameDueToDisconnect(surrenderedPlayerInfo.id, surrenderedPlayerInfo.chosenCharacterKey, "opponent_surrendered_fallback", winnerRole);
|
game.playerExplicitlyLeftAiGame(identifier);
|
||||||
} else {
|
} else {
|
||||||
this._cleanupGame(gameId, "surrender_fallback_cleanup_missing_end_method");
|
console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`);
|
||||||
}
|
this._cleanupGame(gameId, "leave_ai_missing_method");
|
||||||
} else {
|
|
||||||
this._cleanupGame(gameId, "surrender_player_not_found_in_game_instance_fallback");
|
|
||||||
}
|
|
||||||
} else if (game.mode === 'ai') {
|
|
||||||
console.log(`[GameManager.handlePlayerSurrender] User ${identifier} in AI game ${gameId} surrendered. No playerDidSurrender. Disconnect will handle.`);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} surrendered, but game ${gameId} (from map) not found in this.games. User map might be stale or already cleaned up.`);
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
if (this.userIdentifierToGameId[identifier] === gameId || this.userIdentifierToGameId[identifier]) {
|
|
||||||
console.log(`[GameManager.handlePlayerSurrender] Clearing map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]}.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_findClientSocketByIdentifier(identifier) {
|
_findClientSocketByIdentifier(identifier) {
|
||||||
|
// ... (код без изменений)
|
||||||
for (const sid of this.io.sockets.sockets.keys()) {
|
for (const sid of this.io.sockets.sockets.keys()) {
|
||||||
const s = this.io.sockets.sockets.get(sid);
|
const s = this.io.sockets.sockets.get(sid);
|
||||||
if (s && s.userData && s.userData.userId === identifier && s.connected) {
|
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
||||||
// console.log(`[GameManager._findClientSocketByIdentifier] Found active socket ${s.id} for identifier ${identifier}.`);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// console.log(`[GameManager._findClientSocketByIdentifier] No active socket found for identifier ${identifier}.`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect(socketId, identifier) {
|
handleDisconnect(socketId, identifier) {
|
||||||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||||
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
|
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
|
||||||
|
|
||||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState && game.gameState.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket: ${socketId}) is ALREADY OVER. Disconnect processing skipped for game logic.`);
|
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER.`);
|
||||||
// _cleanupGame (called by playerDidSurrender or other end game methods) is responsible for clearing userIdentifierToGameId.
|
|
||||||
// If the map entry still exists, it implies _cleanupGame might not have completed or there's a race condition.
|
|
||||||
// However, we shouldn't initiate new game logic like handlePlayerPotentiallyLeft.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
// Находим информацию об игроке в инстансе игры по identifier
|
||||||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} found (and not game over). PlayerInfo for user ${identifier}: ${playerInfo ? `Role: ${playerInfo.id}, CurrentSocketInGame: ${playerInfo.socket?.id}, IsTempDisconnected: ${playerInfo.isTemporarilyDisconnected}` : 'Not found in game instance'}`);
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
|
||||||
if (playerInfo && playerInfo.socket && playerInfo.socket.id === socketId && !playerInfo.isTemporarilyDisconnected) {
|
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
|
||||||
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active socket for user ${identifier} (Role: ${playerInfo.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
||||||
|
// Передаем роль, идентификатор и ключ персонажа в GameInstance
|
||||||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||||||
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
|
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} is missing handlePlayerPotentiallyLeft method!`);
|
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} missing handlePlayerPotentiallyLeft!`);
|
||||||
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_in_instance_on_disconnect");
|
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
|
||||||
}
|
|
||||||
} else if (playerInfo && playerInfo.socket && playerInfo.socket.id !== socketId) {
|
|
||||||
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier} (active socket in game ${gameIdFromMap} is ${playerInfo.socket.id}). Ignoring this disconnect for game logic.`);
|
|
||||||
} else if (playerInfo && playerInfo.isTemporarilyDisconnected && playerInfo.socket?.id === socketId) {
|
|
||||||
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already being temporarily disconnected. Reconnect timer should handle final cleanup.`);
|
|
||||||
} else if (!playerInfo && gameIdFromMap) {
|
|
||||||
console.log(`[GameManager.handleDisconnect] User ${identifier} was mapped to game ${gameIdFromMap}, but not found in game.players.`);
|
|
||||||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
|
|
||||||
console.warn(`[GameManager.handleDisconnect] Removing stale map entry for ${identifier} to game ${gameIdFromMap} where player was not found in instance.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
}
|
}
|
||||||
|
} else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) {
|
||||||
|
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}.`);
|
||||||
|
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
|
||||||
|
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already temp disconnected. Reconnect timer handles final cleanup.`);
|
||||||
|
} else if (!playerInfoInGame) {
|
||||||
|
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. Clearing map.`);
|
||||||
|
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[GameManager.handleDisconnect] User ${identifier} (Socket: ${socketId}) disconnected, but no active game instance found for gameId ${gameIdFromMap} (or gameId was undefined).`);
|
if (this.userIdentifierToGameId[identifier]) {
|
||||||
if (this.userIdentifierToGameId[identifier]) { // If a mapping exists, even if gameIdFromMap was undefined or game not in this.games
|
|
||||||
console.warn(`[GameManager.handleDisconnect] Removing map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]}.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupGame(gameId, reason = 'unknown') {
|
_cleanupGame(gameId, reason = 'unknown') {
|
||||||
console.log(`[GameManager._cleanupGame] Attempting to cleanup GameID: ${gameId}, Reason: ${reason}`);
|
// ... (Код _cleanupGame почти без изменений, но использует game.playerCount и game.ownerIdentifier)
|
||||||
|
console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`);
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
console.warn(`[GameManager._cleanupGame] Game ${gameId} not found in this.games. Checking pending list and user map.`);
|
|
||||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||||
if (pendingIdx > -1) {
|
if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); this.broadcastAvailablePvPGames(); }
|
||||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
for (const idKey in this.userIdentifierToGameId) { if (this.userIdentifierToGameId[idKey] === gameId) delete this.userIdentifierToGameId[idKey]; }
|
||||||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pending list (instance was already gone). Reason: ${reason}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
return false;
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
}
|
|
||||||
// Ensure any lingering user map entries for this non-existent game are cleared.
|
|
||||||
let mapCleaned = false;
|
|
||||||
for (const idKey in this.userIdentifierToGameId) {
|
|
||||||
if (this.userIdentifierToGameId[idKey] === gameId) {
|
|
||||||
console.log(`[GameManager._cleanupGame] Clearing STALE mapping for user ${idKey} to non-existent game ${gameId}.`);
|
|
||||||
delete this.userIdentifierToGameId[idKey];
|
|
||||||
mapCleaned = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mapCleaned) console.log(`[GameManager._cleanupGame] Stale user maps cleared for game ${gameId}.`);
|
|
||||||
return false; // Game was not found in this.games
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[GameManager._cleanupGame] Cleaning up game ${gameId}. Owner: ${game.ownerIdentifier}. Reason: ${reason}.`);
|
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`);
|
||||||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||||||
if (typeof game.clearAllReconnectTimers === 'function') {
|
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); // Вызываем у GameInstance
|
||||||
game.clearAllReconnectTimers();
|
|
||||||
console.log(`[GameManager._cleanupGame] Called clearAllReconnectTimers for game ${gameId}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure gameState is marked as over if not already
|
|
||||||
if (game.gameState && !game.gameState.isGameOver) {
|
if (game.gameState && !game.gameState.isGameOver) {
|
||||||
console.warn(`[GameManager._cleanupGame] Game ${gameId} was not marked as 'isGameOver' during cleanup. Marking now. Reason: ${reason}`);
|
|
||||||
game.gameState.isGameOver = true;
|
game.gameState.isGameOver = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all players of this game from the global userIdentifierToGameId map
|
let playersCleanedFromMap = 0;
|
||||||
let playersInGameCleaned = 0;
|
Object.values(game.players).forEach(pInfo => { // Игроки теперь в game.players
|
||||||
Object.values(game.players).forEach(pInfo => {
|
|
||||||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||||||
console.log(`[GameManager._cleanupGame] Deleting mapping for player ${pInfo.identifier} (Role: ${pInfo.id}) from game ${gameId}.`);
|
|
||||||
delete this.userIdentifierToGameId[pInfo.identifier];
|
delete this.userIdentifierToGameId[pInfo.identifier];
|
||||||
playersInGameCleaned++;
|
playersCleanedFromMap++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Also check the owner, in case they weren't in game.players (e.g., game created but owner disconnected before fully joining)
|
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId &&
|
||||||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
|
!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Проверка, если владелец не в списке игроков
|
||||||
if (!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Only if not already cleaned
|
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||||||
console.log(`[GameManager._cleanupGame] Deleting mapping for owner ${game.ownerIdentifier} from game ${gameId} (was not in game.players or already cleaned).`);
|
playersCleanedFromMap++;
|
||||||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
|
||||||
playersInGameCleaned++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (playersInGameCleaned > 0) {
|
|
||||||
console.log(`[GameManager._cleanupGame] ${playersInGameCleaned} player mappings cleared for game ${gameId}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||||
if (pendingIdx > -1) {
|
if (pendingIdx > -1) this.pendingPvPGames.splice(pendingIdx, 1);
|
||||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
|
||||||
console.log(`[GameManager._cleanupGame] Game ${gameId} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.games[gameId];
|
delete this.games[gameId];
|
||||||
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
console.log(`[GameManager._cleanupGame] Game ${game.id} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||||||
this.broadcastAvailablePvPGames(); // Update list for all clients
|
this.broadcastAvailablePvPGames();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailablePvPGamesListForClient() {
|
getAvailablePvPGamesListForClient() {
|
||||||
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Generating list from pending: ${this.pendingPvPGames.join(', ')}`);
|
// ... (Код без изменений, использует game.playerCount и game.ownerIdentifier)
|
||||||
return this.pendingPvPGames
|
return this.pendingPvPGames
|
||||||
.map(gameId => {
|
.map(gameId => {
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||||
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
// Находим первого игрока (владельца) в инстансе игры
|
||||||
let p1Username = 'Игрок', p1CharName = 'Неизвестный';
|
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
|
let p1Username = 'Игрок';
|
||||||
|
let p1CharName = 'Неизвестный';
|
||||||
const ownerId = game.ownerIdentifier;
|
const ownerId = game.ownerIdentifier;
|
||||||
|
|
||||||
if (p1Info && p1Info.socket && p1Info.socket.userData) { // Check for userData
|
if (p1Entry && p1Entry.socket?.userData) {
|
||||||
p1Username = p1Info.socket.userData.username || `User#${String(p1Info.identifier).substring(0,4)}`;
|
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
||||||
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
|
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
|
||||||
p1CharName = charData?.name || p1Info.chosenCharacterKey || 'Не выбран';
|
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
|
||||||
} else if (ownerId){
|
} else if (ownerId){ // Фоллбэк на поиск по ownerId, если p1Entry не найден или без userData
|
||||||
// console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is pending, p1Info not found or no socket/userData. Using owner info. Owner: ${ownerId}`);
|
|
||||||
// Try to find owner's socket if they are still connected to the server (even if not in this game's player list actively)
|
|
||||||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||||||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||||||
const ownerCharKey = game.playerCharacterKey; // This should be the char key of the first player/owner
|
const ownerCharKey = game.playerCharacterKey; // Ключ персонажа первого игрока из GameInstance
|
||||||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||||||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||||||
}
|
}
|
||||||
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} - Owner: ${ownerId}, P1 Username: ${p1Username}, Char: ${p1CharName}`);
|
|
||||||
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -454,147 +380,58 @@ class GameManager {
|
|||||||
|
|
||||||
broadcastAvailablePvPGames() {
|
broadcastAvailablePvPGames() {
|
||||||
const list = this.getAvailablePvPGamesListForClient();
|
const list = this.getAvailablePvPGamesListForClient();
|
||||||
// console.log(`[GameManager.broadcastAvailablePvPGames] Broadcasting list of ${list.length} games.`);
|
|
||||||
this.io.emit('availablePvPGamesList', list);
|
this.io.emit('availablePvPGamesList', list);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequestGameState(socket, identifier) {
|
handleRequestGameState(socket, identifier) {
|
||||||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||||
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (New Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
|
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
|
||||||
|
|
||||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found for user ${identifier}. Game mode: ${game.mode}, Active players in instance: ${game.playerCount}, Total player entries: ${Object.keys(game.players).length}`);
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); // Ищем по identifier
|
||||||
const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
|
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
|
||||||
console.log(`[GameManager.handleRequestGameState] PlayerInfo for user ${identifier} in game ${gameIdFromMap}: ${playerInfoInGameInstance ? `Role: ${playerInfoInGameInstance.id}, OldSocketInGame: ${playerInfoInGameInstance.socket?.id}, TempDisco: ${playerInfoInGameInstance.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
|
|
||||||
|
|
||||||
if (playerInfoInGameInstance) {
|
if (playerInfoInGame) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} for user ${identifier} IS ALREADY OVER. Emitting 'gameNotFound'. Cleanup should handle map.`);
|
|
||||||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
||||||
// _cleanupGame is responsible for clearing userIdentifierToGameId when a game ends.
|
if(this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||||
// If the game is over, but the user is still mapped, _cleanupGame might not have run or completed fully.
|
|
||||||
// We don't call _cleanupGame here directly, as it might be called by the game ending logic itself.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Передаем РОЛЬ и НОВЫЙ СОКЕТ в GameInstance для обработки реконнекта
|
||||||
console.log(`[GameManager.handleRequestGameState] Restoring game ${gameIdFromMap} for user ${identifier}. NewSocket: ${socket.id}. OldSocketInGame: ${playerInfoInGameInstance.socket?.id}. Player role: ${playerInfoInGameInstance.id}`);
|
|
||||||
|
|
||||||
if (typeof game.handlePlayerReconnected === 'function') {
|
if (typeof game.handlePlayerReconnected === 'function') {
|
||||||
const reconnected = game.handlePlayerReconnected(playerInfoInGameInstance.id, socket);
|
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
||||||
console.log(`[GameManager.handleRequestGameState] Called game.handlePlayerReconnected for role ${playerInfoInGameInstance.id}. Result: ${reconnected}`);
|
// ... (обработка результата reconnected, если нужно)
|
||||||
if (!reconnected && game.gameState && !game.gameState.isGameOver) { // if reconnected failed but game is active
|
|
||||||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected returned false for user ${identifier}. This might indicate an issue.`);
|
|
||||||
// It could be that the player wasn't marked as disconnected, or an error occurred.
|
|
||||||
// The client might still expect game state.
|
|
||||||
} else if (!reconnected && game.gameState?.isGameOver) {
|
|
||||||
// If reconnect failed AND game is over, ensure client gets gameNotFound.
|
|
||||||
socket.emit('gameNotFound', { message: 'Не удалось восстановить сессию: игра уже завершена.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} is missing handlePlayerReconnected method! Attempting fallback socket update.`);
|
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
|
||||||
const oldSocketId = playerInfoInGameInstance.socket?.id;
|
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm');
|
||||||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
|
||||||
delete game.players[oldSocketId];
|
|
||||||
}
|
|
||||||
playerInfoInGameInstance.socket = socket;
|
|
||||||
game.players[socket.id] = playerInfoInGameInstance;
|
|
||||||
if (!game.playerSockets[playerInfoInGameInstance.id] || game.playerSockets[playerInfoInGameInstance.id].id !== socket.id) {
|
|
||||||
game.playerSockets[playerInfoInGameInstance.id] = socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If handlePlayerReconnected returned false but game is not over, we might still need to send state.
|
|
||||||
// If game became game over inside handlePlayerReconnected, it should have emitted gameOver.
|
|
||||||
|
|
||||||
// Re-check game over state as handlePlayerReconnected might have changed it (e.g. if opponent didn't reconnect and game ended)
|
|
||||||
if (game.gameState?.isGameOver) {
|
|
||||||
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} became game over during reconnect logic for ${identifier}. Emitting 'gameNotFound'.`);
|
|
||||||
socket.emit('gameNotFound', { message: 'Игра завершилась во время попытки переподключения.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
socket.join(game.id);
|
|
||||||
console.log(`[GameManager.handleRequestGameState] New socket ${socket.id} for user ${identifier} joined room ${game.id}.`);
|
|
||||||
|
|
||||||
const pCharKey = playerInfoInGameInstance.chosenCharacterKey;
|
|
||||||
const pData = dataUtils.getCharacterData(pCharKey);
|
|
||||||
const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
|
||||||
const oCharKeyFromGameState = game.gameState?.[opponentRole]?.characterKey;
|
|
||||||
const oCharKeyFromInstance = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey;
|
|
||||||
const oCharKey = oCharKeyFromGameState || oCharKeyFromInstance;
|
|
||||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
|
||||||
|
|
||||||
console.log(`[GameManager.handleRequestGameState] User's charKey: ${pCharKey}. Opponent's role: ${opponentRole}, charKey: ${oCharKey || 'None (pending/AI placeholder)'}.`);
|
|
||||||
|
|
||||||
if (pData && (oData || (game.mode === 'pvp' && game.playerCount === 1 && !oCharKey) || game.mode === 'ai') && game.gameState) {
|
|
||||||
const gameStateToSend = game.gameState;
|
|
||||||
const logBufferToSend = game.consumeLogBuffer();
|
|
||||||
console.log(`[GameManager.handleRequestGameState] Emitting 'gameStarted' (for restore) to ${identifier} for game ${game.id}. Game state isGameOver: ${gameStateToSend.isGameOver}. Log entries: ${logBufferToSend.length}`);
|
|
||||||
|
|
||||||
socket.emit('gameStarted', {
|
|
||||||
gameId: game.id,
|
|
||||||
yourPlayerId: playerInfoInGameInstance.id,
|
|
||||||
initialGameState: gameStateToSend,
|
|
||||||
playerBaseStats: pData.baseStats,
|
|
||||||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
|
||||||
playerAbilities: pData.abilities,
|
|
||||||
opponentAbilities: oData?.abilities || [],
|
|
||||||
log: logBufferToSend,
|
|
||||||
clientConfig: { ...GAME_CONFIG }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier && !game.gameState.isGameOver) {
|
|
||||||
console.log(`[GameManager.handleRequestGameState] PvP game ${game.id} is still pending for owner ${identifier}. Emitting 'waitingForOpponent'.`);
|
|
||||||
socket.emit('waitingForOpponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!game.gameState.isGameOver && typeof game.turnTimer?.start === 'function' && !game.isGameEffectivelyPaused()) {
|
|
||||||
const isAiTurnForTimer = game.mode === 'ai' && !game.gameState.isPlayerTurn && game.gameState.opponent?.characterKey !== null;
|
|
||||||
console.log(`[GameManager.handleRequestGameState] Restarting turn timer for game ${game.id}. isPlayerTurn: ${game.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`);
|
|
||||||
game.turnTimer.start(game.gameState.isPlayerTurn, isAiTurnForTimer);
|
|
||||||
} else if (game.gameState.isGameOver) {
|
|
||||||
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is already over, no timer restart.`);
|
|
||||||
} else if (game.isGameEffectivelyPaused()){
|
|
||||||
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is effectively paused, turn timer not started.`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`[GameManager.handleRequestGameState] Data load failed for game ${game.id} / user ${identifier} on reconnect. pData: ${!!pData}, oData: ${!!oData} (oCharKey: ${oCharKey}), gameState: ${!!game.gameState}`);
|
|
||||||
this._handleGameRecoveryError(socket, game.id, identifier, 'data_load_fail_reconnect_manager');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.handleRequestGameState] User ${identifier} was mapped to game ${gameIdFromMap}, but NOT found in game.players. This indicates a serious state inconsistency.`);
|
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_reconnect_gm');
|
||||||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_instance_reconnect_manager');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[GameManager.handleRequestGameState] No active game session found for user ${identifier} (GameID from map was ${gameIdFromMap || 'undefined'}). Emitting 'gameNotFound'.`);
|
|
||||||
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
||||||
// Ensure map is clear if it's somehow stale
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
if (this.userIdentifierToGameId[identifier]) {
|
|
||||||
console.warn(`[GameManager.handleRequestGameState] Clearing stale map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]} but game not found.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||||||
|
// ... (код без изменений)
|
||||||
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||||
if (gameId) {
|
if (gameId && this.games[gameId]) { // Проверяем, что игра еще существует перед очисткой
|
||||||
// Attempt to cleanup the problematic game
|
|
||||||
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
||||||
} else if (this.userIdentifierToGameId[identifier]) {
|
} else if (this.userIdentifierToGameId[identifier]) {
|
||||||
// If gameId was null, but user was still mapped, cleanup the mapped game
|
// Если игра уже удалена, но пользователь все еще к ней привязан
|
||||||
const problematicGameId = this.userIdentifierToGameId[identifier];
|
const problematicGameId = this.userIdentifierToGameId[identifier];
|
||||||
console.warn(`[GameManager._handleGameRecoveryError] GameId was null/undefined for user ${identifier}, but they were in map to game ${problematicGameId}. Attempting cleanup of ${problematicGameId}.`);
|
if (this.games[problematicGameId]) { // Если она все же есть
|
||||||
this._cleanupGame(problematicGameId, `recovery_error_null_gameid_for_${identifier}_reason_${reasonCode}`);
|
this._cleanupGame(problematicGameId, `recovery_error_stale_map_${identifier}_reason_${reasonCode}`);
|
||||||
|
} else { // Если ее нет, просто чистим карту
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// This will also clear userIdentifierToGameId[identifier] if _cleanupGame didn't.
|
// Если после _cleanupGame пользователь все еще привязан (маловероятно, но для гарантии)
|
||||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
|
|
||||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,118 +3,237 @@
|
|||||||
class TurnTimer {
|
class TurnTimer {
|
||||||
/**
|
/**
|
||||||
* Конструктор таймера хода.
|
* Конструктор таймера хода.
|
||||||
* @param {number} turnDurationMs - Длительность хода в миллисекундах.
|
* @param {number} turnDurationMs - Изначальная длительность хода в миллисекундах.
|
||||||
* @param {number} updateIntervalMs - Интервал для отправки обновлений времени клиентам (в мс).
|
* @param {number} updateIntervalMs - Интервал для отправки обновлений времени клиентам (в мс).
|
||||||
* @param {function} onTimeoutCallback - Колбэк, вызываемый при истечении времени хода.
|
* @param {function} onTimeoutCallback - Колбэк, вызываемый при истечении времени хода.
|
||||||
* @param {function} onTickCallback - Колбэк, вызываемый на каждом тике обновления (передает remainingTime, isPlayerTurnForTimer).
|
* @param {function} onTickCallback - Колбэк, вызываемый на каждом тике обновления (передает remainingTime, isPlayerTurnForTimer, isPaused).
|
||||||
|
* @param {string} [gameIdForLogs=''] - (Опционально) ID игры для более понятных логов таймера.
|
||||||
*/
|
*/
|
||||||
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback) {
|
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') {
|
||||||
this.turnDurationMs = turnDurationMs;
|
this.initialTurnDurationMs = turnDurationMs; // Сохраняем начальную полную длительность хода
|
||||||
|
this.currentEffectiveDurationMs = turnDurationMs; // Длительность, с которой стартует текущий отсчет (может быть меньше initial при resume)
|
||||||
|
|
||||||
this.updateIntervalMs = updateIntervalMs;
|
this.updateIntervalMs = updateIntervalMs;
|
||||||
this.onTimeoutCallback = onTimeoutCallback;
|
this.onTimeoutCallback = onTimeoutCallback;
|
||||||
this.onTickCallback = onTickCallback;
|
this.onTickCallback = onTickCallback;
|
||||||
|
this.gameId = gameIdForLogs; // Для логов
|
||||||
|
|
||||||
this.timerId = null; // ID для setTimeout (обработка таймаута)
|
this.timeoutId = null; // ID для setTimeout (обработка общего таймаута хода)
|
||||||
this.updateIntervalId = null; // ID для setInterval (обновление клиента)
|
this.tickIntervalId = null; // ID для setInterval (периодическое обновление клиента)
|
||||||
this.startTime = 0; // Время начала текущего отсчета (Date.now())
|
|
||||||
this.isRunning = false;
|
|
||||||
this.isCurrentPlayerActualTurnForTick = false; // Храним, для чьего хода запущен таймер (для onTickCallback)
|
|
||||||
this.isAiCurrentlyMakingMove = false; // Флаг, что сейчас ход AI (таймер не тикает для игрока)
|
|
||||||
|
|
||||||
// console.log(`[TurnTimer] Initialized with duration: ${turnDurationMs}ms, update interval: ${updateIntervalMs}ms`);
|
this.startTimeMs = 0; // Время (Date.now()) начала текущего отсчета таймера
|
||||||
|
this.isRunning = false; // Активен ли таймер в данный момент (идет отсчет)
|
||||||
|
|
||||||
|
// Состояние, для которого был запущен/приостановлен таймер
|
||||||
|
this.isForPlayerTurn = false; // true, если таймер отсчитывает ход игрока (слот 'player')
|
||||||
|
this.isAiCurrentlyMoving = false; // true, если это ход AI, и таймер для реального игрока не должен "тикать"
|
||||||
|
|
||||||
|
this.isManuallyPaused = false; // Флаг, что таймер был приостановлен вызовом pause()
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запускает или перезапускает таймер хода.
|
* Запускает или перезапускает таймер хода.
|
||||||
* @param {boolean} isPlayerTurn - true, если сейчас ход слота 'player', false - если ход слота 'opponent'.
|
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player', false - если ход слота 'opponent'.
|
||||||
* @param {boolean} isAiTurn - true, если текущий ход делает AI (в этом случае таймер для реального игрока не тикает).
|
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI (таймер для реального игрока не тикает).
|
||||||
|
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого оставшегося времени.
|
||||||
*/
|
*/
|
||||||
start(isPlayerTurn, isAiTurn = false) {
|
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
||||||
this.clear(); // Сначала очищаем предыдущие таймеры
|
this.clear(true); // Очищаем предыдущие таймеры, сохраняя флаг isManuallyPaused если это resume
|
||||||
|
|
||||||
|
this.isForPlayerTurn = isPlayerSlotTurn;
|
||||||
|
this.isAiCurrentlyMakingMove = isAiMakingMove;
|
||||||
|
// При явном старте (не resume) сбрасываем флаг ручной паузы
|
||||||
|
if (customRemainingTimeMs === null) {
|
||||||
|
this.isManuallyPaused = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.isCurrentPlayerActualTurnForTick = isPlayerTurn; // Сохраняем, чей ход для onTick
|
|
||||||
this.isAiCurrentlyMakingMove = isAiTurn;
|
|
||||||
|
|
||||||
// Таймер и отсчет времени запускаются только если это НЕ ход AI
|
|
||||||
if (this.isAiCurrentlyMakingMove) {
|
if (this.isAiCurrentlyMakingMove) {
|
||||||
this.isRunning = false;
|
this.isRunning = false; // Для хода AI основной таймер не "бежит" для игрока
|
||||||
// console.log(`[TurnTimer] Start called, but it's AI's turn. Timer not started for player.`);
|
// console.log(`[TurnTimer ${this.gameId}] Start: AI's turn. Player timer not ticking.`);
|
||||||
// Уведомляем один раз, что таймер неактивен (ход AI)
|
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
this.onTickCallback(null, this.isCurrentPlayerActualTurnForTick);
|
// Уведомляем один раз, что таймер неактивен (ход AI), передаем isPaused = false (т.к. это не ручная пауза)
|
||||||
|
// Время может быть полным или оставшимся, если AI "думает"
|
||||||
|
this.onTickCallback(this.initialTurnDurationMs, this.isForPlayerTurn, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startTime = Date.now();
|
// Устанавливаем длительность для текущего запуска
|
||||||
this.isRunning = true;
|
this.currentEffectiveDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0)
|
||||||
// console.log(`[TurnTimer] Started for ${isPlayerTurn ? 'Player' : 'Opponent'} at ${new Date(this.startTime).toLocaleTimeString()}. AI turn: ${isAiTurn}`);
|
? customRemainingTimeMs
|
||||||
|
: this.initialTurnDurationMs;
|
||||||
|
|
||||||
// Таймер на истечение общего времени хода
|
this.startTimeMs = Date.now();
|
||||||
this.timerId = setTimeout(() => {
|
this.isRunning = true;
|
||||||
// console.log(`[TurnTimer] Timeout occurred! Was running: ${this.isRunning}`);
|
// console.log(`[TurnTimer ${this.gameId}] Started. Effective Duration: ${this.currentEffectiveDurationMs}ms. For ${this.isForPlayerTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${this.isAiCurrentlyMakingMove}`);
|
||||||
if (this.isRunning) { // Дополнительная проверка, что таймер все еще должен был работать
|
|
||||||
this.isRunning = false; // Помечаем, что таймер больше не работает
|
// Основной таймер на истечение времени хода
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Timeout occurred! Was running: ${this.isRunning}`);
|
||||||
|
if (this.isRunning) { // Доп. проверка, что таймер все еще должен был работать
|
||||||
|
this.isRunning = false;
|
||||||
if (this.onTimeoutCallback) {
|
if (this.onTimeoutCallback) {
|
||||||
this.onTimeoutCallback();
|
this.onTimeoutCallback();
|
||||||
}
|
}
|
||||||
this.clear(); // Очищаем и интервал обновления после таймаута
|
this.clear(); // Очищаем и интервал обновления после таймаута
|
||||||
}
|
}
|
||||||
}, this.turnDurationMs);
|
}, this.currentEffectiveDurationMs);
|
||||||
|
|
||||||
// Интервал для отправки обновлений клиентам
|
// Интервал для отправки обновлений клиентам
|
||||||
this.updateIntervalId = setInterval(() => {
|
this.tickIntervalId = setInterval(() => {
|
||||||
if (!this.isRunning) { // Если таймер был остановлен (например, ход сделан или игра окончена)
|
if (!this.isRunning) {
|
||||||
this.clear(); // Убедимся, что интервал тоже очищен
|
// Если таймер был остановлен (например, ход сделан, игра окончена, или pause вызван),
|
||||||
|
// но интервал еще не очищен - очищаем.
|
||||||
|
this.clear(this.isManuallyPaused); // Сохраняем флаг, если это была ручная пауза
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsedTime = Date.now() - this.startTime;
|
const elapsedTime = Date.now() - this.startTimeMs;
|
||||||
const remainingTime = Math.max(0, this.turnDurationMs - elapsedTime);
|
const remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||||||
|
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
// Передаем isCurrentPlayerActualTurnForTick, чтобы клиент знал, для чьего хода это время
|
// isManuallyPaused здесь всегда false, т.к. если бы была пауза, isRunning был бы false
|
||||||
this.onTickCallback(remainingTime, this.isCurrentPlayerActualTurnForTick);
|
this.onTickCallback(remainingTime, this.isForPlayerTurn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingTime <= 0 && this.isRunning) { // Если время вышло по интервалу (на всякий случай, setTimeout должен сработать)
|
if (remainingTime <= 0 && this.isRunning) {
|
||||||
// console.log(`[TurnTimer] Remaining time reached 0 in interval. Forcing timeout logic.`);
|
// Время вышло по интервалу (на всякий случай, setTimeout должен сработать)
|
||||||
// Не вызываем onTimeoutCallback здесь напрямую, чтобы избежать двойного вызова,
|
// Не вызываем onTimeoutCallback здесь напрямую, чтобы избежать двойного вызова.
|
||||||
// setTimeout должен это обработать. Просто очищаем интервал.
|
this.clear(this.isManuallyPaused); // Очищаем интервал, setTimeout сработает для onTimeoutCallback
|
||||||
this.clear(); // Очищаем интервал, setTimeout сработает для onTimeoutCallback
|
|
||||||
}
|
}
|
||||||
}, this.updateIntervalMs);
|
}, this.updateIntervalMs);
|
||||||
|
|
||||||
// Отправляем начальное значение немедленно
|
// Отправляем начальное значение немедленно
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
this.onTickCallback(this.turnDurationMs, this.isCurrentPlayerActualTurnForTick);
|
this.onTickCallback(this.currentEffectiveDurationMs, this.isForPlayerTurn, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Приостанавливает таймер и возвращает его текущее состояние.
|
||||||
|
* @returns {{remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean}}
|
||||||
|
* - remainingTime: Оставшееся время в мс.
|
||||||
|
* - forPlayerRoleIsPlayer: true, если таймер был для хода игрока (слот 'player').
|
||||||
|
* - isAiCurrentlyMoving: true, если это был ход AI.
|
||||||
|
*/
|
||||||
|
pause() {
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Pause called. isRunning: ${this.isRunning}, isAiCurrentlyMoving: ${this.isAiCurrentlyMoving}`);
|
||||||
|
|
||||||
|
let remainingTime = 0;
|
||||||
|
const wasForPlayerTurn = this.isForPlayerTurn;
|
||||||
|
const wasAiMoving = this.isAiCurrentlyMoving;
|
||||||
|
|
||||||
|
if (this.isAiCurrentlyMakingMove) {
|
||||||
|
// Если это был ход AI, таймер для игрока не тикал, считаем, что у него полное время.
|
||||||
|
// Однако, если AI "думал" и мы хотим сохранить это, логика должна быть сложнее.
|
||||||
|
// Для простоты, если AI ход, то время "не шло" для игрока.
|
||||||
|
remainingTime = this.initialTurnDurationMs;
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Paused during AI move. Effective remaining time for player turn: ${remainingTime}ms.`);
|
||||||
|
} else if (this.isRunning) {
|
||||||
|
const elapsedTime = Date.now() - this.startTimeMs;
|
||||||
|
remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Paused while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTime}ms.`);
|
||||||
|
} else {
|
||||||
|
// Если таймер не был запущен (например, уже истек или был очищен),
|
||||||
|
// или был уже на паузе, возвращаем 0 или последнее известное значение.
|
||||||
|
// Если isManuallyPaused уже true, то просто возвращаем то, что было.
|
||||||
|
remainingTime = this.isManuallyPaused ? this.currentEffectiveDurationMs : 0; // currentEffectiveDurationMs тут может быть уже оставшимся временем
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Pause called, but timer not actively running or already paused. Returning current/zero remaining time: ${remainingTime}ms.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isManuallyPaused = true; // Устанавливаем флаг ручной паузы
|
||||||
|
this.clear(true); // Очищаем внутренние таймеры, сохраняя флаг isManuallyPaused
|
||||||
|
this.isRunning = false; // Явно указываем, что отсчет остановлен
|
||||||
|
|
||||||
|
// Уведомляем клиента, что таймер на паузе
|
||||||
|
if (this.onTickCallback) {
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Notifying client of pause. Remaining: ${remainingTime}, ForPlayer: ${wasForPlayerTurn}`);
|
||||||
|
this.onTickCallback(remainingTime, wasForPlayerTurn, true); // isPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return { remainingTime, forPlayerRoleIsPlayer: wasForPlayerTurn, isAiCurrentlyMoving: wasAiMoving };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возобновляет таймер с указанного оставшегося времени и для указанного состояния.
|
||||||
|
* @param {number} remainingTimeMs - Оставшееся время в миллисекундах для возобновления.
|
||||||
|
* @param {boolean} forPlayerSlotTurn - Для чьего хода (слот 'player' = true) возобновляется таймер.
|
||||||
|
* @param {boolean} isAiMakingMove - Был ли это ход AI, когда таймер приостановили (и возобновляем ли ход AI).
|
||||||
|
*/
|
||||||
|
resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) {
|
||||||
|
if (!this.isManuallyPaused) {
|
||||||
|
// console.warn(`[TurnTimer ${this.gameId}] Resume called, but timer was not manually paused. Starting normally or doing nothing.`);
|
||||||
|
// Если не был на ручной паузе, то либо запускаем заново (если не был ход AI), либо ничего не делаем
|
||||||
|
// if (!isAiMakingMove) this.start(forPlayerSlotTurn, false, remainingTimeMs > 0 ? remainingTimeMs : null);
|
||||||
|
// Безопаснее просто выйти, если не был на ручной паузе, GameInstance должен управлять этим.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTimeMs <= 0) {
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Resume called with 0 or less time. Triggering timeout.`);
|
||||||
|
this.isManuallyPaused = false; // Сбрасываем флаг
|
||||||
|
if (this.onTimeoutCallback) {
|
||||||
|
this.onTimeoutCallback(); // Немедленный таймаут
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Resuming. Remaining: ${remainingTimeMs}ms. For ${forPlayerSlotTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${isAiMakingMove}`);
|
||||||
|
|
||||||
|
this.isManuallyPaused = false; // Сбрасываем флаг ручной паузы перед стартом
|
||||||
|
// Запускаем таймер с сохраненным состоянием и оставшимся временем
|
||||||
|
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очищает (останавливает) все активные таймеры (setTimeout и setInterval).
|
* Очищает (останавливает) все активные таймеры (setTimeout и setInterval).
|
||||||
|
* @param {boolean} [preserveManuallyPausedFlag=false] - Если true, не сбрасывает флаг isManuallyPaused.
|
||||||
|
* Используется внутренне при вызове clear из pause().
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear(preserveManuallyPausedFlag = false) {
|
||||||
if (this.timerId) {
|
if (this.timeoutId) {
|
||||||
clearTimeout(this.timerId);
|
clearTimeout(this.timeoutId);
|
||||||
this.timerId = null;
|
this.timeoutId = null;
|
||||||
}
|
}
|
||||||
if (this.updateIntervalId) {
|
if (this.tickIntervalId) {
|
||||||
clearInterval(this.updateIntervalId);
|
clearInterval(this.tickIntervalId);
|
||||||
this.updateIntervalId = null;
|
this.tickIntervalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wasPreviouslyRunning = this.isRunning; // Запоминаем, работал ли он до clear
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.startTime = 0;
|
// this.startTimeMs = 0; // Не сбрасываем startTime, чтобы pause мог корректно вычислить remainingTime
|
||||||
// console.log(`[TurnTimer] Cleared.`);
|
|
||||||
|
if (!preserveManuallyPausedFlag) {
|
||||||
|
this.isManuallyPaused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если таймер был очищен не через pause(), он был активен (и это не был ход AI, который и так не тикает)
|
||||||
|
// то опционально можно уведомить клиента, что таймер больше не тикает (например, ход сделан)
|
||||||
|
// Это может быть полезно, чтобы клиент сбросил свой отображаемый таймер на '--'
|
||||||
|
// if (wasPreviouslyRunning && !this.isAiCurrentlyMakingMove && !this.isManuallyPaused && this.onTickCallback) {
|
||||||
|
// // console.log(`[TurnTimer ${this.gameId}] Cleared while running (not AI, not manual pause). Notifying client.`);
|
||||||
|
// this.onTickCallback(null, this.isForPlayerTurn, this.isManuallyPaused); // remainingTime = null
|
||||||
|
// }
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Cleared. Was running: ${wasPreviouslyRunning}. PreservePaused: ${preserveManuallyPausedFlag}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, активен ли таймер в данный момент.
|
* Проверяет, активен ли таймер в данный момент (идет ли отсчет).
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isActive() {
|
isActive() {
|
||||||
return this.isRunning;
|
return this.isRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, был ли таймер приостановлен вручную вызовом pause().
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isPaused() {
|
||||||
|
return this.isManuallyPaused;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TurnTimer;
|
module.exports = TurnTimer;
|
@ -1,96 +1,103 @@
|
|||||||
// /server/game/logic/combatLogic.js
|
// /server/game/logic/combatLogic.js
|
||||||
|
|
||||||
// GAME_CONFIG и gameData/dataUtils будут передаваться в функции как параметры.
|
// Предполагается, что gameLogic.getRandomTaunt и dataUtils будут доступны
|
||||||
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно импортировать для внутренних нужд, если не все приходит через параметры
|
// через параметры, передаваемые из GameInstance, или через объект gameLogic.
|
||||||
|
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно, если нужно
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает базовую атаку одного бойца по другому.
|
* Обрабатывает базовую атаку одного бойца по другому.
|
||||||
* @param {object} attackerState - Состояние атакующего бойца из gameState.
|
* @param {object} attackerState - Состояние атакующего бойца из gameState.
|
||||||
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
|
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
|
||||||
* @param {object} attackerBaseStats - Базовые статы атакующего (из dataUtils.getCharacterBaseStats).
|
* @param {object} attackerBaseStats - Базовые статы атакующего.
|
||||||
* @param {object} defenderBaseStats - Базовые статы защищающегося (из dataUtils.getCharacterBaseStats).
|
* @param {object} defenderBaseStats - Базовые статы защищающегося.
|
||||||
* @param {object} currentGameState - Текущее полное состояние игры (для getRandomTaunt).
|
* @param {object} currentGameState - Текущее полное состояние игры.
|
||||||
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
|
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
|
||||||
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||||||
* @param {object} defenderFullData - Полные данные защищающегося персонажа (baseStats, abilities) из dataUtils.getCharacterData(defenderKey), для getRandomTaunt.
|
* @param {object} dataUtils - Утилиты для доступа к данным игры.
|
||||||
|
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt, переданная для использования.
|
||||||
*/
|
*/
|
||||||
function performAttack(
|
function performAttack(
|
||||||
attackerState,
|
attackerState,
|
||||||
defenderState,
|
defenderState,
|
||||||
attackerBaseStats,
|
attackerBaseStats,
|
||||||
defenderBaseStats,
|
defenderBaseStats,
|
||||||
currentGameState, // Добавлен для контекста насмешек
|
currentGameState,
|
||||||
addToLogCallback,
|
addToLogCallback,
|
||||||
configToUse,
|
configToUse,
|
||||||
defenderFullData // Добавлен для контекста насмешек цели
|
dataUtils, // Добавлен dataUtils
|
||||||
|
getRandomTauntFunction // Добавлена функция для насмешек
|
||||||
) {
|
) {
|
||||||
// Расчет базового урона с вариацией
|
|
||||||
let damage = Math.floor(
|
let damage = Math.floor(
|
||||||
attackerBaseStats.attackPower *
|
attackerBaseStats.attackPower *
|
||||||
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
|
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
|
||||||
);
|
);
|
||||||
let tauntMessagePart = "";
|
let wasBlocked = false;
|
||||||
|
|
||||||
// Проверка на блок
|
|
||||||
if (defenderState.isBlocking) {
|
if (defenderState.isBlocking) {
|
||||||
const initialDamage = damage;
|
const initialDamage = damage;
|
||||||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||||
|
wasBlocked = true;
|
||||||
// Проверка на насмешку ОТ защищающегося (если это Елена или Альмагест) при блокировании атаки
|
|
||||||
if (defenderState.characterKey === 'elena' || defenderState.characterKey === 'almagest') {
|
|
||||||
// Импортируем getRandomTaunt здесь или передаем как параметр, если он в другом файле logic
|
|
||||||
// Предположим, getRandomTaunt доступен в gameLogic (который будет передан или импортирован)
|
|
||||||
// Для примера, если бы он был в этом же файле или импортирован:
|
|
||||||
// const blockTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameData, currentGameState);
|
|
||||||
// Поскольку getRandomTaunt теперь в gameLogic.js, он должен быть вызван оттуда или передан.
|
|
||||||
// В GameInstance.js мы вызываем gameLogic.getRandomTaunt, так что здесь это дублирование.
|
|
||||||
// Лучше, чтобы GameInstance сам обрабатывал насмешки или передавал их как результат.
|
|
||||||
// Для простоты здесь оставим, но это кандидат на рефакторинг вызова насмешек в GameInstance.
|
|
||||||
// Однако, если defenderFullData передается, мы можем вызвать его, предполагая, что gameLogic.getRandomTaunt будет импортирован
|
|
||||||
// или доступен в объекте gameLogic, переданном в GameInstance.
|
|
||||||
// const blockTaunt = require('./index').getRandomTaunt(...) // Пример циклической зависимости, так не надо
|
|
||||||
// Будем считать, что GameInstance готовит насмешку заранее или эта функция вызывается с уже готовой насмешкой.
|
|
||||||
// Либо, если getRandomTaunt - это часть 'gameLogic' объекта, то:
|
|
||||||
// const blockTaunt = gameLogicFunctions.getRandomTaunt(...)
|
|
||||||
// Сейчас для простоты оставим вызов, но это архитектурный момент.
|
|
||||||
// Предположим, что gameLogic.getRandomTaunt доступен через какой-то объект, например, `sharedLogic`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (addToLogCallback) {
|
if (addToLogCallback) {
|
||||||
addToLogCallback(
|
addToLogCallback(
|
||||||
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`,
|
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`,
|
||||||
configToUse.LOG_TYPE_BLOCK
|
configToUse.LOG_TYPE_BLOCK
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Насмешка при попадании также должна обрабатываться централизованно или передаваться
|
|
||||||
if (addToLogCallback) {
|
if (addToLogCallback) {
|
||||||
addToLogCallback(
|
addToLogCallback(
|
||||||
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.${tauntMessagePart}`,
|
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`,
|
||||||
configToUse.LOG_TYPE_DAMAGE
|
configToUse.LOG_TYPE_DAMAGE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
const actualDamageDealt = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||||
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||||
|
|
||||||
|
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
|
||||||
|
if (getRandomTauntFunction && dataUtils) {
|
||||||
|
let reactionTauntTrigger = null;
|
||||||
|
if (wasBlocked) {
|
||||||
|
reactionTauntTrigger = 'onOpponentAttackBlocked';
|
||||||
|
} else if (actualDamageDealt > 0) { // Если урон прошел
|
||||||
|
reactionTauntTrigger = 'onOpponentAttackHit';
|
||||||
|
}
|
||||||
|
// Можно добавить 'onOpponentAttackMissed' если actualDamageDealt === 0 и !wasBlocked
|
||||||
|
|
||||||
|
if (reactionTauntTrigger) {
|
||||||
|
const attackerFullData = dataUtils.getCharacterData(attackerState.characterKey);
|
||||||
|
if (attackerFullData) { // Убедимся, что данные атакующего есть
|
||||||
|
const reactionTaunt = getRandomTauntFunction(
|
||||||
|
defenderState.characterKey, // Кто говорит (защищающийся)
|
||||||
|
reactionTauntTrigger, // Триггер (onOpponentAttackBlocked или onOpponentAttackHit)
|
||||||
|
{}, // Контекст (пока пустой для этих реакций)
|
||||||
|
configToUse,
|
||||||
|
attackerFullData, // Оппонент для говорящего - это атакующий
|
||||||
|
currentGameState
|
||||||
|
);
|
||||||
|
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||||
|
addToLogCallback(`${defenderState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Применяет эффект способности (урон, лечение, наложение баффа/дебаффа и т.д.).
|
* Применяет эффект способности.
|
||||||
* Насмешки, связанные с самим КАСТОМ способности (selfCastAbility), должны быть обработаны до вызова этой функции.
|
|
||||||
* Насмешки, связанные с РЕАКЦИЕЙ цели на эффект, могут быть обработаны здесь или после.
|
|
||||||
* @param {object} ability - Объект способности.
|
* @param {object} ability - Объект способности.
|
||||||
* @param {object} casterState - Состояние бойца, применившего способность.
|
* @param {object} casterState - Состояние бойца, применившего способность.
|
||||||
* @param {object} targetState - Состояние цели способности.
|
* @param {object} targetState - Состояние цели способности.
|
||||||
* @param {object} casterBaseStats - Базовые статы кастера.
|
* @param {object} casterBaseStats - Базовые статы кастера.
|
||||||
* @param {object} targetBaseStats - Базовые статы цели.
|
* @param {object} targetBaseStats - Базовые статы цели.
|
||||||
* @param {object} currentGameState - Текущее полное состояние игры (для getRandomTaunt, если он здесь вызывается).
|
* @param {object} currentGameState - Текущее полное состояние игры.
|
||||||
* @param {function} addToLogCallback - Функция для добавления лога.
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
* @param {object} configToUse - Конфигурация игры.
|
* @param {object} configToUse - Конфигурация игры.
|
||||||
* @param {object} targetFullData - Полные данные цели (baseStats, abilities) для getRandomTaunt.
|
* @param {object} dataUtils - Утилиты для доступа к данным игры.
|
||||||
|
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt.
|
||||||
|
* @param {function} checkIfActionWasSuccessfulFunction - Функция для проверки успеха действия (для контекстных насмешек).
|
||||||
*/
|
*/
|
||||||
function applyAbilityEffect(
|
function applyAbilityEffect(
|
||||||
ability,
|
ability,
|
||||||
@ -101,66 +108,64 @@ function applyAbilityEffect(
|
|||||||
currentGameState,
|
currentGameState,
|
||||||
addToLogCallback,
|
addToLogCallback,
|
||||||
configToUse,
|
configToUse,
|
||||||
targetFullData // Для насмешек цели
|
dataUtils, // Добавлен dataUtils
|
||||||
|
getRandomTauntFunction, // Добавлена функция для насмешек
|
||||||
|
checkIfActionWasSuccessfulFunction // Добавлена функция для проверки успеха
|
||||||
) {
|
) {
|
||||||
let tauntMessagePart = ""; // Для насмешки цели
|
let abilityApplicationSucceeded = true; // Флаг для отслеживания, применилась ли способность успешно (для контекста насмешек)
|
||||||
|
let actionOutcomeForTaunt = null; // 'success' или 'fail' для способностей типа безмолвия
|
||||||
// Насмешка цели (если это Елена/Альмагест) на применение способности противником
|
|
||||||
// Этот вызов лучше делать в GameInstance, передавая результат сюда, или эта функция должна иметь доступ к getRandomTaunt
|
|
||||||
// if ((targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') && casterState.id !== targetState.id) {
|
|
||||||
// const reactionTaunt = require('./index').getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id }, configToUse, targetFullData, currentGameState);
|
|
||||||
// if (reactionTaunt !== "(Молчание)") tauntMessagePart = ` (${reactionTaunt})`;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
// --- Основная логика применения способности ---
|
||||||
switch (ability.type) {
|
switch (ability.type) {
|
||||||
case configToUse.ACTION_TYPE_HEAL:
|
case configToUse.ACTION_TYPE_HEAL:
|
||||||
const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE));
|
const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE));
|
||||||
const actualHeal = Math.min(healAmount, casterBaseStats.maxHp - casterState.currentHp);
|
const actualHeal = Math.min(healAmount, casterBaseStats.maxHp - casterState.currentHp);
|
||||||
if (actualHeal > 0) {
|
if (actualHeal > 0) {
|
||||||
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
||||||
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!${tauntMessagePart}`, configToUse.LOG_TYPE_HEAL);
|
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO);
|
||||||
|
abilityApplicationSucceeded = false; // Можно считать это "неудачей" для реакции, если хотите
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DAMAGE:
|
case configToUse.ACTION_TYPE_DAMAGE:
|
||||||
let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
||||||
|
let wasAbilityBlocked = false;
|
||||||
if (targetState.isBlocking) {
|
if (targetState.isBlocking) {
|
||||||
const initialDamage = damage;
|
const initialDamage = damage;
|
||||||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
wasAbilityBlocked = true;
|
||||||
|
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`, configToUse.LOG_TYPE_BLOCK);
|
||||||
}
|
}
|
||||||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
||||||
if (addToLogCallback && !targetState.isBlocking) {
|
if (addToLogCallback && !wasAbilityBlocked) {
|
||||||
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!${tauntMessagePart}`, configToUse.LOG_TYPE_DAMAGE);
|
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE);
|
||||||
}
|
}
|
||||||
|
if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false; // Если урона не было (например, из-за эффектов)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_BUFF:
|
case configToUse.ACTION_TYPE_BUFF:
|
||||||
// Проверка на уже активный бафф должна быть сделана до вызова этой функции (в GameInstance)
|
|
||||||
let effectDescriptionBuff = ability.description;
|
let effectDescriptionBuff = ability.description;
|
||||||
if (typeof ability.descriptionFunction === 'function') {
|
if (typeof ability.descriptionFunction === 'function') {
|
||||||
// Для описания баффа может потребоваться информация о противнике (цели баффа, если бафф накладывается на другого)
|
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats); // targetBaseStats здесь оппонент кастера
|
||||||
// В данном случае, баффы накладываются на себя, так что targetBaseStats не всегда релевантен для описания.
|
|
||||||
// Передаем targetBaseStats (оппонента кастера), если описание функции его ожидает.
|
|
||||||
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats);
|
|
||||||
}
|
}
|
||||||
casterState.activeEffects.push({
|
casterState.activeEffects.push({
|
||||||
id: ability.id, name: ability.name, description: effectDescriptionBuff,
|
id: ability.id, name: ability.name, description: effectDescriptionBuff,
|
||||||
type: ability.type, duration: ability.duration,
|
type: ability.type, duration: ability.duration,
|
||||||
turnsLeft: ability.duration, // Длительность эффекта в ходах владельца
|
turnsLeft: ability.duration,
|
||||||
grantsBlock: !!ability.grantsBlock,
|
grantsBlock: !!ability.grantsBlock,
|
||||||
isDelayed: !!ability.isDelayed,
|
isDelayed: !!ability.isDelayed,
|
||||||
justCast: true
|
justCast: true
|
||||||
});
|
});
|
||||||
if (ability.grantsBlock) require('./effectsLogic').updateBlockingStatus(casterState); // Обновляем статус блока
|
if (ability.grantsBlock) require('./effectsLogic').updateBlockingStatus(casterState);
|
||||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DISABLE:
|
case configToUse.ACTION_TYPE_DISABLE:
|
||||||
// Логика для 'Гипнотический взгляд' / 'Раскол Разума' (полное безмолвие)
|
|
||||||
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
||||||
|
// ... (логика полного безмолвия как у вас)
|
||||||
|
// Установите actionOutcomeForTaunt = 'success' или 'fail' если нужно
|
||||||
const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
||||||
if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) {
|
if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) {
|
||||||
targetState.activeEffects.push({
|
targetState.activeEffects.push({
|
||||||
@ -168,17 +173,20 @@ function applyAbilityEffect(
|
|||||||
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
||||||
power: ability.power, isFullSilence: true, justCast: true
|
power: ability.power, isFullSilence: true, justCast: true
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}! Способности заблокированы на ${ability.effectDuration} хода и наносится урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}! Способности заблокированы на ${ability.effectDuration} хода и наносится урон!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
|
actionOutcomeForTaunt = 'success';
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
|
||||||
|
abilityApplicationSucceeded = false;
|
||||||
|
actionOutcomeForTaunt = 'fail'; // Считаем провалом, если уже активен
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Логика для 'Эхо Безмолвия' Баларда
|
|
||||||
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
|
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
|
||||||
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
|
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
|
||||||
// Насмешка цели на успех/провал должна быть обработана в GameInstance, т.к. результат известен только здесь
|
actionOutcomeForTaunt = success ? 'success' : 'fail'; // Устанавливаем для контекста насмешки
|
||||||
if (success) {
|
if (success) {
|
||||||
const targetAbilitiesList = require('../../data/dataUtils').getCharacterAbilities(targetState.characterKey); // Получаем абилки цели
|
// ... (ваша логика наложения безмолвия на способность)
|
||||||
|
const targetAbilitiesList = dataUtils.getCharacterAbilities(targetState.characterKey);
|
||||||
const availableAbilitiesToSilence = targetAbilitiesList.filter(pa =>
|
const availableAbilitiesToSilence = targetAbilitiesList.filter(pa =>
|
||||||
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
|
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
|
||||||
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`)
|
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`)
|
||||||
@ -193,39 +201,43 @@ function applyAbilityEffect(
|
|||||||
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id,
|
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id,
|
||||||
duration: turns, turnsLeft: turns + 1, justCast: true
|
duration: turns, turnsLeft: turns + 1, justCast: true
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!`, configToUse.LOG_TYPE_INFO);
|
||||||
|
actionOutcomeForTaunt = 'fail'; // Провал, если нечего глушить
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DEBUFF:
|
case configToUse.ACTION_TYPE_DEBUFF:
|
||||||
// Логика для 'Печать Слабости' / 'Проклятие Увядания'
|
// ... (логика дебаффа как у вас)
|
||||||
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF) {
|
// Установите actionOutcomeForTaunt если нужно
|
||||||
const effectIdDebuff = 'effect_' + ability.id;
|
const effectIdDebuff = 'effect_' + ability.id;
|
||||||
if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) {
|
if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) {
|
||||||
let effectDescriptionDebuff = ability.description;
|
let effectDescriptionDebuff = ability.description;
|
||||||
if (typeof ability.descriptionFunction === 'function') {
|
if (typeof ability.descriptionFunction === 'function') {
|
||||||
effectDescriptionDebuff = ability.descriptionFunction(configToUse, targetBaseStats);
|
effectDescriptionDebuff = ability.descriptionFunction(configToUse, targetBaseStats);
|
||||||
}
|
|
||||||
targetState.activeEffects.push({
|
|
||||||
id: effectIdDebuff, name: ability.name, description: effectDescriptionDebuff,
|
|
||||||
type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id,
|
|
||||||
duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
|
||||||
power: ability.power, justCast: true
|
|
||||||
});
|
|
||||||
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
|
||||||
} else {
|
|
||||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
|
||||||
}
|
}
|
||||||
|
targetState.activeEffects.push({
|
||||||
|
id: effectIdDebuff, name: ability.name, description: effectDescriptionDebuff,
|
||||||
|
type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id,
|
||||||
|
duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
||||||
|
power: ability.power, justCast: true
|
||||||
|
});
|
||||||
|
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.`, configToUse.LOG_TYPE_EFFECT);
|
||||||
|
actionOutcomeForTaunt = 'success';
|
||||||
|
} else {
|
||||||
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
|
||||||
|
abilityApplicationSucceeded = false;
|
||||||
|
actionOutcomeForTaunt = 'fail';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DRAIN: // Похищение Света Баларда
|
case configToUse.ACTION_TYPE_DRAIN:
|
||||||
|
// ... (логика дрейна как у вас)
|
||||||
if (casterState.characterKey === 'balard') {
|
if (casterState.characterKey === 'balard') {
|
||||||
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
|
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
|
||||||
if (ability.powerDamage > 0) {
|
if (ability.powerDamage > 0) {
|
||||||
@ -249,19 +261,51 @@ function applyAbilityEffect(
|
|||||||
if (manaDrained > 0) logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`;
|
if (manaDrained > 0) logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`;
|
||||||
else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
||||||
else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
||||||
logMsgDrain += tauntMessagePart;
|
|
||||||
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
|
||||||
|
if (manaDrained <= 0 && damageDealtDrain <=0) abilityApplicationSucceeded = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
|
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
|
||||||
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type}`);
|
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type}`);
|
||||||
|
abilityApplicationSucceeded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Насмешка от цели (targetState) в ответ на применение способности ---
|
||||||
|
// Вызываем только если способность не была нацелена на самого себя и успешно применилась (или как вы решите)
|
||||||
|
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id && abilityApplicationSucceeded) {
|
||||||
|
const casterFullData = dataUtils.getCharacterData(casterState.characterKey);
|
||||||
|
if (casterFullData) { // Убедимся, что данные кастера есть
|
||||||
|
let tauntContext = { abilityId: ability.id };
|
||||||
|
if (actionOutcomeForTaunt) { // Если для этой способности важен исход (success/fail)
|
||||||
|
tauntContext.outcome = actionOutcomeForTaunt;
|
||||||
|
} else if (checkIfActionWasSuccessfulFunction) {
|
||||||
|
// Если есть общая функция проверки успеха (менее специфично, чем actionOutcomeForTaunt)
|
||||||
|
// Это пример, вам нужно реализовать checkIfActionWasSuccessfulFunction
|
||||||
|
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
|
||||||
|
// tauntContext.outcome = success ? 'success' : 'fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const reactionTaunt = getRandomTauntFunction(
|
||||||
|
targetState.characterKey, // Кто говорит (цель способности)
|
||||||
|
'onOpponentAction', // Триггер
|
||||||
|
tauntContext, // Контекст: ID способности и исход (если нужен)
|
||||||
|
configToUse,
|
||||||
|
casterFullData, // Оппонент для говорящего - это кастер
|
||||||
|
currentGameState
|
||||||
|
);
|
||||||
|
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||||
|
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет валидность использования способности (ресурс, КД, безмолвие и т.д.).
|
* Проверяет валидность использования способности.
|
||||||
* @param {object} ability - Объект способности.
|
* @param {object} ability - Объект способности.
|
||||||
* @param {object} casterState - Состояние кастера.
|
* @param {object} casterState - Состояние кастера.
|
||||||
* @param {object} targetState - Состояние цели.
|
* @param {object} targetState - Состояние цели.
|
||||||
@ -269,6 +313,7 @@ function applyAbilityEffect(
|
|||||||
* @returns {{isValid: boolean, reason: string|null}} Результат проверки.
|
* @returns {{isValid: boolean, reason: string|null}} Результат проверки.
|
||||||
*/
|
*/
|
||||||
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
||||||
|
// ... (существующий код checkAbilityValidity без изменений) ...
|
||||||
if (!ability) return { isValid: false, reason: "Способность не найдена." };
|
if (!ability) return { isValid: false, reason: "Способность не найдена." };
|
||||||
|
|
||||||
if (casterState.currentResource < ability.cost) {
|
if (casterState.currentResource < ability.cost) {
|
||||||
@ -277,7 +322,6 @@ function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
|||||||
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
|
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
|
||||||
return { isValid: false, reason: `"${ability.name}" еще на перезарядке.` };
|
return { isValid: false, reason: `"${ability.name}" еще на перезарядке.` };
|
||||||
}
|
}
|
||||||
// Проверка специальных КД Баларда
|
|
||||||
if (casterState.characterKey === 'balard') {
|
if (casterState.characterKey === 'balard') {
|
||||||
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
|
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
|
||||||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
|
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
|
||||||
@ -309,5 +353,5 @@ function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
performAttack,
|
performAttack,
|
||||||
applyAbilityEffect,
|
applyAbilityEffect,
|
||||||
checkAbilityValidity // Экспортируем новую функцию
|
checkAbilityValidity
|
||||||
};
|
};
|
@ -1,87 +1,148 @@
|
|||||||
// /server/game/logic/tauntLogic.js
|
// /server/game/logic/tauntLogic.js
|
||||||
const GAME_CONFIG = require('../../core/config'); // Путь к config.js
|
const GAME_CONFIG = require('../../core/config');
|
||||||
// Вам понадобится доступ к gameData.tauntSystem здесь.
|
// Предполагаем, что gameData.tauntSystem импортируется или доступен.
|
||||||
// Либо импортируйте весь gameData, либо только tauntSystem из data/taunts.js
|
// Если tauntSystem экспортируется напрямую из data/taunts.js:
|
||||||
const gameData = require('../../data'); // Импортируем собранный gameData из data/index.js
|
// const { tauntSystem } = require('../../data/taunts');
|
||||||
|
// Если он часть общего gameData, который собирается в data/index.js:
|
||||||
|
const gameData = require('../../data'); // Тогда используем gameData.tauntSystem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает случайную насмешку из системы насмешек.
|
* Получает случайную насмешку из системы насмешек.
|
||||||
* (Ваша существующая функция getRandomTaunt)
|
* @param {string} speakerCharacterKey - Ключ персонажа, который говорит.
|
||||||
|
* @param {string} trigger - Тип триггера насмешки (например, 'selfCastAbility', 'onBattleState', 'onOpponentAction').
|
||||||
|
* @param {string|number|object} [subTriggerOrContext={}] - Может быть ID способности, специфичный ключ состояния ('start', 'dominating') или объект контекста.
|
||||||
|
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||||||
|
* @param {object} opponentFullData - Полные данные персонажа, к которому обращена насмешка (цель).
|
||||||
|
* @param {object} currentGameState - Текущее полное состояние игры.
|
||||||
|
* @returns {string} Текст насмешки или "(Молчание)".
|
||||||
*/
|
*/
|
||||||
function getRandomTaunt(speakerCharacterKey, trigger, context = {}, configToUse, opponentFullData, currentGameState) {
|
function getRandomTaunt(speakerCharacterKey, trigger, subTriggerOrContext = {}, configToUse, opponentFullData, currentGameState) {
|
||||||
// Проверяем наличие системы насмешек для говорящего персонажа
|
// console.log(`[TauntLogic DEBUG] Called with: speaker=${speakerCharacterKey}, trigger=${trigger}, subTriggerOrContext=`, subTriggerOrContext, `opponentKey=${opponentFullData?.baseStats?.characterKey}`);
|
||||||
const speakerTauntSystem = gameData.tauntSystem?.[speakerCharacterKey]; // Используем gameData.tauntSystem
|
|
||||||
if (!speakerTauntSystem) return "(Молчание)";
|
|
||||||
|
|
||||||
const opponentCharacterKey = opponentFullData?.baseStats?.characterKey || currentGameState?.opponent?.characterKey; // Получаем ключ оппонента
|
const tauntSystemToUse = gameData.tauntSystem || (gameData.default && gameData.default.tauntSystem); // Совместимость, если gameData имеет default экспорт
|
||||||
if (!opponentCharacterKey) { // Если оппонент не определен (например, начало игры с AI, где оппонент еще не fully в gameState)
|
if (!tauntSystemToUse) {
|
||||||
// console.warn(`getRandomTaunt: Opponent character key not determined for speaker ${speakerCharacterKey}, trigger ${trigger}`);
|
console.error("[TauntLogic ERROR] tauntSystem is not available from gameData import!");
|
||||||
// Можно попробовать определить оппонента по-другому или вернуть общую фразу / молчание
|
return "(Молчание)";
|
||||||
if (trigger === 'battleStart' && speakerCharacterKey === 'elena' && currentGameState.gameMode === 'ai') {
|
}
|
||||||
// Для Елены против AI Баларда в начале боя
|
|
||||||
const balardTaunts = speakerTauntSystem.balard;
|
const speakerTauntBranch = tauntSystemToUse[speakerCharacterKey];
|
||||||
if (balardTaunts?.onBattleState?.start) {
|
if (!speakerTauntBranch) {
|
||||||
const potentialTaunts = balardTaunts.onBattleState.start;
|
// console.log(`[TauntLogic] No taunt branch for speaker: ${speakerCharacterKey}`);
|
||||||
return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)";
|
return "(Молчание)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const opponentKeyForTaunts = opponentFullData?.baseStats?.characterKey;
|
||||||
|
if (!opponentKeyForTaunts) {
|
||||||
|
// console.log(`[TauntLogic] Opponent key for taunts not available for speaker ${speakerCharacterKey}, trigger ${trigger}. OpponentData:`, opponentFullData);
|
||||||
|
// Особый случай для старта AI игры, где оппонент (AI Балард) может быть известен, даже если opponentFullData не полон
|
||||||
|
if (trigger === 'onBattleState' && subTriggerOrContext === 'start' && speakerCharacterKey === 'elena' && currentGameState.gameMode === 'ai') {
|
||||||
|
// Елена против Баларда (AI) в начале боя
|
||||||
|
const elenaVsBalardStartTaunts = speakerTauntBranch.balard?.onBattleState?.start;
|
||||||
|
if (Array.isArray(elenaVsBalardStartTaunts) && elenaVsBalardStartTaunts.length > 0) {
|
||||||
|
return elenaVsBalardStartTaunts[Math.floor(Math.random() * elenaVsBalardStartTaunts.length)] || "(Молчание)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "(Молчание)";
|
return "(Молчание)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const specificTauntBranch = speakerTauntBranch[opponentKeyForTaunts];
|
||||||
const tauntBranch = speakerTauntSystem[opponentCharacterKey];
|
if (!specificTauntBranch || !specificTauntBranch[trigger]) {
|
||||||
if (!tauntBranch) {
|
// console.log(`[TauntLogic] No specific taunt branch or trigger branch for ${speakerCharacterKey} vs ${opponentKeyForTaunts}, trigger: ${trigger}`);
|
||||||
return "(Молчание)";
|
return "(Молчание)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tauntSet = specificTauntBranch[trigger];
|
||||||
|
let context = {};
|
||||||
|
let subTriggerKey = null; // Это будет ключ для прямого доступа к массиву насмешек, например, ID способности или 'start'
|
||||||
|
|
||||||
|
if (typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number') {
|
||||||
|
subTriggerKey = subTriggerOrContext;
|
||||||
|
// Если subTriggerOrContext - это ID способности, помещаем его в контекст для onOpponentAction
|
||||||
|
if (trigger === 'onOpponentAction' || trigger === 'selfCastAbility') {
|
||||||
|
context.abilityId = subTriggerOrContext;
|
||||||
|
}
|
||||||
|
} else if (typeof subTriggerOrContext === 'object' && subTriggerOrContext !== null) {
|
||||||
|
context = { ...subTriggerOrContext };
|
||||||
|
// Если ID способности передан в контексте, используем его как subTriggerKey для прямого доступа
|
||||||
|
if (context.abilityId && (trigger === 'selfCastAbility' || trigger === 'onOpponentAction')) {
|
||||||
|
subTriggerKey = context.abilityId;
|
||||||
|
} else if (trigger === 'onBattleState' && typeof context === 'string') { // на случай если GameInstance передает строку для onBattleState
|
||||||
|
subTriggerKey = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Для basicAttack subTriggerKey может быть 'merciful', 'dominating' или null (тогда general)
|
||||||
|
if (trigger === 'basicAttack' && typeof subTriggerOrContext === 'string') {
|
||||||
|
subTriggerKey = subTriggerOrContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(`[TauntLogic DEBUG] Parsed: trigger=${trigger}, subTriggerKey=${subTriggerKey}, context=`, context);
|
||||||
|
|
||||||
let potentialTaunts = [];
|
let potentialTaunts = [];
|
||||||
|
|
||||||
if (trigger === 'battleStart') {
|
if (subTriggerKey !== null && typeof tauntSet === 'object' && !Array.isArray(tauntSet) && tauntSet[subTriggerKey]) {
|
||||||
potentialTaunts = tauntBranch.onBattleState?.start;
|
// Если есть subTriggerKey и tauntSet - это объект (а не массив), то получаем вложенный набор
|
||||||
} else if (trigger === 'opponentNearDefeatCheck') {
|
tauntSet = tauntSet[subTriggerKey];
|
||||||
const opponentState = currentGameState?.player?.characterKey === opponentCharacterKey ? currentGameState.player : currentGameState.opponent;
|
} else if (Array.isArray(tauntSet)) {
|
||||||
if (opponentState && opponentState.maxHp > 0 && opponentState.currentHp / opponentState.maxHp < 0.20) {
|
// Если tauntSet уже массив (например, для onOpponentAttackBlocked), используем его как есть
|
||||||
potentialTaunts = tauntBranch.onBattleState?.opponentNearDefeat;
|
potentialTaunts = tauntSet; // Присваиваем сразу
|
||||||
}
|
} else if (typeof tauntSet === 'object' && tauntSet.general) { // Фоллбэк на general, если subTriggerKey не найден в объекте
|
||||||
} else if (trigger === 'selfCastAbility' && context.abilityId) {
|
tauntSet = tauntSet.general;
|
||||||
potentialTaunts = tauntBranch.selfCastAbility?.[context.abilityId];
|
|
||||||
} else if (trigger === 'basicAttack' && tauntBranch.basicAttack) {
|
|
||||||
const opponentState = currentGameState?.player?.characterKey === opponentCharacterKey ? currentGameState.player : currentGameState.opponent;
|
|
||||||
if (speakerCharacterKey === 'elena' && opponentCharacterKey === 'balard' && opponentState) {
|
|
||||||
const opponentHpPerc = (opponentState.currentHp / opponentState.maxHp) * 100;
|
|
||||||
if (opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT) {
|
|
||||||
potentialTaunts = tauntBranch.basicAttack.dominating;
|
|
||||||
} else {
|
|
||||||
potentialTaunts = tauntBranch.basicAttack.merciful;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
potentialTaunts = tauntBranch.basicAttack.general || []; // Фоллбэк на пустой массив
|
|
||||||
}
|
|
||||||
} else if (trigger === 'onOpponentAction' && context.abilityId) {
|
|
||||||
const actionResponses = tauntBranch.onOpponentAction?.[context.abilityId];
|
|
||||||
if (actionResponses) {
|
|
||||||
if (typeof actionResponses === 'object' && !Array.isArray(actionResponses) && context.outcome && context.outcome in actionResponses) {
|
|
||||||
potentialTaunts = actionResponses[context.outcome];
|
|
||||||
} else if (Array.isArray(actionResponses)) {
|
|
||||||
potentialTaunts = actionResponses;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (trigger === 'onOpponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
|
|
||||||
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
|
|
||||||
} else if (trigger === 'onOpponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
|
|
||||||
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
|
|
||||||
// Фоллбэк на общие фразы при basicAttack, если специфичные не найдены
|
// Специальная обработка для onOpponentAction с исходом (success/fail)
|
||||||
if (trigger === 'basicAttack' && tauntBranch.basicAttack?.general && tauntBranch.basicAttack.general.length > 0) {
|
if (trigger === 'onOpponentAction' && typeof tauntSet === 'object' && !Array.isArray(tauntSet) && context.outcome) {
|
||||||
potentialTaunts = tauntBranch.basicAttack.general;
|
if (tauntSet[context.outcome]) {
|
||||||
|
potentialTaunts = tauntSet[context.outcome];
|
||||||
} else {
|
} else {
|
||||||
return "(Молчание)";
|
// console.log(`[TauntLogic] No outcome '${context.outcome}' for onOpponentAction, abilityId ${context.abilityId}`);
|
||||||
|
potentialTaunts = []; // Явно пустой, чтобы не упасть ниже
|
||||||
}
|
}
|
||||||
|
} else if (Array.isArray(tauntSet)) {
|
||||||
|
potentialTaunts = tauntSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Обработка basicAttack (merciful/dominating/general)
|
||||||
|
if (trigger === 'basicAttack' && specificTauntBranch.basicAttack) { // Убедимся что ветка basicAttack существует
|
||||||
|
const basicAttackBranch = specificTauntBranch.basicAttack;
|
||||||
|
if (speakerCharacterKey === 'elena' && opponentKeyForTaunts === 'balard' && currentGameState && currentGameState[GAME_CONFIG.OPPONENT_ID]) {
|
||||||
|
const opponentState = currentGameState[GAME_CONFIG.OPPONENT_ID]; // Балард всегда оппонент для Елены в этом контексте
|
||||||
|
if (opponentState && opponentState.maxHp > 0) {
|
||||||
|
const opponentHpPerc = (opponentState.currentHp / opponentState.maxHp) * 100;
|
||||||
|
if (opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT && basicAttackBranch.dominating) {
|
||||||
|
potentialTaunts = basicAttackBranch.dominating;
|
||||||
|
} else if (basicAttackBranch.merciful) {
|
||||||
|
potentialTaunts = basicAttackBranch.merciful;
|
||||||
|
} else if (basicAttackBranch.general) { // Фоллбэк на general если нет merciful
|
||||||
|
potentialTaunts = basicAttackBranch.general;
|
||||||
|
}
|
||||||
|
} else if (basicAttackBranch.general) { // Если нет HP данных, используем general
|
||||||
|
potentialTaunts = basicAttackBranch.general;
|
||||||
|
}
|
||||||
|
} else if (basicAttackBranch.general) { // Общий случай для basicAttack
|
||||||
|
potentialTaunts = basicAttackBranch.general;
|
||||||
|
}
|
||||||
|
// Если subTriggerKey был ('merciful'/'dominating') и он найден в basicAttackBranch, то tauntSet уже установлен выше
|
||||||
|
// Этот блок if (trigger === 'basicAttack') должен быть более специфичным или объединен с логикой subTriggerKey выше.
|
||||||
|
// Пока оставим как есть, предполагая, что subTriggerKey для basicAttack обрабатывается отдельно.
|
||||||
|
// Если subTriggerKey был 'merciful' или 'dominating', и такой ключ есть в basicAttackBranch, то tauntSet уже должен быть им.
|
||||||
|
if (subTriggerKey && basicAttackBranch[subTriggerKey]) {
|
||||||
|
potentialTaunts = basicAttackBranch[subTriggerKey];
|
||||||
|
} else if (potentialTaunts.length === 0 && basicAttackBranch.general) { // Если не нашли по subTriggerKey, берем general
|
||||||
|
potentialTaunts = basicAttackBranch.general;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
|
||||||
|
// console.log(`[TauntLogic] No potential taunts found or empty array for ${speakerCharacterKey} vs ${opponentKeyForTaunts}, trigger: ${trigger}, subTriggerKey: ${subTriggerKey}`);
|
||||||
|
return "(Молчание)";
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTaunt = potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)];
|
const selectedTaunt = potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)];
|
||||||
|
// console.log(`[TauntLogic] Selected for ${speakerCharacterKey} vs ${opponentKeyForTaunts} (Trigger: ${trigger}, SubTriggerKey: ${subTriggerKey}): "${selectedTaunt}"`);
|
||||||
return selectedTaunt || "(Молчание)";
|
return selectedTaunt || "(Молчание)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user