Первичное добавление jwt авторизации. Возможны ошибки
This commit is contained in:
parent
0b914621c2
commit
a9d2c7cb27
113
package-lock.json
generated
113
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
@ -97,6 +98,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@ -247,6 +254,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@ -619,6 +635,91 @@
|
|||||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
@ -901,6 +1002,18 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
|
@ -5,9 +5,69 @@ 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 сервера. Лучше вынести в конфигурацию или передавать.
|
||||||
|
// Для примера захардкодим, но в main.js можно будет это улучшить.
|
||||||
|
const API_BASE_URL = dependencies.API_BASE_URL || 'http://127.0.0.1:3200'; // Убедитесь, что это ваш URL
|
||||||
|
|
||||||
|
// Название ключа для хранения JWT в localStorage
|
||||||
|
const JWT_TOKEN_KEY = 'jwtToken';
|
||||||
|
|
||||||
|
async function handleAuthResponse(response, formType) {
|
||||||
|
const regButton = registerForm ? registerForm.querySelector('button') : null;
|
||||||
|
const loginButton = loginForm ? loginForm.querySelector('button') : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success && data.token) {
|
||||||
|
// Успешная аутентификация/регистрация
|
||||||
|
localStorage.setItem(JWT_TOKEN_KEY, data.token); // Сохраняем токен
|
||||||
|
|
||||||
|
clientState.isLoggedIn = true;
|
||||||
|
clientState.loggedInUsername = data.username;
|
||||||
|
clientState.myUserId = data.userId;
|
||||||
|
|
||||||
|
ui.setAuthMessage(''); // Очищаем сообщение об аутентификации
|
||||||
|
ui.showGameSelectionScreen(data.username); // Показываем экран выбора игры
|
||||||
|
|
||||||
|
// Важно: переподключить сокет с новым токеном
|
||||||
|
if (socket.connected) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
// Обновляем auth объект сокета перед подключением
|
||||||
|
// В main.js при создании сокета, он должен уже брать токен из localStorage
|
||||||
|
// Но если сокет уже существует, нужно обновить его auth данные
|
||||||
|
socket.auth = { token: data.token };
|
||||||
|
socket.connect(); // Это вызовет 'connect' и 'requestGameState' в main.js
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Ошибка аутентификации/регистрации
|
||||||
|
clientState.isLoggedIn = false;
|
||||||
|
clientState.loggedInUsername = '';
|
||||||
|
clientState.myUserId = null;
|
||||||
|
localStorage.removeItem(JWT_TOKEN_KEY); // Удаляем старый токен, если был
|
||||||
|
ui.setAuthMessage(data.message || 'Ошибка сервера.', true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ошибка парсинга JSON или другая сетевая ошибка
|
||||||
|
console.error(`[Auth] Error processing ${formType} response:`, error);
|
||||||
|
clientState.isLoggedIn = false;
|
||||||
|
clientState.loggedInUsername = '';
|
||||||
|
clientState.myUserId = null;
|
||||||
|
localStorage.removeItem(JWT_TOKEN_KEY);
|
||||||
|
ui.setAuthMessage('Произошла ошибка сети. Попробуйте снова.', true);
|
||||||
|
} finally {
|
||||||
|
// Разблокируем кнопки в любом случае
|
||||||
|
if (regButton) regButton.disabled = false;
|
||||||
|
if (loginButton) loginButton.disabled = false;
|
||||||
|
if (logoutButton && clientState.isLoggedIn) logoutButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Обработчики событий DOM ---
|
// --- Обработчики событий DOM ---
|
||||||
if (registerForm) {
|
if (registerForm) {
|
||||||
registerForm.addEventListener('submit', (e) => {
|
registerForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const usernameInput = document.getElementById('register-username');
|
const usernameInput = document.getElementById('register-username');
|
||||||
const passwordInput = document.getElementById('register-password');
|
const passwordInput = document.getElementById('register-password');
|
||||||
@ -16,19 +76,32 @@ export function initAuth(dependencies) {
|
|||||||
const username = usernameInput.value;
|
const username = usernameInput.value;
|
||||||
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;
|
||||||
if (regButton) regButton.disabled = true;
|
if (regButton) regButton.disabled = true;
|
||||||
if (loginButton) loginButton.disabled = true;
|
if (loginButton) loginButton.disabled = true;
|
||||||
|
|
||||||
ui.setAuthMessage('Регистрация...');
|
ui.setAuthMessage('Регистрация...');
|
||||||
socket.emit('register', { username, password });
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
await handleAuthResponse(response, 'register');
|
||||||
|
if (response.ok && registerForm) registerForm.reset(); // Очищаем форму при успехе
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Network error during registration:', error);
|
||||||
|
ui.setAuthMessage('Ошибка сети при регистрации. Пожалуйста, проверьте ваше подключение.', true);
|
||||||
|
if (regButton) regButton.disabled = false;
|
||||||
|
if (loginButton) loginButton.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginForm) {
|
if (loginForm) {
|
||||||
loginForm.addEventListener('submit', (e) => {
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const usernameInput = document.getElementById('login-username');
|
const usernameInput = document.getElementById('login-username');
|
||||||
const passwordInput = document.getElementById('login-password');
|
const passwordInput = document.getElementById('login-password');
|
||||||
@ -37,92 +110,101 @@ export function initAuth(dependencies) {
|
|||||||
const username = usernameInput.value;
|
const username = usernameInput.value;
|
||||||
const password = passwordInput.value;
|
const password = passwordInput.value;
|
||||||
|
|
||||||
// Блокируем кнопки на время запроса
|
|
||||||
const loginButton = loginForm.querySelector('button');
|
const loginButton = loginForm.querySelector('button');
|
||||||
const regButton = registerForm ? registerForm.querySelector('button') : null;
|
const regButton = registerForm ? registerForm.querySelector('button') : null;
|
||||||
if (loginButton) loginButton.disabled = true;
|
if (loginButton) loginButton.disabled = true;
|
||||||
if (regButton) regButton.disabled = true;
|
if (regButton) regButton.disabled = true;
|
||||||
|
|
||||||
ui.setAuthMessage('Вход...');
|
ui.setAuthMessage('Вход...');
|
||||||
socket.emit('login', { username, password });
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
await handleAuthResponse(response, 'login');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Network error during login:', error);
|
||||||
|
ui.setAuthMessage('Ошибка сети при входе. Пожалуйста, проверьте ваше подключение.', true);
|
||||||
|
if (loginButton) loginButton.disabled = false;
|
||||||
|
if (regButton) regButton.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutButton) {
|
if (logoutButton) {
|
||||||
logoutButton.addEventListener('click', () => {
|
logoutButton.addEventListener('click', () => {
|
||||||
logoutButton.disabled = true;
|
logoutButton.disabled = true;
|
||||||
socket.emit('logout');
|
|
||||||
|
|
||||||
// Обновляем состояние клиента немедленно, не дожидаясь ответа сервера (опционально)
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||||||
|
// Если игрок в активной PvP игре, отправляем сигнал о сдаче
|
||||||
|
if (clientState.isLoggedIn &&
|
||||||
|
clientState.isInGame &&
|
||||||
|
clientState.currentGameId &&
|
||||||
|
clientState.currentGameState && // Убедимся, что gameState существует
|
||||||
|
clientState.currentGameState.gameMode === 'pvp' && // Проверяем режим игры
|
||||||
|
!clientState.currentGameState.isGameOver) { // Только если игра еще не закончена
|
||||||
|
|
||||||
|
console.log('[Auth] Player is in an active PvP game. Emitting playerSurrender.');
|
||||||
|
socket.emit('playerSurrender');
|
||||||
|
// Не ждем ответа от сервера здесь, так как logout - это безусловное действие на клиенте.
|
||||||
|
// Сервер обработает 'playerSurrender' и соответствующим образом завершит игру.
|
||||||
|
}
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
|
|
||||||
|
// Серверный эндпоинт для логаута не обязателен для JWT,
|
||||||
|
// если нет необходимости аннулировать токен на сервере (что сложно с JWT).
|
||||||
|
// Основное действие - удаление токена на клиенте.
|
||||||
|
// socket.emit('logout'); // Можно оставить, если на сервере есть логика для этого (например, GameManager.handleDisconnect)
|
||||||
|
|
||||||
|
localStorage.removeItem(JWT_TOKEN_KEY); // Удаляем токен
|
||||||
|
|
||||||
clientState.isLoggedIn = false;
|
clientState.isLoggedIn = false;
|
||||||
clientState.loggedInUsername = '';
|
clientState.loggedInUsername = '';
|
||||||
clientState.myUserId = null;
|
clientState.myUserId = null;
|
||||||
// isInGame и другие игровые переменные сбросятся в ui.showAuthScreen()
|
// isInGame и другие игровые переменные сбросятся в ui.showAuthScreen()
|
||||||
// disableGameControls() также будет вызван опосредованно через showAuthScreen -> resetGameVariables
|
// ui.disableGameControls() также будет вызван опосредованно
|
||||||
|
|
||||||
ui.showAuthScreen(); // Показываем экран логина
|
ui.showAuthScreen(); // Показываем экран логина
|
||||||
ui.setGameStatusMessage("Вы вышли из системы."); // Используем gameStatusMessage для общего статуса после выхода
|
ui.setGameStatusMessage("Вы вышли из системы."); // Можно заменить на ui.setAuthMessage, если хотим видеть сообщение на экране логина
|
||||||
// ui.setAuthMessage("Вы вышли из системы."); // или authMessage, если он виден
|
|
||||||
// Кнопка разблокируется при следующем показе userInfoDiv или можно здесь
|
// Переподключаем сокет без токена
|
||||||
// logoutButton.disabled = false; // но лучше, чтобы UI сам управлял этим при показе
|
if (socket.connected) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
socket.auth = { token: null }; // Очищаем токен в auth объекте сокета
|
||||||
|
socket.connect(); // Сокет подключится как неаутентифицированный
|
||||||
|
|
||||||
|
// Кнопка logout будет активирована, когда пользователь снова войдет
|
||||||
|
// или если она видна только залогиненным пользователям, то исчезнет.
|
||||||
|
// (В showAuthScreen logoutButton.disabled устанавливается в true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Обработчики событий Socket.IO ---
|
// --- Обработчики событий Socket.IO ---
|
||||||
socket.on('registerResponse', (data) => {
|
// Старые 'registerResponse' и 'loginResponse' больше не нужны,
|
||||||
ui.setAuthMessage(data.message, !data.success);
|
// так как эти ответы приходят через HTTP.
|
||||||
if (data.success && registerForm) {
|
|
||||||
registerForm.reset(); // Очищаем форму при успехе
|
|
||||||
}
|
|
||||||
// Разблокируем кнопки
|
|
||||||
if (registerForm) {
|
|
||||||
const regButton = registerForm.querySelector('button');
|
|
||||||
if (regButton) regButton.disabled = false;
|
|
||||||
}
|
|
||||||
if (loginForm) {
|
|
||||||
const loginButton = loginForm.querySelector('button');
|
|
||||||
if (loginButton) loginButton.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('loginResponse', (data) => {
|
// Можно добавить обработчик для принудительного разлогинивания от сервера, если такой будет
|
||||||
if (data.success) {
|
|
||||||
clientState.isLoggedIn = true;
|
|
||||||
clientState.loggedInUsername = data.username;
|
|
||||||
clientState.myUserId = data.userId;
|
|
||||||
|
|
||||||
ui.setAuthMessage(""); // Очищаем сообщение об аутентификации
|
|
||||||
ui.showGameSelectionScreen(data.username); // Показываем экран выбора игры
|
|
||||||
// Запрос gameState при успешном логине и реконнекте теперь обрабатывается в main.js
|
|
||||||
// если пользователь уже был залогинен при 'connect'
|
|
||||||
} else {
|
|
||||||
clientState.isLoggedIn = false;
|
|
||||||
clientState.loggedInUsername = '';
|
|
||||||
clientState.myUserId = null;
|
|
||||||
ui.setAuthMessage(data.message, true); // Показываем ошибку
|
|
||||||
}
|
|
||||||
// Разблокируем кнопки
|
|
||||||
if (registerForm) {
|
|
||||||
const regButton = registerForm.querySelector('button');
|
|
||||||
if (regButton) regButton.disabled = false;
|
|
||||||
}
|
|
||||||
if (loginForm) {
|
|
||||||
const loginButton = loginForm.querySelector('button');
|
|
||||||
if (loginButton) loginButton.disabled = false;
|
|
||||||
}
|
|
||||||
// Убедимся, что кнопка logout активна, если пользователь успешно вошел
|
|
||||||
if (logoutButton && clientState.isLoggedIn) {
|
|
||||||
logoutButton.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Примечание: событие 'logout' от сервера обычно не требует специального обработчика здесь,
|
|
||||||
// так как клиент сам инициирует выход и обновляет UI.
|
|
||||||
// Если сервер принудительно разлогинивает, то такой обработчик может понадобиться.
|
|
||||||
// socket.on('forceLogout', (data) => {
|
// socket.on('forceLogout', (data) => {
|
||||||
|
// console.log('[Auth] Forced logout by server:', data.message);
|
||||||
|
// localStorage.removeItem(JWT_TOKEN_KEY);
|
||||||
// clientState.isLoggedIn = false;
|
// clientState.isLoggedIn = false;
|
||||||
// // ...
|
// clientState.loggedInUsername = '';
|
||||||
|
// clientState.myUserId = null;
|
||||||
// ui.showAuthScreen();
|
// ui.showAuthScreen();
|
||||||
// ui.setAuthMessage(data.message || "Вы были разлогинены сервером.");
|
// ui.setAuthMessage(data.message || "Вы были разлогинены сервером.");
|
||||||
|
// if (socket.connected) socket.disconnect();
|
||||||
|
// socket.auth = { token: null };
|
||||||
|
// socket.connect();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
// При загрузке модуля auth.js, проверяем, нет ли уже токена в localStorage
|
||||||
|
// Эта логика лучше всего будет работать в main.js при инициализации сокета,
|
||||||
|
// но здесь можно было бы сделать предварительную проверку и обновление clientState,
|
||||||
|
// если бы это было необходимо до создания сокета.
|
||||||
|
// Однако, поскольку сокет создается в main.js и сразу использует токен из localStorage,
|
||||||
|
// отдельная логика здесь не так критична.
|
||||||
}
|
}
|
@ -1,16 +1,9 @@
|
|||||||
// /public/js/gameplay.js
|
// /public/js/gameplay.js (Откаченная версия, совместимая с последним GameInstance.js)
|
||||||
|
|
||||||
export function initGameplay(dependencies) {
|
export function initGameplay(dependencies) {
|
||||||
const { socket, clientState, ui } = dependencies;
|
const { socket, clientState, ui } = dependencies;
|
||||||
// Элементы управления боем обычно находятся внутри gameWrapper и управляются через ui.js,
|
|
||||||
// но нам могут понадобиться ссылки на кнопки для привязки событий, если они не привязаны в ui.js
|
|
||||||
// или если ui.js не экспортирует их напрямую.
|
|
||||||
// В данном случае, attackButton и abilitiesGrid есть в client.js, так что получим их.
|
|
||||||
// ui.elements из main.js содержит returnToMenuButton
|
|
||||||
const { returnToMenuButton } = ui.elements;
|
const { returnToMenuButton } = ui.elements;
|
||||||
|
|
||||||
// Получаем ссылки на кнопки атаки и способностей напрямую, как было в client.js
|
|
||||||
// или, если бы ui.js их экспортировал, можно было бы через window.gameUI.uiElements
|
|
||||||
const attackButton = document.getElementById('button-attack');
|
const attackButton = document.getElementById('button-attack');
|
||||||
const abilitiesGrid = document.getElementById('abilities-grid');
|
const abilitiesGrid = document.getElementById('abilities-grid');
|
||||||
|
|
||||||
@ -18,28 +11,26 @@ export function initGameplay(dependencies) {
|
|||||||
function enableGameControls(enableAttack = true, enableAbilities = true) {
|
function enableGameControls(enableAttack = true, enableAbilities = true) {
|
||||||
if (attackButton) attackButton.disabled = !enableAttack;
|
if (attackButton) attackButton.disabled = !enableAttack;
|
||||||
if (abilitiesGrid) {
|
if (abilitiesGrid) {
|
||||||
// Предполагаем, что GAME_CONFIG доступен глобально или его нужно передать
|
|
||||||
const config = window.GAME_CONFIG || {};
|
const config = window.GAME_CONFIG || {};
|
||||||
const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
|
const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
|
||||||
abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; });
|
abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; });
|
||||||
}
|
}
|
||||||
// Если кнопка блока есть и управляется отсюда
|
if (window.gameUI?.updateUI) {
|
||||||
// if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true;
|
requestAnimationFrame(() => window.gameUI.updateUI());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableGameControls() {
|
function disableGameControls() {
|
||||||
enableGameControls(false, false);
|
enableGameControls(false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Эта функция была в client.js, переносим сюда
|
|
||||||
function initializeAbilityButtons() {
|
function initializeAbilityButtons() {
|
||||||
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
|
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
|
||||||
if (abilitiesGrid) abilitiesGrid.innerHTML = '<p class="placeholder-text">Ошибка загрузки способностей.</p>';
|
if (abilitiesGrid) abilitiesGrid.innerHTML = '<p class="placeholder-text">Ошибка загрузки способностей.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
abilitiesGrid.innerHTML = ''; // Очищаем предыдущие кнопки
|
abilitiesGrid.innerHTML = '';
|
||||||
const config = window.GAME_CONFIG;
|
const config = window.GAME_CONFIG;
|
||||||
// Используем данные из clientState, которые были обновлены из событий сервера
|
|
||||||
const abilitiesToDisplay = clientState.playerAbilitiesServer;
|
const abilitiesToDisplay = clientState.playerAbilitiesServer;
|
||||||
const baseStatsForResource = clientState.playerBaseStatsServer;
|
const baseStatsForResource = clientState.playerBaseStatsServer;
|
||||||
|
|
||||||
@ -56,34 +47,18 @@ export function initGameplay(dependencies) {
|
|||||||
button.id = `ability-btn-${ability.id}`;
|
button.id = `ability-btn-${ability.id}`;
|
||||||
button.classList.add(abilityButtonClass);
|
button.classList.add(abilityButtonClass);
|
||||||
button.dataset.abilityId = ability.id;
|
button.dataset.abilityId = ability.id;
|
||||||
|
let cooldown = ability.cooldown;
|
||||||
let cooldown = ability.cooldown; // Это базовый КД из данных персонажа
|
|
||||||
let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
|
let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
|
||||||
let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
|
let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
|
||||||
button.setAttribute('title', title);
|
button.setAttribute('title', title);
|
||||||
|
const nameSpan = document.createElement('span'); nameSpan.classList.add('ability-name'); nameSpan.textContent = ability.name; button.appendChild(nameSpan);
|
||||||
const nameSpan = document.createElement('span');
|
const descSpan = document.createElement('span'); descSpan.classList.add('ability-desc'); descSpan.textContent = `(${ability.cost} ${resourceName})`; button.appendChild(descSpan);
|
||||||
nameSpan.classList.add('ability-name');
|
const cdDisplay = document.createElement('span'); cdDisplay.classList.add('ability-cooldown-display'); cdDisplay.style.display = 'none'; button.appendChild(cdDisplay);
|
||||||
nameSpan.textContent = ability.name;
|
|
||||||
button.appendChild(nameSpan);
|
|
||||||
|
|
||||||
const descSpan = document.createElement('span');
|
|
||||||
descSpan.classList.add('ability-desc');
|
|
||||||
descSpan.textContent = `(${ability.cost} ${resourceName})`;
|
|
||||||
button.appendChild(descSpan);
|
|
||||||
|
|
||||||
const cdDisplay = document.createElement('span');
|
|
||||||
cdDisplay.classList.add('ability-cooldown-display'); // Для отображения текущего КД
|
|
||||||
cdDisplay.style.display = 'none'; // Скрыт по умолчанию
|
|
||||||
button.appendChild(cdDisplay);
|
|
||||||
|
|
||||||
button.addEventListener('click', handleAbilityButtonClick);
|
button.addEventListener('click', handleAbilityButtonClick);
|
||||||
abilitiesGrid.appendChild(button);
|
abilitiesGrid.appendChild(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
const placeholder = abilitiesGrid.querySelector('.placeholder-text');
|
const placeholder = abilitiesGrid.querySelector('.placeholder-text');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
// После инициализации кнопок, их состояние (disabled/enabled) будет управляться window.gameUI.updateUI()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAbilityButtonClick(event) {
|
function handleAbilityButtonClick(event) {
|
||||||
@ -94,22 +69,13 @@ export function initGameplay(dependencies) {
|
|||||||
abilityId &&
|
abilityId &&
|
||||||
clientState.currentGameState &&
|
clientState.currentGameState &&
|
||||||
!clientState.currentGameState.isGameOver) {
|
!clientState.currentGameState.isGameOver) {
|
||||||
// Перед отправкой действия можно добавить быструю проверку на клиенте (например, хватает ли ресурса),
|
|
||||||
// но основная валидация все равно на сервере.
|
|
||||||
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
|
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
|
||||||
disableGameControls(); // Блокируем управление до ответа сервера или следующего хода
|
disableGameControls();
|
||||||
} else {
|
} else {
|
||||||
console.warn("Cannot perform ability action, invalid state:", {
|
console.warn("Cannot perform ability action, invalid state");
|
||||||
isLoggedIn: clientState.isLoggedIn,
|
|
||||||
isInGame: clientState.isInGame,
|
|
||||||
gameId: clientState.currentGameId,
|
|
||||||
abilityId,
|
|
||||||
gameState: clientState.currentGameState
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Обработчики событий DOM ---
|
// --- Обработчики событий DOM ---
|
||||||
if (attackButton) {
|
if (attackButton) {
|
||||||
attackButton.addEventListener('click', () => {
|
attackButton.addEventListener('click', () => {
|
||||||
@ -119,95 +85,50 @@ export function initGameplay(dependencies) {
|
|||||||
clientState.currentGameState &&
|
clientState.currentGameState &&
|
||||||
!clientState.currentGameState.isGameOver) {
|
!clientState.currentGameState.isGameOver) {
|
||||||
socket.emit('playerAction', { actionType: 'attack' });
|
socket.emit('playerAction', { actionType: 'attack' });
|
||||||
disableGameControls(); // Блокируем управление до ответа сервера или следующего хода
|
disableGameControls();
|
||||||
} else {
|
} else {
|
||||||
console.warn("Cannot perform attack action, invalid state.");
|
console.warn("Cannot perform attack action, invalid state.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnToMenuButton) { // Кнопка из модалки GameOver
|
if (returnToMenuButton) {
|
||||||
returnToMenuButton.addEventListener('click', () => {
|
returnToMenuButton.addEventListener('click', () => {
|
||||||
if (!clientState.isLoggedIn) {
|
if (!clientState.isLoggedIn) {
|
||||||
ui.showAuthScreen(); // Если как-то оказались здесь без логина
|
ui.showAuthScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returnToMenuButton.disabled = true; // Блокируем на время перехода
|
returnToMenuButton.disabled = true;
|
||||||
// ui.resetGameVariables(); // Вызывается в showGameSelectionScreen
|
clientState.isInGame = false;
|
||||||
clientState.isInGame = false; // Устанавливаем, что мы больше не в игре
|
disableGameControls();
|
||||||
disableGameControls(); // Деактивируем игровые контролы
|
ui.showGameSelectionScreen(clientState.loggedInUsername);
|
||||||
// window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } }); // Скрываем модалку (делается в showGameSelectionScreen)
|
|
||||||
|
|
||||||
ui.showGameSelectionScreen(clientState.loggedInUsername); // Возвращаемся на экран выбора
|
|
||||||
// Кнопка returnToMenuButton включится при следующем показе модалки GameOver (логика в ui.js или здесь при gameOver)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Обработчики событий Socket.IO ---
|
|
||||||
socket.on('gameStarted', (data) => {
|
|
||||||
if (!clientState.isLoggedIn) return; // Игнорируем, если не залогинены
|
|
||||||
console.log('[Gameplay] Game started:', data);
|
|
||||||
|
|
||||||
// Обновляем состояние клиента
|
// --- ОБЩИЙ ОБРАБОТЧИК ДЛЯ ЗАПУСКА/ВОССТАНОВЛЕНИЯ ИГРЫ ---
|
||||||
clientState.currentGameId = data.gameId;
|
function handleGameDataReceived(data, eventName = "unknown") {
|
||||||
clientState.myPlayerId = data.yourPlayerId;
|
|
||||||
clientState.currentGameState = data.initialGameState;
|
|
||||||
clientState.playerBaseStatsServer = data.playerBaseStats;
|
|
||||||
clientState.opponentBaseStatsServer = data.opponentBaseStats;
|
|
||||||
clientState.playerAbilitiesServer = data.playerAbilities;
|
|
||||||
clientState.opponentAbilitiesServer = data.opponentAbilities;
|
|
||||||
clientState.myCharacterKey = data.playerBaseStats?.characterKey;
|
|
||||||
clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey;
|
|
||||||
clientState.isInGame = true;
|
|
||||||
|
|
||||||
if (data.clientConfig) { // Если сервер прислал конфиг
|
|
||||||
// Важно: GAME_CONFIG используется в ui.js и других местах
|
|
||||||
window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig };
|
|
||||||
} else if (!window.GAME_CONFIG) { // Базовый конфиг, если не пришел с сервера и не определен
|
|
||||||
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' /* ... другие важные ключи ... */ };
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
|
|
||||||
|
|
||||||
ui.showGameScreen(); // Показываем игровой экран
|
|
||||||
initializeAbilityButtons(); // Инициализируем кнопки способностей с новыми данными
|
|
||||||
|
|
||||||
// Очистка лога перед началом новой игры
|
|
||||||
if (window.gameUI?.uiElements?.log?.list) {
|
|
||||||
window.gameUI.uiElements.log.list.innerHTML = '';
|
|
||||||
}
|
|
||||||
// Добавление начальных логов, если есть
|
|
||||||
if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
|
|
||||||
data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Первичное обновление UI боевого экрана
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
|
|
||||||
window.gameUI.updateUI();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// ui.hideGameOverModal(); // Теперь делается в showGameScreen
|
|
||||||
ui.setGameStatusMessage(""); // Очищаем общий статус
|
|
||||||
// Таймер хода будет обновлен событием 'turnTimerUpdate'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Используется для восстановления состояния уже идущей игры (например, при реконнекте)
|
|
||||||
socket.on('gameState', (data) => {
|
|
||||||
if (!clientState.isLoggedIn) return;
|
if (!clientState.isLoggedIn) return;
|
||||||
console.log('[Gameplay] Received full gameState (e.g. on reconnect):', data);
|
const username = clientState.loggedInUsername || 'N/A'; // Для логов
|
||||||
|
console.log(`[CLIENT ${username}] ${eventName} received.`);
|
||||||
|
// if (data.log) console.log(`[CLIENT ${username}] ${eventName} log content:`, JSON.parse(JSON.stringify(data.log)));
|
||||||
|
|
||||||
|
|
||||||
// Обновляем состояние клиента (похоже на gameStarted)
|
|
||||||
clientState.currentGameId = data.gameId;
|
clientState.currentGameId = data.gameId;
|
||||||
clientState.myPlayerId = data.yourPlayerId;
|
clientState.myPlayerId = data.yourPlayerId;
|
||||||
clientState.currentGameState = data.gameState; // Используем gameState вместо initialGameState
|
clientState.currentGameState = data.initialGameState || data.gameState;
|
||||||
clientState.playerBaseStatsServer = data.playerBaseStats;
|
clientState.playerBaseStatsServer = data.playerBaseStats;
|
||||||
clientState.opponentBaseStatsServer = data.opponentBaseStats;
|
clientState.opponentBaseStatsServer = data.opponentBaseStats;
|
||||||
clientState.playerAbilitiesServer = data.playerAbilities;
|
clientState.playerAbilitiesServer = data.playerAbilities;
|
||||||
clientState.opponentAbilitiesServer = data.opponentAbilities;
|
clientState.opponentAbilitiesServer = data.opponentAbilities;
|
||||||
clientState.myCharacterKey = data.playerBaseStats?.characterKey;
|
clientState.myCharacterKey = data.playerBaseStats?.characterKey;
|
||||||
clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey;
|
clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey;
|
||||||
clientState.isInGame = true; // Устанавливаем, что мы в игре
|
|
||||||
|
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
|
clientState.isInGame = true;
|
||||||
|
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||||
|
clientState.isInGame = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.clientConfig) {
|
if (data.clientConfig) {
|
||||||
window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig };
|
window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig };
|
||||||
@ -216,46 +137,132 @@ export function initGameplay(dependencies) {
|
|||||||
}
|
}
|
||||||
ui.updateGlobalWindowVariablesForUI();
|
ui.updateGlobalWindowVariablesForUI();
|
||||||
|
|
||||||
if (!clientState.isInGame || document.querySelector('.game-wrapper').style.display === 'none') {
|
const gameWrapperElement = document.querySelector('.game-wrapper');
|
||||||
ui.showGameScreen(); // Показываем игровой экран, если еще не там
|
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
|
const isGameWrapperVisible = gameWrapperElement && (gameWrapperElement.style.display === 'flex' || getComputedStyle(gameWrapperElement).display === 'flex');
|
||||||
|
if (!isGameWrapperVisible) {
|
||||||
|
ui.showGameScreen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
initializeAbilityButtons(); // Переинициализируем кнопки способностей
|
|
||||||
|
|
||||||
// Лог при 'gameState' может быть уже накопленным, очищаем и добавляем новый
|
initializeAbilityButtons();
|
||||||
if (window.gameUI?.uiElements?.log?.list && data.log) {
|
|
||||||
window.gameUI.uiElements.log.list.innerHTML = '';
|
if (window.gameUI?.uiElements?.log?.list) {
|
||||||
|
// console.log(`[CLIENT ${username}] Log BEFORE clear in ${eventName}:`, window.gameUI.uiElements.log.list.innerHTML.substring(0,100));
|
||||||
|
window.gameUI.uiElements.log.list.innerHTML = ''; // Очищаем UI-лог перед добавлением новых
|
||||||
|
// console.log(`[CLIENT ${username}] Log AFTER clear in ${eventName}:`, window.gameUI.uiElements.log.list.innerHTML);
|
||||||
}
|
}
|
||||||
if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
|
if (window.gameUI?.addToLog && data.log) {
|
||||||
data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
|
data.log.forEach(logEntry => {
|
||||||
|
// console.log(`[CLIENT ${username}] Adding to UI log from ${eventName}: "${logEntry.message}"`);
|
||||||
|
window.gameUI.addToLog(logEntry.message, logEntry.type);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
|
if (window.gameUI?.updateUI) {
|
||||||
window.gameUI.updateUI();
|
window.gameUI.updateUI();
|
||||||
}
|
}
|
||||||
|
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver && window.GAME_CONFIG) {
|
||||||
|
const config = window.GAME_CONFIG;
|
||||||
|
const isMyActualTurn = clientState.myPlayerId &&
|
||||||
|
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
|
||||||
|
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
|
||||||
|
if (isMyActualTurn) {
|
||||||
|
enableGameControls();
|
||||||
|
} else {
|
||||||
|
disableGameControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// ui.hideGameOverModal(); // Делается в showGameScreen
|
|
||||||
// Таймер хода будет обновлен событием 'turnTimerUpdate'
|
// Управление gameStatusMessage
|
||||||
|
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||||
|
// gameOver имеет свой обработчик статуса (внутри socket.on('gameOver',...))
|
||||||
|
} else if (eventName === 'gameStarted' || eventName === 'gameState (reconnect)') {
|
||||||
|
// Это начало игры или восстановление сессии, статус должен быть чистым
|
||||||
|
console.log(`[CLIENT ${username}] ${eventName} - Clearing game status message because it's a fresh game/state load.`);
|
||||||
|
ui.setGameStatusMessage("");
|
||||||
|
} else {
|
||||||
|
// Для gameStateUpdate и других событий, не являющихся полной перезагрузкой,
|
||||||
|
// gameStatusMessage будет управляться в их обработчиках или через turnTimerUpdate.
|
||||||
|
// Если игра продолжается и не gameOver, общее сообщение "Ожидание" должно сниматься.
|
||||||
|
if (clientState.isInGame) {
|
||||||
|
ui.setGameStatusMessage("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если игра пришла завершенной, то showGameOver должен быть вызван.
|
||||||
|
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||||
|
if (window.gameUI?.showGameOver && !document.getElementById('game-over-screen').classList.contains('hidden')) {
|
||||||
|
// Экран уже показан
|
||||||
|
} else if (window.gameUI?.showGameOver) {
|
||||||
|
let playerWon = false;
|
||||||
|
if (data.winnerId) {
|
||||||
|
playerWon = data.winnerId === clientState.myPlayerId;
|
||||||
|
} else if (clientState.currentGameState.player && clientState.currentGameState.opponent) {
|
||||||
|
if (clientState.currentGameState.player.currentHp > 0 && clientState.currentGameState.opponent.currentHp <=0) {
|
||||||
|
playerWon = clientState.myPlayerId === clientState.currentGameState.player.id;
|
||||||
|
} else if (clientState.currentGameState.opponent.currentHp > 0 && clientState.currentGameState.player.currentHp <=0) {
|
||||||
|
playerWon = clientState.myPlayerId === clientState.currentGameState.opponent.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.gameUI.showGameOver(playerWon, data.reason || "Игра завершена", clientState.opponentCharacterKey, { finalGameState: clientState.currentGameState, ...data });
|
||||||
|
}
|
||||||
|
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Обработчики событий Socket.IO ---
|
||||||
|
socket.on('gameStarted', (data) => {
|
||||||
|
handleGameDataReceived(data, 'gameStarted');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('gameState', (data) => {
|
||||||
|
handleGameDataReceived(data, 'gameState (reconnect)');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('gameStateUpdate', (data) => {
|
socket.on('gameStateUpdate', (data) => {
|
||||||
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
||||||
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
|
console.log(`[CLIENT ${username}] Event: gameStateUpdate.`);
|
||||||
|
|
||||||
clientState.currentGameState = data.gameState;
|
clientState.currentGameState = data.gameState;
|
||||||
ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js
|
ui.updateGlobalWindowVariablesForUI();
|
||||||
|
|
||||||
if (window.gameUI?.updateUI) window.gameUI.updateUI();
|
if (window.gameUI?.updateUI) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.gameUI.updateUI();
|
||||||
|
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver && window.GAME_CONFIG) {
|
||||||
|
const config = window.GAME_CONFIG;
|
||||||
|
const isMyActualTurn = clientState.myPlayerId &&
|
||||||
|
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
|
||||||
|
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
|
||||||
|
|
||||||
|
if (isMyActualTurn) {
|
||||||
|
enableGameControls();
|
||||||
|
} else {
|
||||||
|
disableGameControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[CLIENT ${username}] gameStateUpdate - Clearing game status message as game is active.`);
|
||||||
|
ui.setGameStatusMessage("");
|
||||||
|
|
||||||
|
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||||
|
disableGameControls();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем только новые логи, если они есть в этом частичном обновлении
|
|
||||||
if (window.gameUI?.addToLog && data.log) {
|
if (window.gameUI?.addToLog && data.log) {
|
||||||
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
||||||
}
|
}
|
||||||
// Логика включения/выключения контролов на основе gameState.isPlayerTurn и myPlayerId
|
|
||||||
// обычно делается внутри window.gameUI.updateUI()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('logUpdate', (data) => {
|
socket.on('logUpdate', (data) => {
|
||||||
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
||||||
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
|
// console.log(`[CLIENT ${username}] Event: logUpdate. Logs:`, data.log);
|
||||||
if (window.gameUI?.addToLog && data.log) {
|
if (window.gameUI?.addToLog && data.log) {
|
||||||
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
||||||
}
|
}
|
||||||
@ -263,77 +270,93 @@ export function initGameplay(dependencies) {
|
|||||||
|
|
||||||
socket.on('gameOver', (data) => {
|
socket.on('gameOver', (data) => {
|
||||||
if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
|
if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
|
||||||
// Если мы не в игре, но залогинены, запросим состояние (вдруг это старое событие)
|
|
||||||
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
|
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
|
||||||
else if (!clientState.isLoggedIn) ui.showAuthScreen(); // Если вообще не залогинены
|
else if (!clientState.isLoggedIn) ui.showAuthScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
|
console.log(`[CLIENT ${username}] Event: gameOver.`);
|
||||||
|
|
||||||
const playerWon = data.winnerId === clientState.myPlayerId;
|
const playerWon = data.winnerId === clientState.myPlayerId;
|
||||||
clientState.currentGameState = data.finalGameState; // Обновляем состояние последним
|
clientState.currentGameState = data.finalGameState;
|
||||||
// clientState.isInGame = false; // НЕ СБРАСЫВАЕМ ЗДЕСЬ, чтобы UI показывал экран GameOver. Сбросится при выходе в меню.
|
clientState.isInGame = false;
|
||||||
|
|
||||||
ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js
|
ui.updateGlobalWindowVariablesForUI();
|
||||||
|
|
||||||
if (window.gameUI?.updateUI) window.gameUI.updateUI(); // Обновляем панели в последний раз
|
|
||||||
|
|
||||||
|
if (window.gameUI?.updateUI) requestAnimationFrame(() => window.gameUI.updateUI());
|
||||||
if (window.gameUI?.addToLog && data.log) {
|
if (window.gameUI?.addToLog && data.log) {
|
||||||
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.gameUI?.showGameOver) {
|
if (window.gameUI?.showGameOver) {
|
||||||
const oppKey = clientState.opponentBaseStatsServer?.characterKey;
|
const oppKey = clientState.opponentBaseStatsServer?.characterKey;
|
||||||
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data); // ui.js покажет модалку
|
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
|
||||||
}
|
}
|
||||||
|
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||||
if (returnToMenuButton) returnToMenuButton.disabled = false; // Активируем кнопку "Вернуться в меню"
|
// `ui.setGameStatusMessage` будет установлено специфичным сообщением о результате игры
|
||||||
|
// ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
||||||
ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
|
||||||
|
|
||||||
// Обновляем UI таймера, чтобы показать "Конец" или скрыть
|
|
||||||
if (window.gameUI?.updateTurnTimerDisplay) {
|
if (window.gameUI?.updateTurnTimerDisplay) {
|
||||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
|
||||||
}
|
}
|
||||||
// Контролы должны быть заблокированы, т.к. игра окончена (ui.js->updateUI это сделает)
|
disableGameControls();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('opponentDisconnected', (data) => {
|
socket.on('opponentDisconnected', (data) => {
|
||||||
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
||||||
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
|
console.log(`[CLIENT ${username}] Event: opponentDisconnected.`);
|
||||||
const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник';
|
const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник';
|
||||||
if (window.gameUI?.addToLog) {
|
|
||||||
window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если игра еще не окончена, сервер может дать время на переподключение или объявить победу
|
// Сообщение об отключении оппонента должно приходить через 'logUpdate' от сервера
|
||||||
|
// if (window.gameUI?.addToLog) {
|
||||||
|
// window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
|
||||||
|
// }
|
||||||
|
|
||||||
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
|
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
|
||||||
disableGameControls(); // Блокируем управление, пока сервер не решит исход
|
disableGameControls();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('turnTimerUpdate', (data) => {
|
socket.on('turnTimerUpdate', (data) => {
|
||||||
if (!clientState.isInGame || !clientState.currentGameState || clientState.currentGameState.isGameOver) {
|
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
|
||||||
// Если игра окончена или не в игре, обновляем таймер соответственно
|
if (window.gameUI?.updateTurnTimerDisplay && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
if (window.gameUI?.updateTurnTimerDisplay && !clientState.currentGameState?.isGameOver) {
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
||||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
|
if (clientState.currentGameState.isGameOver) {
|
||||||
const config = window.GAME_CONFIG || {};
|
if (window.gameUI?.updateTurnTimerDisplay) {
|
||||||
// Определяем, является ли текущий ход ходом этого клиента
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
||||||
const isMyActualTurn = clientState.myPlayerId && clientState.currentGameState.isPlayerTurn === (clientState.myPlayerId === config.PLAYER_ID);
|
}
|
||||||
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode);
|
disableGameControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
|
// console.log(`[CLIENT ${username}] Event: turnTimerUpdate.`);
|
||||||
|
|
||||||
|
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
|
||||||
|
const config = window.GAME_CONFIG;
|
||||||
|
const isMyActualTurn = clientState.myPlayerId && clientState.currentGameState &&
|
||||||
|
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
|
||||||
|
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
|
||||||
|
|
||||||
|
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode);
|
||||||
|
|
||||||
|
if (isMyActualTurn) {
|
||||||
|
enableGameControls();
|
||||||
|
} else {
|
||||||
|
disableGameControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientState.currentGameState.isGameOver) {
|
||||||
|
console.log(`[CLIENT ${username}] turnTimerUpdate - Clearing game status message as timer is active.`);
|
||||||
|
ui.setGameStatusMessage("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Логика включения/выключения контролов на основе isMyActualTurn
|
|
||||||
// обычно выполняется в window.gameUI.updateUI(), которая вызывается после gameStateUpdate.
|
|
||||||
// Если turnTimerUpdate приходит отдельно и должен влиять на контролы, то нужно добавить:
|
|
||||||
// if (isMyActualTurn) enableGameControls(); else disableGameControls();
|
|
||||||
// Но это может конфликтовать с логикой в updateUI(). Обычно updateUI() - главный источник правды.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Начальная деактивация игровых контролов при загрузке модуля
|
// Начальная деактивация
|
||||||
disableGameControls();
|
disableGameControls();
|
||||||
}
|
}
|
@ -3,56 +3,37 @@
|
|||||||
import { initAuth } from './auth.js';
|
import { initAuth } from './auth.js';
|
||||||
import { initGameSetup } from './gameSetup.js';
|
import { initGameSetup } from './gameSetup.js';
|
||||||
import { initGameplay } from './gameplay.js';
|
import { initGameplay } from './gameplay.js';
|
||||||
// Предполагаем, что ui.js загружен перед этим скриптом (в HTML)
|
// ui.js загружен глобально
|
||||||
// и создал глобальный объект window.gameUI
|
|
||||||
// Также ui.js будет читать window.gameState, window.gameData, window.myPlayerId, window.GAME_CONFIG
|
function parseJwtPayload(token) {
|
||||||
|
try {
|
||||||
|
if (typeof token !== 'string') { return null; }
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length !== 3) { return null; }
|
||||||
|
const base64Url = parts[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[parseJwtPayload] Error parsing JWT payload:", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const socket = io({
|
const SERVER_URL = 'http://127.0.0.1:3200';
|
||||||
// Опции Socket.IO, если нужны
|
const API_BASE_URL = SERVER_URL;
|
||||||
});
|
const initialToken = localStorage.getItem('jwtToken');
|
||||||
|
|
||||||
// --- DOM Элементы для общего UI-управления ---
|
|
||||||
// (Эти элементы управляют общим потоком приложения, а не деталями боя)
|
|
||||||
const authSection = document.getElementById('auth-section');
|
|
||||||
const loginForm = document.getElementById('login-form');
|
|
||||||
const registerForm = document.getElementById('register-form');
|
|
||||||
const authMessage = document.getElementById('auth-message');
|
|
||||||
|
|
||||||
const statusContainer = document.getElementById('status-container');
|
|
||||||
const userInfoDiv = document.getElementById('user-info');
|
|
||||||
const loggedInUsernameSpan = document.getElementById('logged-in-username');
|
|
||||||
const logoutButton = document.getElementById('logout-button'); // Для auth.js
|
|
||||||
|
|
||||||
const gameSetupDiv = document.getElementById('game-setup');
|
|
||||||
const createAIGameButton = document.getElementById('create-ai-game');
|
|
||||||
const createPvPGameButton = document.getElementById('create-pvp-game');
|
|
||||||
const joinPvPGameButton = document.getElementById('join-pvp-game');
|
|
||||||
const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
|
|
||||||
const gameIdInput = document.getElementById('game-id-input');
|
|
||||||
const availableGamesDiv = document.getElementById('available-games-list');
|
|
||||||
const gameStatusMessage = document.getElementById('game-status-message');
|
|
||||||
const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]'); // для gameSetup.js
|
|
||||||
|
|
||||||
const gameWrapper = document.querySelector('.game-wrapper');
|
|
||||||
// Элементы, связанные с gameOver, управляются через window.gameUI.showGameOver,
|
|
||||||
// но кнопка "Вернуться в меню" может быть здесь для общего сброса.
|
|
||||||
const returnToMenuButton = document.getElementById('return-to-menu-button');
|
|
||||||
const turnTimerContainer = document.getElementById('turn-timer-container');
|
|
||||||
const turnTimerSpan = document.getElementById('turn-timer');
|
|
||||||
|
|
||||||
|
|
||||||
// --- Состояние клиента (глобальное для main и передаваемое в модули) ---
|
|
||||||
// Это состояние будет модифицироваться из разных модулей
|
|
||||||
let clientState = {
|
let clientState = {
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
loggedInUsername: '',
|
loggedInUsername: '',
|
||||||
myUserId: null,
|
myUserId: null,
|
||||||
isInGame: false,
|
isInGame: false,
|
||||||
// Игровые переменные, которые ранее были глобальными в client.js
|
|
||||||
// и от которых зависит ui.js
|
|
||||||
currentGameId: null,
|
currentGameId: null,
|
||||||
currentGameState: null,
|
currentGameState: null, // Будет объектом или null
|
||||||
myPlayerId: null,
|
myPlayerId: null,
|
||||||
myCharacterKey: null,
|
myCharacterKey: null,
|
||||||
opponentCharacterKey: null,
|
opponentCharacterKey: null,
|
||||||
@ -62,10 +43,55 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
opponentAbilitiesServer: null,
|
opponentAbilitiesServer: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обновляем глобальные переменные window, на которые рассчитывает ui.js
|
if (initialToken) {
|
||||||
// Это временная мера. В идеале, ui.js должен получать эти данные как аргументы функций.
|
const decodedToken = parseJwtPayload(initialToken);
|
||||||
|
if (decodedToken && decodedToken.userId && decodedToken.username) {
|
||||||
|
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||||
|
if (decodedToken.exp && decodedToken.exp > nowInSeconds) {
|
||||||
|
console.log("[Client Init] Token found, pre-populating clientState.");
|
||||||
|
clientState.isLoggedIn = true;
|
||||||
|
clientState.myUserId = decodedToken.userId;
|
||||||
|
clientState.loggedInUsername = decodedToken.username;
|
||||||
|
} else {
|
||||||
|
console.warn("[Client Init] Token expired or invalid 'exp'. Clearing.");
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("[Client Init] Token invalid or missing data. Clearing.");
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = io(SERVER_URL, {
|
||||||
|
autoConnect: false,
|
||||||
|
auth: { token: localStorage.getItem('jwtToken') }
|
||||||
|
});
|
||||||
|
|
||||||
|
const authSection = document.getElementById('auth-section');
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const registerForm = document.getElementById('register-form');
|
||||||
|
const authMessage = document.getElementById('auth-message');
|
||||||
|
const statusContainer = document.getElementById('status-container');
|
||||||
|
const userInfoDiv = document.getElementById('user-info');
|
||||||
|
const loggedInUsernameSpan = document.getElementById('logged-in-username');
|
||||||
|
const logoutButton = document.getElementById('logout-button');
|
||||||
|
const gameSetupDiv = document.getElementById('game-setup');
|
||||||
|
const createAIGameButton = document.getElementById('create-ai-game');
|
||||||
|
const createPvPGameButton = document.getElementById('create-pvp-game');
|
||||||
|
const joinPvPGameButton = document.getElementById('join-pvp-game');
|
||||||
|
const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
|
||||||
|
const gameIdInput = document.getElementById('game-id-input');
|
||||||
|
const availableGamesDiv = document.getElementById('available-games-list');
|
||||||
|
const gameStatusMessage = document.getElementById('game-status-message');
|
||||||
|
const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]');
|
||||||
|
const gameWrapper = document.querySelector('.game-wrapper');
|
||||||
|
const returnToMenuButton = document.getElementById('return-to-menu-button'); // Он же в ui.elements.gameOver.returnToMenuButton
|
||||||
|
const turnTimerContainer = document.getElementById('turn-timer-container');
|
||||||
|
const turnTimerSpan = document.getElementById('turn-timer');
|
||||||
|
|
||||||
function updateGlobalWindowVariablesForUI() {
|
function updateGlobalWindowVariablesForUI() {
|
||||||
window.gameState = clientState.currentGameState;
|
// console.log("[Main] Updating global window variables. currentGameState:", clientState.currentGameState ? JSON.parse(JSON.stringify(clientState.currentGameState)) : null);
|
||||||
|
window.gameState = clientState.currentGameState; // Может быть null
|
||||||
window.gameData = {
|
window.gameData = {
|
||||||
playerBaseStats: clientState.playerBaseStatsServer,
|
playerBaseStats: clientState.playerBaseStatsServer,
|
||||||
opponentBaseStats: clientState.opponentBaseStatsServer,
|
opponentBaseStats: clientState.opponentBaseStatsServer,
|
||||||
@ -73,78 +99,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
opponentAbilities: clientState.opponentAbilitiesServer
|
opponentAbilities: clientState.opponentAbilitiesServer
|
||||||
};
|
};
|
||||||
window.myPlayerId = clientState.myPlayerId;
|
window.myPlayerId = clientState.myPlayerId;
|
||||||
// window.GAME_CONFIG остается как есть, если он глобальный и не меняется часто
|
// window.GAME_CONFIG устанавливается при gameStarted/gameState из gameplay.js
|
||||||
// Если GAME_CONFIG приходит от сервера, его тоже нужно обновлять здесь
|
|
||||||
// if (clientState.serverConfig) window.GAME_CONFIG = { ...clientState.serverConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- Функции управления UI (для переключения основных экранов и общих сообщений) ---
|
|
||||||
function showAuthScreen() {
|
|
||||||
authSection.style.display = 'block';
|
|
||||||
userInfoDiv.style.display = 'none';
|
|
||||||
gameSetupDiv.style.display = 'none';
|
|
||||||
gameWrapper.style.display = 'none';
|
|
||||||
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver, если была
|
|
||||||
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
|
|
||||||
}
|
|
||||||
setAuthMessage("Ожидание подключения к серверу...");
|
|
||||||
statusContainer.style.display = 'block';
|
|
||||||
clientState.isInGame = false;
|
|
||||||
// disableGameControls(); // Вызов будет из gameplay.js
|
|
||||||
resetGameVariables(); // Важно для сброса состояния
|
|
||||||
updateGlobalWindowVariablesForUI(); // Обновляем глоб. переменные для ui.js
|
|
||||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
|
||||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showGameSelectionScreen(username) {
|
|
||||||
authSection.style.display = 'none';
|
|
||||||
userInfoDiv.style.display = 'block';
|
|
||||||
loggedInUsernameSpan.textContent = username;
|
|
||||||
gameSetupDiv.style.display = 'block';
|
|
||||||
gameWrapper.style.display = 'none';
|
|
||||||
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver
|
|
||||||
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
|
|
||||||
}
|
|
||||||
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
|
|
||||||
statusContainer.style.display = 'block';
|
|
||||||
socket.emit('requestPvPGameList'); // Запрашиваем список игр
|
|
||||||
if (availableGamesDiv) availableGamesDiv.innerHTML = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>'; // Очистка перед запросом
|
|
||||||
if (gameIdInput) gameIdInput.value = '';
|
|
||||||
|
|
||||||
const elenaRadio = document.getElementById('char-elena'); // Для сброса выбора персонажа
|
|
||||||
if (elenaRadio) elenaRadio.checked = true;
|
|
||||||
|
|
||||||
clientState.isInGame = false;
|
|
||||||
// disableGameControls(); // Вызов будет из gameplay.js
|
|
||||||
resetGameVariables();
|
|
||||||
updateGlobalWindowVariablesForUI();
|
|
||||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
|
||||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
|
||||||
enableSetupButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showGameScreen() {
|
|
||||||
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver
|
|
||||||
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
|
|
||||||
}
|
|
||||||
authSection.style.display = 'none';
|
|
||||||
userInfoDiv.style.display = 'block'; // Оставляем инфо о пользователе
|
|
||||||
gameSetupDiv.style.display = 'none';
|
|
||||||
gameWrapper.style.display = 'flex';
|
|
||||||
setGameStatusMessage(""); // Очищаем статус
|
|
||||||
statusContainer.style.display = 'none'; // Скрываем общий статус контейнер
|
|
||||||
clientState.isInGame = true;
|
|
||||||
// disableGameControls(); // Начальная деактивация, gameplay.js включит при ходе
|
|
||||||
updateGlobalWindowVariablesForUI(); // Убедимся, что ui.js имеет свежие данные
|
|
||||||
if (turnTimerContainer) turnTimerContainer.style.display = 'block';
|
|
||||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetGameVariables() {
|
function resetGameVariables() {
|
||||||
|
console.log("[Main:resetGameVariables] Resetting game variables. State BEFORE:", JSON.parse(JSON.stringify(clientState)));
|
||||||
clientState.currentGameId = null;
|
clientState.currentGameId = null;
|
||||||
|
// ВАЖНО: currentGameState должен быть сброшен в состояние "нет игры"
|
||||||
|
// Либо null, либо объект, который ui.js интерпретирует как "нет игры"
|
||||||
clientState.currentGameState = null;
|
clientState.currentGameState = null;
|
||||||
|
// Можно также так, если ui.js лучше работает с объектом:
|
||||||
|
// clientState.currentGameState = { isGameOver: false, player: null, opponent: null, turnNumber: 0 };
|
||||||
|
|
||||||
clientState.myPlayerId = null;
|
clientState.myPlayerId = null;
|
||||||
clientState.myCharacterKey = null;
|
clientState.myCharacterKey = null;
|
||||||
clientState.opponentCharacterKey = null;
|
clientState.opponentCharacterKey = null;
|
||||||
@ -152,17 +118,123 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
clientState.opponentBaseStatsServer = null;
|
clientState.opponentBaseStatsServer = null;
|
||||||
clientState.playerAbilitiesServer = null;
|
clientState.playerAbilitiesServer = null;
|
||||||
clientState.opponentAbilitiesServer = null;
|
clientState.opponentAbilitiesServer = null;
|
||||||
// Также обновляем глобальные переменные для ui.js
|
// clientState.isInGame будет установлено в вызывающей функции (showAuthScreen/showGameSelectionScreen)
|
||||||
updateGlobalWindowVariablesForUI();
|
|
||||||
|
updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные СРАЗУ после сброса
|
||||||
|
console.log("[Main:resetGameVariables] Game variables reset. State AFTER:", JSON.parse(JSON.stringify(clientState)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function explicitlyHideGameOverModal() {
|
||||||
|
console.log("[Main:explicitlyHideGameOverModal] Attempting to hide Game Over modal.");
|
||||||
|
if (window.gameUI?.uiElements?.gameOver?.screen && window.GAME_CONFIG) {
|
||||||
|
const gameOverScreenElement = window.gameUI.uiElements.gameOver.screen;
|
||||||
|
const modalContentElement = window.gameUI.uiElements.gameOver.modalContent;
|
||||||
|
const messageElement = window.gameUI.uiElements.gameOver.message;
|
||||||
|
const hiddenClass = window.GAME_CONFIG.CSS_CLASS_HIDDEN || 'hidden';
|
||||||
|
|
||||||
|
if (gameOverScreenElement && !gameOverScreenElement.classList.contains(hiddenClass)) {
|
||||||
|
gameOverScreenElement.classList.add(hiddenClass);
|
||||||
|
// Принудительно сбрасываем стили для анимации скрытия, если она есть
|
||||||
|
gameOverScreenElement.style.opacity = '0';
|
||||||
|
if (modalContentElement) {
|
||||||
|
modalContentElement.style.transform = 'scale(0.8) translateY(30px)';
|
||||||
|
modalContentElement.style.opacity = '0';
|
||||||
|
}
|
||||||
|
console.log("[Main:explicitlyHideGameOverModal] Game Over screen explicitly hidden.");
|
||||||
|
} else if (gameOverScreenElement) {
|
||||||
|
console.log("[Main:explicitlyHideGameOverModal] Game Over screen was already hidden or not found.");
|
||||||
|
}
|
||||||
|
if (messageElement) messageElement.textContent = ''; // Очищаем сообщение
|
||||||
|
} else {
|
||||||
|
console.warn("[Main:explicitlyHideGameOverModal] Cannot hide Game Over modal: gameUI or GAME_CONFIG not available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showAuthScreen() {
|
||||||
|
console.log("[Main:showAuthScreen] Showing Auth Screen. Resetting game state.");
|
||||||
|
authSection.style.display = 'block';
|
||||||
|
userInfoDiv.style.display = 'none';
|
||||||
|
gameSetupDiv.style.display = 'none';
|
||||||
|
gameWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
explicitlyHideGameOverModal(); // <-- ЯВНО СКРЫВАЕМ МОДАЛКУ
|
||||||
|
|
||||||
|
statusContainer.style.display = 'block';
|
||||||
|
clientState.isInGame = false; // Важно
|
||||||
|
resetGameVariables(); // Сбрасываем все переменные предыдущей игры
|
||||||
|
|
||||||
|
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||||
|
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||||
|
if(registerForm) registerForm.querySelector('button').disabled = false;
|
||||||
|
if(loginForm) loginForm.querySelector('button').disabled = false;
|
||||||
|
if(logoutButton) logoutButton.disabled = true; // Кнопка Logout должна быть недоступна на экране логина
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGameSelectionScreen(username) {
|
||||||
|
console.log(`[Main:showGameSelectionScreen] Showing Game Selection Screen for ${username}. Resetting game state.`);
|
||||||
|
authSection.style.display = 'none';
|
||||||
|
userInfoDiv.style.display = 'block';
|
||||||
|
if(loggedInUsernameSpan) loggedInUsernameSpan.textContent = username;
|
||||||
|
if(logoutButton) logoutButton.disabled = false; // Logout доступен
|
||||||
|
gameSetupDiv.style.display = 'block';
|
||||||
|
gameWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
explicitlyHideGameOverModal(); // <-- ЯВНО СКРЫВАЕМ МОДАЛКУ
|
||||||
|
|
||||||
|
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
|
||||||
|
statusContainer.style.display = 'block';
|
||||||
|
|
||||||
|
if (socket.connected) {
|
||||||
|
socket.emit('requestPvPGameList');
|
||||||
|
} else {
|
||||||
|
console.warn("[Main:showGameSelectionScreen] Socket not connected, cannot request PvP game list yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableGamesDiv) availableGamesDiv.innerHTML = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>';
|
||||||
|
if (gameIdInput) gameIdInput.value = '';
|
||||||
|
const elenaRadio = document.getElementById('char-elena');
|
||||||
|
if (elenaRadio) elenaRadio.checked = true;
|
||||||
|
|
||||||
|
clientState.isInGame = false; // Важно
|
||||||
|
resetGameVariables(); // Сбрасываем все переменные предыдущей игры
|
||||||
|
|
||||||
|
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||||
|
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||||
|
enableSetupButtons();
|
||||||
|
// Убедимся, что кнопка "Вернуться в меню" на gameOver модалке (если она вдруг видима) активна,
|
||||||
|
// хотя сама модалка должна быть скрыта.
|
||||||
|
if (window.gameUI?.uiElements?.gameOver?.returnToMenuButton) {
|
||||||
|
window.gameUI.uiElements.gameOver.returnToMenuButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGameScreen() {
|
||||||
|
console.log("[Main:showGameScreen] Showing Game Screen.");
|
||||||
|
// Не нужно здесь вызывать explicitlyHideGameOverModal, так как если игра начинается,
|
||||||
|
// а модалка была видима, это ошибка логики где-то еще.
|
||||||
|
// GameStarted/GameState должно само приводить UI в порядок.
|
||||||
|
authSection.style.display = 'none';
|
||||||
|
userInfoDiv.style.display = 'block';
|
||||||
|
if(logoutButton) logoutButton.disabled = false;
|
||||||
|
gameSetupDiv.style.display = 'none';
|
||||||
|
gameWrapper.style.display = 'flex';
|
||||||
|
setGameStatusMessage("");
|
||||||
|
statusContainer.style.display = 'none';
|
||||||
|
clientState.isInGame = true; // Важно
|
||||||
|
updateGlobalWindowVariablesForUI(); // Обновляем перед тем, как UI начнет рендерить игровой экран
|
||||||
|
if (turnTimerContainer) turnTimerContainer.style.display = 'block';
|
||||||
|
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function setAuthMessage(message, isError = false) {
|
function setAuthMessage(message, isError = false) {
|
||||||
if (authMessage) {
|
if (authMessage) {
|
||||||
authMessage.textContent = message;
|
authMessage.textContent = message;
|
||||||
authMessage.className = isError ? 'error' : 'success';
|
authMessage.className = isError ? 'error' : 'success';
|
||||||
authMessage.style.display = message ? 'block' : 'none';
|
authMessage.style.display = message ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
if (message && gameStatusMessage) gameStatusMessage.style.display = 'none'; // Скрываем другой статус
|
if (message && gameStatusMessage && gameStatusMessage.style.display !== 'none') gameStatusMessage.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGameStatusMessage(message, isError = false) {
|
function setGameStatusMessage(message, isError = false) {
|
||||||
@ -172,10 +244,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
|
gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
|
||||||
if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
|
if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
if (message && authMessage) authMessage.style.display = 'none'; // Скрываем другой статус
|
if (message && authMessage && authMessage.style.display !== 'none') authMessage.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции для управления кнопками на экране выбора игры (могут быть вызваны из gameSetup)
|
|
||||||
function disableSetupButtons() {
|
function disableSetupButtons() {
|
||||||
if(createAIGameButton) createAIGameButton.disabled = true;
|
if(createAIGameButton) createAIGameButton.disabled = true;
|
||||||
if(createPvPGameButton) createPvPGameButton.disabled = true;
|
if(createPvPGameButton) createPvPGameButton.disabled = true;
|
||||||
@ -188,109 +259,159 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if(createPvPGameButton) createPvPGameButton.disabled = false;
|
if(createPvPGameButton) createPvPGameButton.disabled = false;
|
||||||
if(joinPvPGameButton) joinPvPGameButton.disabled = false;
|
if(joinPvPGameButton) joinPvPGameButton.disabled = false;
|
||||||
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
|
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
|
||||||
// Кнопки в списке игр включаются в updateAvailableGamesList (в gameSetup.js)
|
// Кнопки в списке доступных игр управляются в gameSetup.js -> updateAvailableGamesList
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Сборка зависимостей для передачи в модули ---
|
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
socket,
|
socket,
|
||||||
clientState, // Объект состояния, который модули могут читать и изменять
|
clientState,
|
||||||
ui: { // Функции и элементы для управления общим UI и состоянием
|
ui: {
|
||||||
showAuthScreen,
|
showAuthScreen,
|
||||||
showGameSelectionScreen,
|
showGameSelectionScreen,
|
||||||
showGameScreen,
|
showGameScreen,
|
||||||
setAuthMessage,
|
setAuthMessage,
|
||||||
setGameStatusMessage,
|
setGameStatusMessage,
|
||||||
resetGameVariables,
|
resetGameVariables, // Передаем, чтобы другие модули могли вызвать при необходимости (хотя лучше избегать)
|
||||||
updateGlobalWindowVariablesForUI, // Важно для ui.js
|
updateGlobalWindowVariablesForUI,
|
||||||
disableSetupButtons,
|
disableSetupButtons,
|
||||||
enableSetupButtons,
|
enableSetupButtons,
|
||||||
elements: { // Передаем элементы, нужные для специфической логики модулей
|
elements: {
|
||||||
// Для auth.js
|
loginForm, registerForm, logoutButton,
|
||||||
loginForm,
|
createAIGameButton, createPvPGameButton, joinPvPGameButton,
|
||||||
registerForm,
|
findRandomPvPGameButton, gameIdInput, availableGamesDiv,
|
||||||
logoutButton,
|
pvpCharacterRadios, returnToMenuButton, // returnToMenuButton из gameplay.js, но здесь тоже может быть полезен
|
||||||
// Для gameSetup.js
|
|
||||||
createAIGameButton,
|
|
||||||
createPvPGameButton,
|
|
||||||
joinPvPGameButton,
|
|
||||||
findRandomPvPGameButton,
|
|
||||||
gameIdInput,
|
|
||||||
availableGamesDiv,
|
|
||||||
pvpCharacterRadios,
|
|
||||||
// Для gameplay.js (или для обработки gameover здесь)
|
|
||||||
returnToMenuButton,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// gameUI: window.gameUI // Можно передать, если модули должны напрямую вызывать gameUI.
|
API_BASE_URL: API_BASE_URL
|
||||||
// Но пока gameplay.js будет использовать глобальный window.gameUI
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Инициализация модулей
|
|
||||||
initAuth(dependencies);
|
initAuth(dependencies);
|
||||||
initGameSetup(dependencies);
|
initGameSetup(dependencies);
|
||||||
initGameplay(dependencies);
|
initGameplay(dependencies);
|
||||||
|
|
||||||
|
|
||||||
// --- Обработчики событий Socket.IO (глобальные для приложения) ---
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('[Client] Socket connected:', socket.id);
|
const currentToken = socket.auth.token || localStorage.getItem('jwtToken');
|
||||||
setAuthMessage("Успешно подключено к серверу. Вход...");
|
console.log('[Main:SocketConnect] Socket connected:', socket.id, 'Auth token sent:', !!currentToken);
|
||||||
|
|
||||||
if (clientState.isLoggedIn && clientState.myUserId) {
|
if (clientState.isLoggedIn && clientState.myUserId) {
|
||||||
// Пытаемся восстановить состояние игры, если были залогинены
|
console.log(`[Main:SocketConnect] Client state indicates logged in as ${clientState.loggedInUsername}. Requesting game state.`);
|
||||||
|
if (authSection.style.display === 'block' || gameSetupDiv.style.display === 'block') {
|
||||||
|
// Если мы на экране логина или выбора игры, но считаем себя залогиненными,
|
||||||
|
// покажем сообщение о восстановлении.
|
||||||
|
setGameStatusMessage("Восстановление игровой сессии...");
|
||||||
|
}
|
||||||
|
// Не очищаем здесь resetGameVariables, так как gameplay.js ожидает, что clientState может содержать
|
||||||
|
// предыдущие данные, которые он перезапишет при получении gameState или gameStarted.
|
||||||
|
// Если придет gameNotFound, то там уже будет reset.
|
||||||
socket.emit('requestGameState');
|
socket.emit('requestGameState');
|
||||||
} else {
|
} else {
|
||||||
// Показываем экран логина, если не залогинены
|
console.log('[Main:SocketConnect] Client state indicates NOT logged in. Showing auth screen.');
|
||||||
showAuthScreen();
|
showAuthScreen(); // Убеждаемся, что все сброшено и показан экран логина
|
||||||
|
setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('connect_error', (err) => {
|
||||||
console.warn('[Client] Disconnected:', reason);
|
console.error('[Main:SocketConnectError] Socket connection error:', err.message, err.data ? err.data : '');
|
||||||
setGameStatusMessage(`Отключено от сервера: ${reason}. Попытка переподключения...`, true);
|
const errorMessageLower = err.message ? err.message.toLowerCase() : "";
|
||||||
// Здесь можно добавить логику для UI, показывающую состояние "отключено"
|
const isAuthError = errorMessageLower.includes('auth') || errorMessageLower.includes('token') ||
|
||||||
// disableGameControls(); // будет в gameplay
|
errorMessageLower.includes('unauthorized') || err.message === 'invalid token' ||
|
||||||
if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
|
err.message === 'no token' || (err.data && typeof err.data === 'string' && err.data.toLowerCase().includes('auth'));
|
||||||
// Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию
|
|
||||||
|
if (isAuthError) {
|
||||||
|
console.warn('[Main:SocketConnectError] Authentication error. Clearing token, resetting state, showing auth screen.');
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
clientState.isLoggedIn = false;
|
||||||
|
clientState.loggedInUsername = '';
|
||||||
|
clientState.myUserId = null;
|
||||||
|
if (socket.auth) socket.auth.token = null;
|
||||||
|
|
||||||
|
showAuthScreen(); // Это вызовет resetGameVariables и скроет модалку
|
||||||
|
setAuthMessage("Ошибка аутентификации. Пожалуйста, войдите снова.", true);
|
||||||
|
} else {
|
||||||
|
if (clientState.isLoggedIn && clientState.isInGame) {
|
||||||
|
setGameStatusMessage(`Ошибка подключения: ${err.message}. Попытка переподключения...`, true);
|
||||||
|
} else if (clientState.isLoggedIn) {
|
||||||
|
setGameStatusMessage(`Ошибка подключения к серверу: ${err.message}. Попытка переподключения...`, true);
|
||||||
|
} else {
|
||||||
|
setAuthMessage(`Ошибка подключения к серверу: ${err.message}. Попытка переподключения...`, true);
|
||||||
|
if (authSection.style.display !== 'block') {
|
||||||
|
showAuthScreen(); // Если не на экране логина, но ошибка не auth, все равно показываем его
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (turnTimerSpan) turnTimerSpan.textContent = 'Ошибка';
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
console.warn('[Main:SocketDisconnect] Disconnected from server:', reason);
|
||||||
|
// Сообщения в зависимости от текущего состояния
|
||||||
|
if (clientState.isInGame) {
|
||||||
|
setGameStatusMessage(`Потеряно соединение: ${reason}. Попытка переподключения...`, true);
|
||||||
|
} else if (clientState.isLoggedIn) {
|
||||||
|
// Уже должен быть на экране выбора игры или восстановления, setGameStatusMessage там уместно
|
||||||
|
if (gameSetupDiv.style.display === 'block') {
|
||||||
|
setGameStatusMessage(`Потеряно соединение с сервером: ${reason}. Попытка переподключения...`, true);
|
||||||
|
} else {
|
||||||
|
// Если где-то между экранами, но залогинен
|
||||||
|
setAuthMessage(`Потеряно соединение: ${reason}. Попытка переподключения...`, true); // Используем authMessage для общего случая
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAuthMessage(`Потеряно соединение с сервером: ${reason}. Попытка переподключения...`, true);
|
||||||
|
}
|
||||||
|
if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
|
||||||
|
// Не сбрасываем clientState.isLoggedIn здесь, чтобы socket.connect мог попытаться восстановить сессию
|
||||||
});
|
});
|
||||||
|
|
||||||
// Общая обработка ошибок от сервера, если они не перехвачены в модулях
|
|
||||||
socket.on('gameError', (data) => {
|
socket.on('gameError', (data) => {
|
||||||
console.error('[Client] Received gameError:', data.message);
|
console.error('[Main:SocketGameError] Received gameError from server:', data.message);
|
||||||
// Показываем ошибку пользователю
|
|
||||||
if (clientState.isInGame && window.gameUI?.addToLog) {
|
if (clientState.isInGame && window.gameUI?.addToLog) {
|
||||||
window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system');
|
window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system');
|
||||||
// Здесь можно решить, нужно ли возвращать в меню или просто показать сообщение
|
// Можно добавить setGameStatusMessage и здесь, если ошибка критическая для игры
|
||||||
} else if (clientState.isLoggedIn) {
|
} else if (clientState.isLoggedIn) { // На экране выбора игры
|
||||||
setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
|
setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
|
||||||
enableSetupButtons(); // Возвращаем активность кнопкам на экране выбора игры
|
enableSetupButtons(); // Разблокировать кнопки, если ошибка при создании/присоединении
|
||||||
} else {
|
} else { // На экране логина
|
||||||
setAuthMessage(`❌ Ошибка: ${data.message}`, true);
|
setAuthMessage(`❌ Ошибка: ${data.message}`, true);
|
||||||
if(registerForm) registerForm.querySelector('button').disabled = false;
|
if(registerForm) registerForm.querySelector('button').disabled = false;
|
||||||
if(loginForm) loginForm.querySelector('button').disabled = false;
|
if(loginForm) loginForm.querySelector('button').disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик для gameNotFound, который может прийти при реконнекте, если игры нет
|
|
||||||
socket.on('gameNotFound', (data) => {
|
socket.on('gameNotFound', (data) => {
|
||||||
console.log('[Client] Main: Game not found/ended:', data?.message);
|
console.log('[Main:SocketGameNotFound] Game not found/ended after request:', data?.message);
|
||||||
dependencies.ui.resetGameVariables(); // Сбрасываем игровые переменные
|
|
||||||
clientState.isInGame = false;
|
// Важно: gameNotFound означает, что активной игры нет.
|
||||||
// disableGameControls(); // в gameplay
|
// Сбрасываем состояние и показываем экран выбора игры, если залогинены.
|
||||||
if (window.gameUI?.showGameOver) window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } }); // Скрыть модалку
|
clientState.isInGame = false; // Явно выходим из игры
|
||||||
|
resetGameVariables(); // Полный сброс игровых переменных
|
||||||
|
explicitlyHideGameOverModal(); // Убеждаемся, что модалка скрыта
|
||||||
|
|
||||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||||
|
|
||||||
if (clientState.isLoggedIn) {
|
if (clientState.isLoggedIn && clientState.myUserId) {
|
||||||
showGameSelectionScreen(clientState.loggedInUsername);
|
showGameSelectionScreen(clientState.loggedInUsername); // Переходим на выбор игры (он вызовет resetGameVariables еще раз, но это не страшно)
|
||||||
setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.");
|
setGameStatusMessage(data?.message || "Активная игровая сессия не найдена. Выберите новую игру.");
|
||||||
} else {
|
} else {
|
||||||
|
// Если по какой-то причине мы не залогинены (например, токен истек и connect_error сбросил isLoggedIn)
|
||||||
showAuthScreen();
|
showAuthScreen();
|
||||||
setAuthMessage(data?.message || "Пожалуйста, войдите.");
|
setAuthMessage(data?.message || "Пожалуйста, войдите.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Инициализация UI
|
||||||
|
authSection.style.display = 'none';
|
||||||
|
gameSetupDiv.style.display = 'none';
|
||||||
|
gameWrapper.style.display = 'none';
|
||||||
|
userInfoDiv.style.display = 'none';
|
||||||
|
statusContainer.style.display = 'block';
|
||||||
|
|
||||||
// --- Инициализация UI ---
|
if (clientState.isLoggedIn) {
|
||||||
showAuthScreen(); // Показываем начальный экран аутентификации
|
setGameStatusMessage("Подключение и восстановление сессии..."); // Или setAuthMessage, если statusContainer не виден сразу
|
||||||
|
} else {
|
||||||
|
setAuthMessage("Подключение к серверу...");
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.connect();
|
||||||
});
|
});
|
@ -1,14 +1,15 @@
|
|||||||
// /server/auth/authService.js
|
// /server/auth/authService.js
|
||||||
const bcrypt = require('bcryptjs'); // Для хеширования паролей
|
const bcrypt = require('bcryptjs'); // Для хеширования паролей
|
||||||
const db = require('../core/db'); // Путь к вашему модулю для работы с базой данных (в папке core)
|
const jwt = require('jsonwebtoken'); // <<< ДОБАВЛЕНО
|
||||||
|
const db = require('../core/db'); // Путь к вашему модулю для работы с базой данных
|
||||||
|
|
||||||
const SALT_ROUNDS = 10; // Количество раундов для генерации соли bcrypt
|
const SALT_ROUNDS = 10; // Количество раундов для генерации соли bcrypt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Регистрирует нового пользователя.
|
* Регистрирует нового пользователя и генерирует JWT.
|
||||||
* @param {string} username - Имя пользователя.
|
* @param {string} username - Имя пользователя.
|
||||||
* @param {string} password - Пароль пользователя.
|
* @param {string} password - Пароль пользователя.
|
||||||
* @returns {Promise<object>} Объект с результатом: { success: boolean, message: string, userId?: number, username?: string }
|
* @returns {Promise<object>} Объект с результатом: { success: boolean, message: string, token?: string, userId?: number, username?: string }
|
||||||
*/
|
*/
|
||||||
async function registerUser(username, password) {
|
async function registerUser(username, password) {
|
||||||
console.log(`[AuthService DEBUG] registerUser called with username: "${username}"`);
|
console.log(`[AuthService DEBUG] registerUser called with username: "${username}"`);
|
||||||
@ -50,11 +51,21 @@ async function registerUser(username, password) {
|
|||||||
console.log(`[AuthService DEBUG] Stage C: DB insert result for "${username}":`, result);
|
console.log(`[AuthService DEBUG] Stage C: DB insert result for "${username}":`, result);
|
||||||
|
|
||||||
if (result && result.insertId) {
|
if (result && result.insertId) {
|
||||||
console.log(`[AuthService] Пользователь "${username}" успешно зарегистрирован с ID: ${result.insertId}.`);
|
const userId = result.insertId;
|
||||||
|
// Генерируем JWT токен
|
||||||
|
const tokenPayload = { userId: userId, username: username };
|
||||||
|
const token = jwt.sign(
|
||||||
|
tokenPayload,
|
||||||
|
process.env.JWT_SECRET, // Используем секрет из .env
|
||||||
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '1h' } // Используем срок из .env или по умолчанию 1 час
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[AuthService] Пользователь "${username}" успешно зарегистрирован с ID: ${userId}. Токен выдан.`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Регистрация прошла успешно!',
|
message: 'Регистрация прошла успешно! Вы вошли в систему.',
|
||||||
userId: result.insertId,
|
token: token, // <<< ВОЗВРАЩАЕМ ТОКЕН
|
||||||
|
userId: userId,
|
||||||
username: username // Возвращаем и имя пользователя
|
username: username // Возвращаем и имя пользователя
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -74,10 +85,10 @@ async function registerUser(username, password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выполняет вход пользователя.
|
* Выполняет вход пользователя и генерирует JWT.
|
||||||
* @param {string} username - Имя пользователя.
|
* @param {string} username - Имя пользователя.
|
||||||
* @param {string} password - Пароль пользователя.
|
* @param {string} password - Пароль пользователя.
|
||||||
* @returns {Promise<object>} Объект с результатом: { success: boolean, message: string, userId?: number, username?: string }
|
* @returns {Promise<object>} Объект с результатом: { success: boolean, message: string, token?: string, userId?: number, username?: string }
|
||||||
*/
|
*/
|
||||||
async function loginUser(username, password) {
|
async function loginUser(username, password) {
|
||||||
console.log(`[AuthService DEBUG] loginUser called with username: "${username}"`);
|
console.log(`[AuthService DEBUG] loginUser called with username: "${username}"`);
|
||||||
@ -104,10 +115,19 @@ async function loginUser(username, password) {
|
|||||||
console.log(`[AuthService DEBUG] Password comparison result for "${username}": ${passwordMatch}`);
|
console.log(`[AuthService DEBUG] Password comparison result for "${username}": ${passwordMatch}`);
|
||||||
|
|
||||||
if (passwordMatch) {
|
if (passwordMatch) {
|
||||||
console.log(`[AuthService] Пользователь "${user.username}" (ID: ${user.id}) успешно вошел в систему.`);
|
// Генерируем JWT токен
|
||||||
|
const tokenPayload = { userId: user.id, username: user.username };
|
||||||
|
const token = jwt.sign(
|
||||||
|
tokenPayload,
|
||||||
|
process.env.JWT_SECRET, // Используем секрет из .env
|
||||||
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '1h' } // Используем срок из .env или по умолчанию 1 час
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[AuthService] Пользователь "${user.username}" (ID: ${user.id}) успешно вошел в систему. Токен выдан.`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Вход выполнен успешно!',
|
message: 'Вход выполнен успешно!',
|
||||||
|
token: token, // <<< ВОЗВРАЩАЕМ ТОКЕН
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username // Возвращаем имя пользователя
|
username: user.username // Возвращаем имя пользователя
|
||||||
};
|
};
|
||||||
|
207
server/bc.js
207
server/bc.js
@ -1,104 +1,155 @@
|
|||||||
// /server/bc.js - Главный файл сервера Battle Club
|
// /server/bc.js - Главный файл сервера Battle Club
|
||||||
|
|
||||||
// Загружаем переменные окружения В САМОМ НАЧАЛЕ, до всех других импортов,
|
|
||||||
// которые могут зависеть от process.env
|
|
||||||
// Убедитесь, что путь к .env правильный относительно места запуска приложения.
|
|
||||||
// Если bc.js запускается из папки server/, а .env в корне, то путь должен быть '../.env'
|
|
||||||
// Но обычно dotenv ищет .env в process.cwd()
|
|
||||||
// Но обычно dotenv ищет .env в process.cwd()
|
|
||||||
|
|
||||||
//Test
|
|
||||||
|
|
||||||
require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') });
|
require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') });
|
||||||
|
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
// Импорт серверных модулей из их новых местоположений
|
// Импорт серверных модулей
|
||||||
const authService = require('./auth/authService'); // Сервис аутентификации
|
const authService = require('./auth/authService');
|
||||||
const GameManager = require('./game/GameManager'); // Менеджер игр
|
const GameManager = require('./game/GameManager');
|
||||||
const db = require('./core/db'); // Модуль базы данных (важно, чтобы он тоже использовал dotenv)
|
const db = require('./core/db');
|
||||||
const GAME_CONFIG = require('./core/config'); // Глобальный конфиг игры
|
const GAME_CONFIG = require('./core/config');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
// Настройка Socket.IO
|
// --- НАСТРОЙКА EXPRESS ---
|
||||||
const io = new Server(server, {
|
const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
||||||
cors: {
|
if (!clientOrigin && process.env.NODE_ENV !== 'development') {
|
||||||
// origin: process.env.CORS_ORIGIN || "https://pavel-chagovsky.com:3200", // Пример, если нужно CORS из .env
|
console.warn("[Server Config] CORS_ORIGIN_CLIENT не установлен для продакшн сборки. HTTP API могут быть недоступны.");
|
||||||
// methods: ["GET", "POST"]
|
}
|
||||||
},
|
|
||||||
// pingInterval: 10000,
|
|
||||||
// pingTimeout: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Раздача статических файлов из папки 'public'
|
app.use(cors({
|
||||||
|
origin: clientOrigin,
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||||
|
|
||||||
// Создаем экземпляр GameManager
|
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
||||||
const gameManager = new GameManager(io);
|
app.post('/auth/register', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
console.log(`[HTTP /auth/register] Attempt for username: "${username}"`);
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
||||||
|
}
|
||||||
|
const result = await authService.registerUser(username, password);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(201).json(result);
|
||||||
|
} else {
|
||||||
|
res.status(400).json(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/auth/login', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
console.log(`[HTTP /auth/login] Attempt for username: "${username}"`);
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
||||||
|
}
|
||||||
|
const result = await authService.loginUser(username, password);
|
||||||
|
if (result.success) {
|
||||||
|
res.json(result);
|
||||||
|
} else {
|
||||||
|
res.status(401).json(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
||||||
|
if (!socketCorsOrigin && process.env.NODE_ENV !== 'development') {
|
||||||
|
console.warn("[Server Config] CORS_ORIGIN_SOCKET не установлен для продакшн сборки. Socket.IO может быть недоступен.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: socketCorsOrigin,
|
||||||
|
methods: ["GET", "POST"]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameManager = new GameManager(io);
|
||||||
const loggedInUsers = {};
|
const loggedInUsers = {};
|
||||||
|
|
||||||
|
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
||||||
|
io.use(async (socket, next) => {
|
||||||
|
const token = socket.handshake.auth.token;
|
||||||
|
console.log(`[Socket.IO Middleware] Attempting to auth socket ${socket.id}. Token ${token ? 'present' : 'absent'}.`);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
socket.userData = { userId: decoded.userId, username: decoded.username };
|
||||||
|
console.log(`[Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`);
|
if (socket.userData && socket.userData.userId) {
|
||||||
socket.userData = null;
|
console.log(`[Socket.IO] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected: ${socket.id}`);
|
||||||
|
loggedInUsers[socket.id] = socket.userData;
|
||||||
socket.on('register', async (data) => {
|
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
||||||
console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
const result = await authService.registerUser(data?.username, data?.password);
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`);
|
|
||||||
} else {
|
|
||||||
console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`);
|
|
||||||
}
|
}
|
||||||
socket.emit('registerResponse', result);
|
} else {
|
||||||
});
|
console.log(`[Socket.IO] Unauthenticated user connected: ${socket.id}. No game state will be restored.`);
|
||||||
|
}
|
||||||
socket.on('login', async (data) => {
|
|
||||||
console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`);
|
|
||||||
const result = await authService.loginUser(data?.username, data?.password);
|
|
||||||
if (result.success && result.userId && result.username) {
|
|
||||||
console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`);
|
|
||||||
socket.userData = { userId: result.userId, username: result.username };
|
|
||||||
loggedInUsers[socket.id] = socket.userData;
|
|
||||||
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
|
||||||
gameManager.handleRequestGameState(socket, result.userId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`);
|
|
||||||
socket.userData = null;
|
|
||||||
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
|
|
||||||
}
|
|
||||||
socket.emit('loginResponse', result);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('logout', () => {
|
socket.on('logout', () => {
|
||||||
const username = socket.userData?.username || 'UnknownUser';
|
const username = socket.userData?.username || 'UnknownUser';
|
||||||
const userId = socket.userData?.userId;
|
const userId = socket.userData?.userId;
|
||||||
console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`);
|
console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`);
|
||||||
if (gameManager && typeof gameManager.handleDisconnect === 'function' && userId) {
|
// GameManager.handleDisconnect будет вызван автоматически при событии 'disconnect'
|
||||||
gameManager.handleDisconnect(socket.id, userId);
|
// Если игрок нажал "выход", но еще в игре, клиент пошлет 'playerSurrender' ДО этого.
|
||||||
}
|
// Здесь просто очищаем локальные данные сессии для сокета.
|
||||||
if (loggedInUsers[socket.id]) {
|
if (loggedInUsers[socket.id]) {
|
||||||
delete loggedInUsers[socket.id];
|
delete loggedInUsers[socket.id];
|
||||||
}
|
}
|
||||||
socket.userData = null;
|
socket.userData = null;
|
||||||
console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out.`);
|
console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out (client-side action).`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('createGame', (data) => {
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
socket.on('playerSurrender', () => {
|
||||||
const mode = data?.mode || 'ai';
|
if (!socket.userData?.userId) {
|
||||||
if (mode === 'pvp' && !socket.userData) {
|
socket.emit('gameError', { message: 'Необходимо войти в систему, чтобы сдаться в игре.' });
|
||||||
socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[Socket.IO] Create Game from ${socket.userData?.username || socket.id} (ID: ${identifier}). Mode: ${mode}, Char: ${data?.characterKey}`);
|
const identifier = socket.userData.userId;
|
||||||
|
const username = socket.userData.username;
|
||||||
|
console.log(`[Socket.IO] Player Surrender request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
|
||||||
|
if (gameManager && typeof gameManager.handlePlayerSurrender === 'function') {
|
||||||
|
gameManager.handlePlayerSurrender(identifier);
|
||||||
|
} else {
|
||||||
|
console.error("[Socket.IO] CRITICAL: gameManager or handlePlayerSurrender method not found!");
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при обработке сдачи игры.' });
|
||||||
|
}
|
||||||
|
// После этого клиент выполнит logout, что вызовет 'disconnect' и GameManager.handleDisconnect
|
||||||
|
});
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
|
socket.on('createGame', (data) => {
|
||||||
|
if (!socket.userData?.userId) {
|
||||||
|
socket.emit('gameError', { message: 'Необходимо войти в систему для создания игры.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const identifier = socket.userData.userId;
|
||||||
|
const mode = data?.mode || 'ai';
|
||||||
|
console.log(`[Socket.IO] Create Game from ${socket.userData.username} (ID: ${identifier}). Mode: ${mode}, Char: ${data?.characterKey}`);
|
||||||
gameManager.createGame(socket, mode, data?.characterKey, identifier);
|
gameManager.createGame(socket, mode, data?.characterKey, identifier);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -134,26 +185,30 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('playerAction', (actionData) => {
|
socket.on('playerAction', (actionData) => {
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
if (!socket.userData?.userId) {
|
||||||
|
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const identifier = socket.userData.userId;
|
||||||
gameManager.handlePlayerAction(identifier, actionData);
|
gameManager.handlePlayerAction(identifier, actionData);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
const identifier = socket.userData?.userId;
|
||||||
console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`);
|
const username = socket.userData?.username || 'UnauthenticatedUser';
|
||||||
gameManager.handleDisconnect(socket.id, identifier);
|
console.log(`[Socket.IO] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
||||||
|
if (identifier) {
|
||||||
|
gameManager.handleDisconnect(socket.id, identifier);
|
||||||
|
}
|
||||||
if (loggedInUsers[socket.id]) {
|
if (loggedInUsers[socket.id]) {
|
||||||
delete loggedInUsers[socket.id];
|
delete loggedInUsers[socket.id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Запуск HTTP сервера
|
|
||||||
// Используем переменные окружения или значения по умолчанию
|
|
||||||
const PORT = parseInt(process.env.BC_APP_PORT || '3200', 10);
|
const PORT = parseInt(process.env.BC_APP_PORT || '3200', 10);
|
||||||
const HOSTNAME = process.env.BC_APP_HOSTNAME || '127.0.0.1';
|
const HOSTNAME = process.env.BC_APP_HOSTNAME || '127.0.0.1';
|
||||||
|
|
||||||
// Проверка, что порт является числом
|
|
||||||
if (isNaN(PORT)) {
|
if (isNaN(PORT)) {
|
||||||
console.error(`[Server FATAL] Некорректное значение для BC_APP_PORT: "${process.env.BC_APP_PORT}". Ожидается число.`);
|
console.error(`[Server FATAL] Некорректное значение для BC_APP_PORT: "${process.env.BC_APP_PORT}". Ожидается число.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -170,11 +225,13 @@ server.listen(PORT, HOSTNAME, () => {
|
|||||||
}
|
}
|
||||||
console.log(`Environment (NODE_ENV): ${process.env.NODE_ENV || 'not set (defaults to development behavior)'}`);
|
console.log(`Environment (NODE_ENV): ${process.env.NODE_ENV || 'not set (defaults to development behavior)'}`);
|
||||||
console.log(`Serving static files from: ${path.join(__dirname, '..', 'public')}`);
|
console.log(`Serving static files from: ${path.join(__dirname, '..', 'public')}`);
|
||||||
|
console.log(`Client HTTP requests should target: http://${HOSTNAME}:${PORT}`);
|
||||||
|
console.log(`Socket.IO CORS origin set to: ${socketCorsOrigin || 'Not explicitly set, defaults may apply'}`);
|
||||||
|
console.log(`HTTP API CORS origin set to: ${clientOrigin || 'Not explicitly set, defaults may apply'}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
|
console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
// process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
|
@ -29,6 +29,7 @@ const GAME_CONFIG = {
|
|||||||
TURN_DURATION_SECONDS: 60, // Длительность хода в секундах
|
TURN_DURATION_SECONDS: 60, // Длительность хода в секундах
|
||||||
TURN_DURATION_MS: 60 * 1000, // Длительность хода в миллисекундах
|
TURN_DURATION_MS: 60 * 1000, // Длительность хода в миллисекундах
|
||||||
TIMER_UPDATE_INTERVAL_MS: 1000, // Интервал обновления таймера на клиенте (в мс)
|
TIMER_UPDATE_INTERVAL_MS: 1000, // Интервал обновления таймера на клиенте (в мс)
|
||||||
|
RECONNECT_TIMEOUT_MS: 5000,
|
||||||
|
|
||||||
// --- Идентификаторы и Типы ---
|
// --- Идентификаторы и Типы ---
|
||||||
PLAYER_ID: 'player', // Технический идентификатор для слота 'Игрок 1'
|
PLAYER_ID: 'player', // Технический идентификатор для слота 'Игрок 1'
|
||||||
|
@ -7,335 +7,594 @@ const GAME_CONFIG = require('../core/config');
|
|||||||
class GameManager {
|
class GameManager {
|
||||||
constructor(io) {
|
constructor(io) {
|
||||||
this.io = io;
|
this.io = io;
|
||||||
this.games = {};
|
this.games = {}; // Активные инстансы игр { gameId: GameInstance }
|
||||||
this.userIdentifierToGameId = {};
|
this.userIdentifierToGameId = {}; // { userId: gameId }
|
||||||
this.pendingPvPGames = [];
|
this.pendingPvPGames = []; // Массив gameId игр, ожидающих второго игрока
|
||||||
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}`);
|
||||||
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)) {
|
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||||
const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const playerInfo = Object.values(gameToRemove.players).find(p => p.identifier === identifier);
|
||||||
if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) {
|
if (playerInfo && playerInfo.id === GAME_CONFIG.PLAYER_ID) {
|
||||||
console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} (socket: ${currentSocketId}) created/joined new game. Removing their previous owned pending PvP game: ${oldPendingGameId}`);
|
||||||
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game');
|
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game_creation_or_join');
|
||||||
|
} else {
|
||||||
|
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} had pending game ${oldPendingGameId}, but was not the primary player. Not removing.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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 = 'elena', identifier) {
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey}`);
|
||||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
||||||
this.handleRequestGameState(socket, 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}`);
|
||||||
return;
|
|
||||||
|
// Если игра существует и НЕ завершена
|
||||||
|
if (!existingGame.gameState?.isGameOver) {
|
||||||
|
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
||||||
|
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
||||||
|
} else {
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||||||
|
}
|
||||||
|
this.handleRequestGameState(socket, identifier); // Попытка восстановить сессию в существующей игре
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Игра существует, но завершена. GameManager должен был ее очистить.
|
||||||
|
// Если мы здесь, значит, что-то пошло не так с очисткой.
|
||||||
|
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);
|
||||||
|
|
||||||
const gameId = uuidv4();
|
const gameId = uuidv4();
|
||||||
|
console.log(`[GameManager.createGame] Generated new GameID: ${gameId}`);
|
||||||
const game = new GameInstance(gameId, this.io, mode, this);
|
const game = new GameInstance(gameId, this.io, mode, this);
|
||||||
game.ownerIdentifier = identifier; // Устанавливаем владельца игры
|
|
||||||
this.games[gameId] = game;
|
this.games[gameId] = game;
|
||||||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||||||
|
|
||||||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameId;
|
this.userIdentifierToGameId[identifier] = gameId;
|
||||||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (выбран: ${charKeyForInstance})`);
|
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
||||||
const assignedPlayerId = game.players[socket.id]?.id;
|
const assignedPlayerId = Object.values(game.players).find(p=>p.identifier === identifier)?.id;
|
||||||
|
|
||||||
if (!assignedPlayerId) {
|
if (!assignedPlayerId) {
|
||||||
this._cleanupGame(gameId, 'player_add_failed_no_role');
|
console.error(`[GameManager.createGame] CRITICAL: Failed to assign player role for user ${identifier} in game ${gameId}.`);
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (роль).' });
|
this._cleanupGame(gameId, 'player_add_failed_no_role_assigned');
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось присвоить роль).' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||||||
|
console.log(`[GameManager.createGame] Emitted 'gameCreated' to ${identifier}. gameId: ${gameId}, yourPlayerId: ${assignedPlayerId}`);
|
||||||
|
|
||||||
if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
|
if (mode === 'ai') {
|
||||||
const isInitialized = game.initializeGame();
|
const isInitialized = game.initializeGame();
|
||||||
if (isInitialized) game.startGame();
|
if (isInitialized) {
|
||||||
else this._cleanupGame(gameId, 'initialization_failed_on_create');
|
console.log(`[GameManager.createGame] AI game ${gameId} initialized, starting game...`);
|
||||||
|
game.startGame();
|
||||||
if (game.mode === 'pvp' && game.playerCount === 2) { // Если PvP заполнилась
|
} else {
|
||||||
const idx = this.pendingPvPGames.indexOf(gameId);
|
console.error(`[GameManager.createGame] AI game ${gameId} initialization failed. Cleaning up.`);
|
||||||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
this._cleanupGame(gameId, 'initialization_failed_on_ai_create');
|
||||||
this.broadcastAvailablePvPGames();
|
}
|
||||||
|
} else if (mode === 'pvp') {
|
||||||
|
if (!this.pendingPvPGames.includes(gameId)) {
|
||||||
|
this.pendingPvPGames.push(gameId);
|
||||||
|
console.log(`[GameManager.createGame] PvP game ${gameId} added to pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||||||
}
|
}
|
||||||
} else if (mode === 'pvp' && game.playerCount === 1) {
|
|
||||||
if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId);
|
|
||||||
game.initializeGame();
|
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 {
|
||||||
this._cleanupGame(gameId, 'player_add_failed_instance');
|
console.error(`[GameManager.createGame] game.addPlayer failed for user ${identifier} in game ${gameId}. Cleaning up.`);
|
||||||
|
this._cleanupGame(gameId, 'player_add_failed_in_instance');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGame(socket, gameId, identifier) { // identifier - это userId присоединяющегося
|
joinGame(socket, gameIdToJoin, identifier) {
|
||||||
const game = this.games[gameId];
|
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join GameID: ${gameIdToJoin}`);
|
||||||
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
const game = this.games[gameIdToJoin];
|
||||||
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; }
|
if (!game) {
|
||||||
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
|
console.warn(`[GameManager.joinGame] Game ${gameIdToJoin} not found for user ${identifier}.`);
|
||||||
|
socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return;
|
||||||
// === ИЗМЕНЕНИЕ: Запрет присоединения к своей же игре ===
|
}
|
||||||
if (game.ownerIdentifier === identifier) {
|
if (game.gameState?.isGameOver) {
|
||||||
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете.' });
|
console.warn(`[GameManager.joinGame] User ${identifier} tried to join game ${gameIdToJoin} which is already over.`);
|
||||||
// Можно отправить состояние этой игры, если она действительно ожидает
|
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||||
this.handleRequestGameState(socket, identifier);
|
this._cleanupGame(gameIdToJoin, `attempt_to_join_finished_game_${identifier}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
if (game.mode !== 'pvp') {
|
||||||
|
console.warn(`[GameManager.joinGame] User ${identifier} tried to join non-PvP game ${gameIdToJoin}. Mode: ${game.mode}`);
|
||||||
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameId) {
|
socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP режим).' }); return;
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
|
|
||||||
this.handleRequestGameState(socket, identifier);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Проверка на случай, если игрок пытается присоединиться к игре, где он уже есть (хотя ownerIdentifier проверка выше это частично покрывает для создателя)
|
if (game.playerCount >= 2 && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
||||||
const existingPlayerInThisGame = Object.values(game.players).find(p => p.identifier === identifier);
|
console.warn(`[GameManager.joinGame] User ${identifier} tried to join full PvP game ${gameIdToJoin}. Players: ${game.playerCount}`);
|
||||||
if (existingPlayerInThisGame) {
|
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
|
|
||||||
this.handleRequestGameState(socket, identifier); // Отправляем состояние игры
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (game.ownerIdentifier === identifier && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
||||||
|
console.warn(`[GameManager.joinGame] User ${identifier} (owner) tried to join their own waiting game ${gameIdToJoin} as a new player.`);
|
||||||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете, как новый игрок.' });
|
||||||
|
this.handleRequestGameState(socket, identifier); return;
|
||||||
|
}
|
||||||
|
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameIdToJoin) {
|
||||||
|
const otherGame = this.games[this.userIdentifierToGameId[identifier]];
|
||||||
|
if (otherGame && !otherGame.gameState?.isGameOver) {
|
||||||
|
console.warn(`[GameManager.joinGame] User ${identifier} already in another active game: ${this.userIdentifierToGameId[identifier]}. Cannot join ${gameIdToJoin}.`);
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в другой игре. Сначала завершите или покиньте её.' });
|
||||||
|
this.handleRequestGameState(socket, identifier); return;
|
||||||
|
} 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);
|
||||||
|
|
||||||
if (game.addPlayer(socket, null, identifier)) {
|
if (game.addPlayer(socket, null, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameId;
|
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||||||
console.log(`[GameManager] Игрок ${identifier} присоединился к PvP игре ${gameId}`);
|
console.log(`[GameManager.joinGame] Player ${identifier} successfully added/reconnected to PvP game ${gameIdToJoin}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
||||||
|
if (game.playerCount === 2) {
|
||||||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full with 2 active players. Initializing and starting.`);
|
||||||
const isInitialized = game.initializeGame();
|
const isInitialized = game.initializeGame();
|
||||||
if (isInitialized) game.startGame();
|
if (isInitialized) {
|
||||||
else this._cleanupGame(gameId, 'initialization_failed_on_join');
|
game.startGame();
|
||||||
|
} else {
|
||||||
const idx = this.pendingPvPGames.indexOf(gameId);
|
console.error(`[GameManager.joinGame] PvP game ${gameIdToJoin} initialization failed after 2nd player join. Cleaning up.`);
|
||||||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
this._cleanupGame(gameIdToJoin, 'initialization_failed_on_pvp_join'); return;
|
||||||
|
}
|
||||||
|
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
|
||||||
|
if (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) {
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
console.log(`[GameManager.findAndJoinRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
||||||
this.handleRequestGameState(socket, identifier);
|
if (existingGame && !existingGame.gameState?.isGameOver) {
|
||||||
return;
|
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} already in active game: ${this.userIdentifierToGameId[identifier]}.`);
|
||||||
}
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||||
|
this.handleRequestGameState(socket, identifier); return;
|
||||||
let gameIdToJoin = null;
|
} else if (existingGame && existingGame.gameState?.isGameOver) {
|
||||||
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} mapped to finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up.`);
|
||||||
|
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_find_random_${identifier}`);
|
||||||
// Ищем игру, созданную НЕ текущим пользователем
|
|
||||||
for (const id of [...this.pendingPvPGames]) {
|
|
||||||
const pendingGame = this.games[id];
|
|
||||||
// === ИЗМЕНЕНИЕ: Убеждаемся, что не присоединяемся к игре, которую сами создали и ожидаем ===
|
|
||||||
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
|
||||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
|
||||||
const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
|
||||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
|
||||||
gameIdToJoin = id; break;
|
|
||||||
}
|
|
||||||
if (!gameIdToJoin) gameIdToJoin = id; // Берем первую подходящую, если нет с нужным персонажем
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
|
|
||||||
|
let gameIdToJoin = null;
|
||||||
|
console.log(`[GameManager.findAndJoinRandomPvPGame] Searching pending games for ${identifier}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||||||
|
for (const id of [...this.pendingPvPGames]) { // Iterate over a copy in case of modification
|
||||||
|
const pendingGame = this.games[id];
|
||||||
|
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
||||||
|
if (!pendingGame.gameState || !pendingGame.gameState.isGameOver) {
|
||||||
|
gameIdToJoin = id;
|
||||||
|
console.log(`[GameManager.findAndJoinRandomPvPGame] Found suitable pending game: ${gameIdToJoin} for user ${identifier}.`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
console.log(`[GameManager.findAndJoinRandomPvPGame] Pending game ${id} is already over. Skipping and cleaning.`);
|
||||||
|
this._cleanupGame(id, `stale_finished_pending_game_during_find_random`); // Clean up stale finished game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (gameIdToJoin) {
|
if (gameIdToJoin) {
|
||||||
this.joinGame(socket, gameIdToJoin, identifier);
|
this.joinGame(socket, gameIdToJoin, identifier);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`[GameManager.findAndJoinRandomPvPGame] No suitable pending game found for ${identifier}. Creating a new PvP game.`);
|
||||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||||||
// Сообщение о создании новой игры отправляется из createGame/initializeGame/startGame
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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}`);
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (game) {
|
if (game) {
|
||||||
|
if (game.gameState?.isGameOver) {
|
||||||
|
console.warn(`[GameManager.handlePlayerAction] User ${identifier} in game ${gameId} attempted action, but game is ALREADY OVER. Action ignored.`);
|
||||||
|
game.playerSockets[identifier]?.socket.emit('gameError', {message: "Действие невозможно: игра уже завершена."});
|
||||||
|
// Potentially send gameNotFound or re-send gameOver if client missed it
|
||||||
|
this.handleRequestGameState(game.playerSockets[identifier]?.socket || this._findClientSocketByIdentifier(identifier), identifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
const currentSocketId = playerInfo?.socket?.id;
|
if (playerInfo && playerInfo.socket && playerInfo.socket.connected && !playerInfo.isTemporarilyDisconnected) {
|
||||||
if (playerInfo && currentSocketId) {
|
console.log(`[GameManager.handlePlayerAction] Forwarding action from user ${identifier} (Socket: ${playerInfo.socket.id}) to game ${gameId}.`);
|
||||||
const actualSocket = this.io.sockets.sockets.get(currentSocketId);
|
game.processPlayerAction(playerInfo.socket.id, actionData);
|
||||||
if (actualSocket?.connected) game.processPlayerAction(currentSocketId, actionData);
|
} else if (playerInfo && playerInfo.isTemporarilyDisconnected) {
|
||||||
else console.warn(`[GameManager] Игрок ${identifier}: действие, но сокет ${currentSocketId} отключен.`);
|
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 {
|
} else {
|
||||||
console.warn(`[GameManager] Игрок ${identifier}: действие для игры ${gameId}, но не найден в game.players.`);
|
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];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
const s = this.io.sockets.sockets.get(identifier) || playerInfo?.socket;
|
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||||
if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока).' });
|
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока при действии).' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager] Игрок ${identifier}: действие, но игра ${gameId} не найдена.`);
|
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 s = this.io.sockets.sockets.get(identifier);
|
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||||
if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена.' });
|
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена (игра отсутствует).' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect(socketId, identifier) {
|
handlePlayerSurrender(identifier) {
|
||||||
const gameId = this.userIdentifierToGameId[identifier];
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
|
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) {
|
||||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} tried to surrender in game ${gameId} which is ALREADY OVER. Ignoring.`);
|
||||||
|
// _cleanupGame should have already run or will run.
|
||||||
if (playerInfo) {
|
// Ensure map is clear if it's somehow stale.
|
||||||
// Проверяем, действительно ли отключается АКТУАЛЬНЫЙ сокет этого игрока
|
if (this.userIdentifierToGameId[identifier] === gameId) {
|
||||||
if (playerInfo.socket.id === socketId) {
|
console.warn(`[GameManager.handlePlayerSurrender] Stale map entry for ${identifier} to finished game ${gameId}. Cleaning.`);
|
||||||
console.log(`[GameManager] Актуальный сокет ${socketId} игрока ${identifier} отключился из игры ${gameId}.`);
|
delete this.userIdentifierToGameId[identifier]; // Direct cleanup if game is confirmed over.
|
||||||
const dPlayerRole = playerInfo.id;
|
|
||||||
const dCharKey = playerInfo.chosenCharacterKey;
|
|
||||||
|
|
||||||
game.removePlayer(socketId); // Удаляем именно этот сокет из игры
|
|
||||||
|
|
||||||
if (game.playerCount === 0) {
|
|
||||||
this._cleanupGame(gameId, 'all_players_disconnected');
|
|
||||||
} else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
|
||||||
game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey);
|
|
||||||
} else if (game.mode === 'ai' && game.playerCount === 0 && game.gameState && !game.gameState.isGameOver) {
|
|
||||||
game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey); // Завершаем AI игру, если игрок ушел
|
|
||||||
}
|
|
||||||
// Если игра уже была isGameOver, _cleanupGame был вызван ранее.
|
|
||||||
// userIdentifierToGameId[identifier] для отключившегося игрока УДАЛЯЕТСЯ здесь,
|
|
||||||
// чтобы он мог начать новую игру или переподключиться.
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
} else {
|
|
||||||
// Отключился старый сокет (socketId), но у игрока (identifier) уже новый активный сокет.
|
|
||||||
// Ничего не делаем с игрой, так как игрок по-прежнему в ней с новым сокетом.
|
|
||||||
// Просто логируем, что старый сокет отвалился.
|
|
||||||
console.log(`[GameManager] Отключился старый сокет ${socketId} для игрока ${identifier}, который уже переподключился с сокетом ${playerInfo.socket.id} в игре ${gameId}.`);
|
|
||||||
// Связь userIdentifierToGameId[identifier] остается, так как он все еще в игре.
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof game.playerDidSurrender === 'function') {
|
||||||
|
console.log(`[GameManager.handlePlayerSurrender] Forwarding surrender from user ${identifier} to game ${gameId}.`);
|
||||||
|
game.playerDidSurrender(identifier); // This method will call _cleanupGame internally
|
||||||
} else {
|
} else {
|
||||||
// Игрока с таким identifier нет в этой игре.
|
console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} is missing playerDidSurrender method! Attempting fallback cleanup for PvP.`);
|
||||||
// Это может случиться, если игра была очищена до того, как пришло событие disconnect.
|
if (game.mode === 'pvp' && game.gameState && !game.gameState.isGameOver) {
|
||||||
// console.log(`[GameManager] Отключившийся сокет ${socketId} (identifier: ${identifier}) не найден в активных игроках игры ${gameId} (возможно, игра уже очищена).`);
|
const surrenderedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
delete this.userIdentifierToGameId[identifier]; // На всякий случай.
|
if (surrenderedPlayerInfo) {
|
||||||
|
const opponentInfo = Object.values(game.players).find(p => p.identifier !== identifier && !p.isTemporarilyDisconnected);
|
||||||
|
const winnerRole = opponentInfo ? opponentInfo.id : null;
|
||||||
|
if (typeof game.endGameDueToDisconnect === 'function') {
|
||||||
|
game.endGameDueToDisconnect(surrenderedPlayerInfo.id, surrenderedPlayerInfo.chosenCharacterKey, "opponent_surrendered_fallback", winnerRole);
|
||||||
|
} else {
|
||||||
|
this._cleanupGame(gameId, "surrender_fallback_cleanup_missing_end_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 {
|
} else {
|
||||||
// console.log(`[GameManager] Отключился сокет ${socketId} (identifier: ${identifier}). Активная игра не найдена по идентификатору.`);
|
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.`);
|
||||||
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) {
|
||||||
|
for (const sid of this.io.sockets.sockets.keys()) {
|
||||||
|
const s = this.io.sockets.sockets.get(sid);
|
||||||
|
if (s && s.userData && s.userData.userId === identifier && s.connected) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(socketId, identifier) {
|
||||||
|
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||||
|
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
|
||||||
|
|
||||||
|
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
if (game.gameState && game.gameState.isGameOver) {
|
||||||
|
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket: ${socketId}) is ALREADY OVER. Disconnect processing skipped for game logic.`);
|
||||||
|
// _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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerInfo = Object.values(game.players).find(p => p.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'}`);
|
||||||
|
|
||||||
|
if (playerInfo && playerInfo.socket && playerInfo.socket.id === socketId && !playerInfo.isTemporarilyDisconnected) {
|
||||||
|
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active socket for user ${identifier} (Role: ${playerInfo.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
||||||
|
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||||||
|
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
|
||||||
|
} else {
|
||||||
|
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} is missing handlePlayerPotentiallyLeft method!`);
|
||||||
|
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_in_instance_on_disconnect");
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
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 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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupGame(gameId, reason = 'unknown') {
|
_cleanupGame(gameId, reason = 'unknown') {
|
||||||
|
console.log(`[GameManager._cleanupGame] Attempting to cleanup GameID: ${gameId}, Reason: ${reason}`);
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (!game) return false;
|
if (!game) {
|
||||||
console.log(`[GameManager] Очистка игры ${gameId} (Причина: ${reason}).`);
|
console.warn(`[GameManager._cleanupGame] Game ${gameId} not found in this.games. Checking pending list and user map.`);
|
||||||
|
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
if (pendingIdx > -1) {
|
||||||
|
this.pendingPvPGames.splice(pendingIdx, 1);
|
||||||
Object.values(game.players).forEach(pInfo => {
|
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pending list (instance was already gone). Reason: ${reason}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||||||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
this.broadcastAvailablePvPGames();
|
||||||
delete this.userIdentifierToGameId[pInfo.identifier];
|
|
||||||
}
|
}
|
||||||
});
|
// Ensure any lingering user map entries for this non-existent game are cleared.
|
||||||
if(game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId){
|
let mapCleaned = false;
|
||||||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
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}.`);
|
||||||
|
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||||||
|
if (typeof game.clearAllReconnectTimers === 'function') {
|
||||||
|
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) {
|
||||||
|
console.warn(`[GameManager._cleanupGame] Game ${gameId} was not marked as 'isGameOver' during cleanup. Marking now. Reason: ${reason}`);
|
||||||
|
game.gameState.isGameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all players of this game from the global userIdentifierToGameId map
|
||||||
|
let playersInGameCleaned = 0;
|
||||||
|
Object.values(game.players).forEach(pInfo => {
|
||||||
|
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];
|
||||||
|
playersInGameCleaned++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 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 (!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Only if not already cleaned
|
||||||
|
console.log(`[GameManager._cleanupGame] Deleting mapping for owner ${game.ownerIdentifier} from game ${gameId} (was not in game.players or already cleaned).`);
|
||||||
|
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) this.pendingPvPGames.splice(pendingIdx, 1);
|
if (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];
|
||||||
this.broadcastAvailablePvPGames();
|
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||||||
|
this.broadcastAvailablePvPGames(); // Update list for all clients
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailablePvPGamesListForClient() {
|
getAvailablePvPGamesListForClient() {
|
||||||
return this.pendingPvPGames.map(gameId => {
|
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Generating list from pending: ${this.pendingPvPGames.join(', ')}`);
|
||||||
const game = this.games[gameId];
|
return this.pendingPvPGames
|
||||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
.map(gameId => {
|
||||||
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const game = this.games[gameId];
|
||||||
let p1Username = 'Игрок';
|
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||||
let p1CharName = '';
|
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
let ownerId = game.ownerIdentifier; // === ИЗМЕНЕНИЕ: Получаем ownerId ===
|
let p1Username = 'Игрок', p1CharName = 'Неизвестный';
|
||||||
|
const ownerId = game.ownerIdentifier;
|
||||||
|
|
||||||
if (p1Info) {
|
if (p1Info && p1Info.socket && p1Info.socket.userData) { // Check for userData
|
||||||
p1Username = p1Info.socket?.userData?.username || `User#${String(p1Info.identifier).substring(0,4)}`;
|
p1Username = p1Info.socket.userData.username || `User#${String(p1Info.identifier).substring(0,4)}`;
|
||||||
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
|
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
|
||||||
p1CharName = charData?.name || p1Info.chosenCharacterKey;
|
p1CharName = charData?.name || p1Info.chosenCharacterKey || 'Не выбран';
|
||||||
|
} else if (ownerId){
|
||||||
|
// 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);
|
||||||
|
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 charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||||||
|
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 {
|
return null;
|
||||||
id: gameId,
|
})
|
||||||
status: `Ожидает (Создал: ${p1Username} за ${p1CharName})`,
|
.filter(info => info !== null);
|
||||||
ownerIdentifier: ownerId // === ИЗМЕНЕНИЕ: Отправляем ownerIdentifier клиенту ===
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).filter(info => info !== null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastAvailablePvPGames() {
|
broadcastAvailablePvPGames() {
|
||||||
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
|
const list = this.getAvailablePvPGamesListForClient();
|
||||||
|
// console.log(`[GameManager.broadcastAvailablePvPGames] Broadcasting list of ${list.length} games.`);
|
||||||
|
this.io.emit('availablePvPGamesList', list);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequestGameState(socket, identifier) {
|
handleRequestGameState(socket, identifier) {
|
||||||
const gameId = this.userIdentifierToGameId[identifier];
|
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||||
const game = gameId ? this.games[gameId] : null;
|
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (New Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
|
||||||
|
|
||||||
|
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 playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
|
const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
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 (playerInfoInGameInstance) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
delete this.userIdentifierToGameId[identifier];
|
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 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;
|
||||||
}
|
}
|
||||||
console.log(`[GameManager] Восстановление игры ${gameId} для ${identifier}. Новый сокет ${socket.id}.`);
|
|
||||||
const oldSocketId = playerInfoInGameInstance.socket?.id; // Добавил ?. на случай если сокета нет
|
console.log(`[GameManager.handleRequestGameState] Restoring game ${gameIdFromMap} for user ${identifier}. NewSocket: ${socket.id}. OldSocketInGame: ${playerInfoInGameInstance.socket?.id}. Player role: ${playerInfoInGameInstance.id}`);
|
||||||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
|
||||||
delete game.players[oldSocketId];
|
if (typeof game.handlePlayerReconnected === 'function') {
|
||||||
if(game.playerSockets[playerInfoInGameInstance.id]?.id === oldSocketId) {
|
const reconnected = game.handlePlayerReconnected(playerInfoInGameInstance.id, socket);
|
||||||
delete game.playerSockets[playerInfoInGameInstance.id];
|
console.log(`[GameManager.handleRequestGameState] Called game.handlePlayerReconnected for role ${playerInfoInGameInstance.id}. Result: ${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 {
|
||||||
|
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} is missing handlePlayerReconnected method! Attempting fallback socket update.`);
|
||||||
|
const oldSocketId = playerInfoInGameInstance.socket?.id;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playerInfoInGameInstance.socket = socket;
|
|
||||||
game.players[socket.id] = playerInfoInGameInstance;
|
// If handlePlayerReconnected returned false but game is not over, we might still need to send state.
|
||||||
game.playerSockets[playerInfoInGameInstance.id] = socket;
|
// 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);
|
socket.join(game.id);
|
||||||
|
console.log(`[GameManager.handleRequestGameState] New socket ${socket.id} for user ${identifier} joined room ${game.id}.`);
|
||||||
|
|
||||||
const pCharKey = playerInfoInGameInstance.chosenCharacterKey;
|
const pCharKey = playerInfoInGameInstance.chosenCharacterKey;
|
||||||
const pData = dataUtils.getCharacterData(pCharKey);
|
const pData = dataUtils.getCharacterData(pCharKey);
|
||||||
const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
const oCharKey = game.gameState?.[opponentRole]?.characterKey || (playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey);
|
const oCharKeyFromGameState = game.gameState?.[opponentRole]?.characterKey;
|
||||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; // oData может быть null, если оппонента нет
|
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}`);
|
||||||
|
|
||||||
if (pData && (oData || !game.opponentCharacterKey) && game.gameState) {
|
|
||||||
socket.emit('gameStarted', {
|
socket.emit('gameStarted', {
|
||||||
gameId: game.id, yourPlayerId: playerInfoInGameInstance.id, initialGameState: game.gameState,
|
gameId: game.id,
|
||||||
|
yourPlayerId: playerInfoInGameInstance.id,
|
||||||
|
initialGameState: gameStateToSend,
|
||||||
playerBaseStats: pData.baseStats,
|
playerBaseStats: pData.baseStats,
|
||||||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1}, // Заглушка если оппонента нет
|
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||||
playerAbilities: pData.abilities,
|
playerAbilities: pData.abilities,
|
||||||
opponentAbilities: oData?.abilities || [],
|
opponentAbilities: oData?.abilities || [],
|
||||||
log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
log: logBufferToSend,
|
||||||
|
clientConfig: { ...GAME_CONFIG }
|
||||||
});
|
});
|
||||||
if(game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier) socket.emit('waitingForOpponent');
|
|
||||||
if (!game.gameState.isGameOver && game.turnTimer?.start) {
|
if (game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier && !game.gameState.isGameOver) {
|
||||||
game.turnTimer.start(game.gameState.isPlayerTurn, (game.mode === 'ai' && !game.gameState.isPlayerTurn));
|
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 {
|
} else {
|
||||||
this._handleGameRecoveryError(socket, gameId, identifier, 'data_load_fail_reconnect');
|
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 {
|
||||||
this._handleGameRecoveryError(socket, gameId, identifier, 'player_not_in_instance_reconnect');
|
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_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]) {
|
||||||
|
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] Ошибка восстановления игры ${gameId} для ${identifier} (причина: ${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: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||||
this._cleanupGame(gameId, `recovery_error_${reasonCode}`);
|
if (gameId) {
|
||||||
|
// Attempt to cleanup the problematic game
|
||||||
|
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
||||||
|
} else if (this.userIdentifierToGameId[identifier]) {
|
||||||
|
// If gameId was null, but user was still mapped, cleanup the mapped game
|
||||||
|
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}.`);
|
||||||
|
this._cleanupGame(problematicGameId, `recovery_error_null_gameid_for_${identifier}_reason_${reasonCode}`);
|
||||||
|
}
|
||||||
|
// This will also clear userIdentifierToGameId[identifier] if _cleanupGame didn't.
|
||||||
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
|
|
||||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
// /server/game/instance/GameInstance.js
|
// /server/game/instance/GameInstance.js
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const TurnTimer = require('./TurnTimer');
|
const TurnTimer = require('./TurnTimer');
|
||||||
const gameLogic = require('../logic'); // Импортирует index.js из папки logic
|
const gameLogic = require('../logic');
|
||||||
const dataUtils = require('../../data/dataUtils');
|
const dataUtils = require('../../data/dataUtils');
|
||||||
const GAME_CONFIG = require('../../core/config'); // <--- УБЕДИТЕСЬ, ЧТО GAME_CONFIG ИМПОРТИРОВАН
|
const GAME_CONFIG = require('../../core/config');
|
||||||
|
|
||||||
class GameInstance {
|
class GameInstance {
|
||||||
constructor(gameId, io, mode = 'ai', gameManager) {
|
constructor(gameId, io, mode = 'ai', gameManager) {
|
||||||
@ -20,13 +20,16 @@ class GameInstance {
|
|||||||
this.opponentCharacterKey = null;
|
this.opponentCharacterKey = null;
|
||||||
this.ownerIdentifier = null;
|
this.ownerIdentifier = null;
|
||||||
this.gameManager = gameManager;
|
this.gameManager = gameManager;
|
||||||
|
this.reconnectTimers = {};
|
||||||
|
|
||||||
this.turnTimer = new TurnTimer(
|
this.turnTimer = new TurnTimer(
|
||||||
GAME_CONFIG.TURN_DURATION_MS,
|
GAME_CONFIG.TURN_DURATION_MS,
|
||||||
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
||||||
() => this.handleTurnTimeout(),
|
() => this.handleTurnTimeout(),
|
||||||
(remainingTime, isPlayerTurnForTimer) => {
|
(remainingTime, isPlayerTurnForTimer) => {
|
||||||
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer });
|
if (!this.isGameEffectivelyPaused()) {
|
||||||
|
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -37,15 +40,23 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||||||
if (this.players[socket.id]) {
|
console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
|
||||||
socket.emit('gameError', { message: 'Ваш сокет уже зарегистрирован в этой игре.' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
||||||
if (existingPlayerByIdentifier) {
|
if (existingPlayerByIdentifier) {
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре под другим подключением.' });
|
console.warn(`[GameInstance ${this.id}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`);
|
||||||
|
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||||||
|
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||||
|
}
|
||||||
|
// Если игра уже завершена, и игрок пытается "добавиться" (что маловероятно, если GameManager.handleRequestGameState работает корректно),
|
||||||
|
// то addPlayer не должен успешно завершаться.
|
||||||
|
if (this.gameState && this.gameState.isGameOver) {
|
||||||
|
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.playerCount >= 2) {
|
if (this.playerCount >= 2) {
|
||||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||||||
return false;
|
return false;
|
||||||
@ -63,84 +74,309 @@ class GameInstance {
|
|||||||
actualCharacterKey = 'elena';
|
actualCharacterKey = 'elena';
|
||||||
this.ownerIdentifier = identifier;
|
this.ownerIdentifier = identifier;
|
||||||
} else { // PvP
|
} else { // PvP
|
||||||
if (this.playerCount === 0) {
|
if (!Object.values(this.players).some(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected)) {
|
||||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||||
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
|
actualCharacterKey = (chosenCharacterKey === 'almagest' || chosenCharacterKey === 'balard') ? chosenCharacterKey : 'elena';
|
||||||
this.ownerIdentifier = identifier;
|
this.ownerIdentifier = identifier;
|
||||||
} else {
|
} else if (!Object.values(this.players).some(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected)) {
|
||||||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||||||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
actualCharacterKey = (firstPlayerInfo?.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
|
if (firstPlayerInfo?.chosenCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
||||||
|
else if (firstPlayerInfo?.chosenCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
||||||
|
else actualCharacterKey = 'balard'; // Default if first player is Balard or something else
|
||||||
|
} else {
|
||||||
|
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId);
|
||||||
|
if (oldPlayerSocketIdForRole) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Role ${assignedPlayerId} was previously occupied by socket ${oldPlayerSocketIdForRole}. Removing old entry.`);
|
||||||
|
delete this.players[oldPlayerSocketIdForRole];
|
||||||
|
}
|
||||||
|
|
||||||
this.players[socket.id] = {
|
this.players[socket.id] = {
|
||||||
id: assignedPlayerId, socket: socket,
|
id: assignedPlayerId,
|
||||||
chosenCharacterKey: actualCharacterKey, identifier: identifier
|
socket: socket,
|
||||||
|
chosenCharacterKey: actualCharacterKey,
|
||||||
|
identifier: identifier,
|
||||||
|
isTemporarilyDisconnected: false
|
||||||
};
|
};
|
||||||
this.playerSockets[assignedPlayerId] = socket;
|
this.playerSockets[assignedPlayerId] = socket;
|
||||||
this.playerCount++;
|
this.playerCount++;
|
||||||
socket.join(this.id);
|
socket.join(this.id);
|
||||||
|
|
||||||
const characterBaseStats = dataUtils.getCharacterBaseStats(actualCharacterKey);
|
const characterBaseStats = dataUtils.getCharacterBaseStats(actualCharacterKey);
|
||||||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterBaseStats?.name || 'N/A'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Игроков: ${this.playerCount}.`);
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterBaseStats?.name || 'N/A'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Активных игроков: ${this.playerCount}.`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removePlayer(socketId) {
|
removePlayer(socketId, reason = "unknown_reason_for_removal") {
|
||||||
const playerInfo = this.players[socketId];
|
const playerInfo = this.players[socketId];
|
||||||
if (playerInfo) {
|
if (playerInfo) {
|
||||||
const playerRole = playerInfo.id;
|
const playerRole = playerInfo.id;
|
||||||
console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}) покинул игру.`);
|
console.log(`[GameInstance ${this.id}] Окончательное удаление игрока ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}). Причина: ${reason}.`);
|
||||||
if (playerInfo.socket) { try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } }
|
if (playerInfo.socket) { try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } }
|
||||||
|
|
||||||
|
if (!playerInfo.isTemporarilyDisconnected) { // Только если он не был уже помечен как "временно отключен"
|
||||||
|
this.playerCount--;
|
||||||
|
}
|
||||||
|
|
||||||
delete this.players[socketId];
|
delete this.players[socketId];
|
||||||
this.playerCount--;
|
|
||||||
if (this.playerSockets[playerRole]?.id === socketId) {
|
if (this.playerSockets[playerRole]?.id === socketId) {
|
||||||
delete this.playerSockets[playerRole];
|
delete this.playerSockets[playerRole];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} удален. Активных игроков: ${this.playerCount}.`);
|
||||||
|
|
||||||
|
// Завершаем игру, если она была активна и стала неиграбельной
|
||||||
if (this.gameState && !this.gameState.isGameOver) {
|
if (this.gameState && !this.gameState.isGameOver) {
|
||||||
const isTurnOfDisconnected = (this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.PLAYER_ID) ||
|
const isTurnOfDisconnected = (this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.PLAYER_ID) ||
|
||||||
(!this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.OPPONENT_ID);
|
(!this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.OPPONENT_ID);
|
||||||
if (isTurnOfDisconnected) this.turnTimer.clear();
|
if (isTurnOfDisconnected) this.turnTimer.clear();
|
||||||
|
|
||||||
|
if (this.mode === 'ai' && this.playerCount === 0) {
|
||||||
|
console.log(`[GameInstance ${this.id}] AI игра стала пустой после удаления игрока. Завершение игры.`);
|
||||||
|
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_empty_ai_game");
|
||||||
|
} else if (this.mode === 'pvp' && this.playerCount < 2) {
|
||||||
|
// Убедимся, что остался хотя бы один игрок, чтобы ему присудить победу.
|
||||||
|
// Если playerCount стал 0, то победителя нет.
|
||||||
|
const remainingPlayer = Object.values(this.players).find(p => !p.isTemporarilyDisconnected);
|
||||||
|
const winnerRoleIfAny = remainingPlayer ? remainingPlayer.id : null;
|
||||||
|
|
||||||
|
console.log(`[GameInstance ${this.id}] PvP игра стала неполной (${this.playerCount} игроков) после удаления игрока ${playerInfo.identifier} (роль ${playerRole}).`);
|
||||||
|
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", winnerRoleIfAny);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка удалить игрока по socketId ${socketId}, но он не найден.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeGame() {
|
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||||
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Игроков: ${this.playerCount}.`);
|
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft CALLED for role ${playerIdRole}, identifier ${identifier}, charKey ${characterKey}`);
|
||||||
if (this.mode === 'ai' && this.playerCount === 1) {
|
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||||
this.playerCharacterKey = 'elena'; this.opponentCharacterKey = 'balard';
|
|
||||||
} else if (this.mode === 'pvp' && this.playerCount === 2) {
|
if (!playerEntry || !playerEntry.socket) {
|
||||||
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
console.warn(`[GameInstance ${this.id}] Не найден активный игрок ${identifier} (роль: ${playerIdRole}) для пометки как отключенного.`);
|
||||||
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
|
return;
|
||||||
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
|
}
|
||||||
} else if (this.mode === 'pvp' && this.playerCount === 1) {
|
if (this.gameState && this.gameState.isGameOver) {
|
||||||
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
console.log(`[GameInstance ${this.id}] Игра уже завершена, игнорируем 'potentiallyLeft' для ${identifier}.`);
|
||||||
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
|
return;
|
||||||
this.opponentCharacterKey = null;
|
}
|
||||||
} else {
|
if (playerEntry.isTemporarilyDisconnected) {
|
||||||
console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации!`); return false;
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) уже помечен как временно отключенный. Таймер должен быть активен.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = dataUtils.getCharacterData(this.playerCharacterKey);
|
playerEntry.isTemporarilyDisconnected = true;
|
||||||
let opponentData = null;
|
this.playerCount--; // Уменьшаем счетчик АКТИВНЫХ игроков
|
||||||
const isOpponentDefined = !!this.opponentCharacterKey;
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) помечен как временно отключенный. Активных игроков: ${this.playerCount}. Запуск таймера реконнекта.`);
|
||||||
if (isOpponentDefined) opponentData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
|
||||||
|
|
||||||
if (!playerData || (isOpponentDefined && !opponentData)) {
|
const disconnectedPlayerName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (роль ${playerIdRole})`;
|
||||||
this._handleCriticalError('init_char_data_fail', 'Ошибка загрузки данных персонажей при инициализации.');
|
const disconnectLogMessage = `🔌 Игрок ${disconnectedPlayerName} отключился. Ожидание переподключения...`;
|
||||||
|
this.addToLog(disconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
|
this.io.to(this.id).emit('opponentDisconnected', {
|
||||||
|
disconnectedPlayerId: playerIdRole,
|
||||||
|
disconnectedCharacterName: disconnectedPlayerName,
|
||||||
|
});
|
||||||
|
this.broadcastLogUpdate(); // Отправляем лог об отключении
|
||||||
|
|
||||||
|
const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||||
|
if (currentTurnPlayerRole === playerIdRole) {
|
||||||
|
this.turnTimer.clear();
|
||||||
|
console.log(`[GameInstance ${this.id}] Ход был за отключившимся игроком ${playerIdRole}, таймер хода остановлен.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearReconnectTimer(playerIdRole);
|
||||||
|
this.reconnectTimers[playerIdRole] = setTimeout(() => {
|
||||||
|
console.log(`[GameInstance ${this.id}] Таймер реконнекта для игрока ${identifier} (роль: ${playerIdRole}) истек.`);
|
||||||
|
delete this.reconnectTimers[playerIdRole];
|
||||||
|
const stillDisconnectedPlayerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier && p.isTemporarilyDisconnected);
|
||||||
|
if (stillDisconnectedPlayerEntry) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) не переподключился. Удаляем окончательно.`);
|
||||||
|
this.removePlayer(stillDisconnectedPlayerEntry.socket.id, "reconnect_timeout");
|
||||||
|
} else {
|
||||||
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) уже переподключился или был удален ранее. Таймер истек без действия.`);
|
||||||
|
}
|
||||||
|
}, GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||||
|
const identifier = newSocket.userData?.userId;
|
||||||
|
console.log(`[GameInstance ${this.id}] handlePlayerReconnected CALLED for role ${playerIdRole}, identifier ${identifier}, newSocket ${newSocket.id}`);
|
||||||
|
|
||||||
|
if (this.gameState && this.gameState.isGameOver) {
|
||||||
|
console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) пытается переподключиться к уже ЗАВЕРШЕННОЙ игре. Отправка gameError.`);
|
||||||
|
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию: игра уже завершена.' });
|
||||||
|
// GameManager.handleRequestGameState должен был это перехватить, но на всякий случай.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isOpponentDefined && (!opponentData.baseStats.maxHp || opponentData.baseStats.maxHp <= 0)) {
|
|
||||||
this._handleCriticalError('init_opponent_hp_fail', 'Некорректные HP оппонента при инициализации.');
|
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||||
|
|
||||||
|
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
|
||||||
|
this.clearReconnectTimer(playerIdRole);
|
||||||
|
const oldSocketId = playerEntry.socket.id;
|
||||||
|
|
||||||
|
if (this.players[oldSocketId]) {
|
||||||
|
delete this.players[oldSocketId];
|
||||||
|
}
|
||||||
|
|
||||||
|
playerEntry.socket = newSocket;
|
||||||
|
playerEntry.isTemporarilyDisconnected = false;
|
||||||
|
this.players[newSocket.id] = playerEntry;
|
||||||
|
|
||||||
|
this.playerSockets[playerIdRole] = newSocket;
|
||||||
|
this.playerCount++; // Восстанавливаем счетчик активных
|
||||||
|
|
||||||
|
newSocket.join(this.id);
|
||||||
|
const reconnectedPlayerName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey || `Игрок (роль ${playerIdRole})`;
|
||||||
|
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (${reconnectedPlayerName}) успешно переподключен с новым сокетом ${newSocket.id}. Старый сокет: ${oldSocketId}. Активных игроков: ${this.playerCount}.`);
|
||||||
|
|
||||||
|
const reconnectLogMessage = `🔌 Игрок ${reconnectedPlayerName} снова в игре!`;
|
||||||
|
this.addToLog(reconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
|
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||||
|
const opponentRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
|
const oCharKey = this.gameState?.[opponentRoleKey]?.characterKey;
|
||||||
|
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||||
|
|
||||||
|
const logForReconnectedPlayer = this.consumeLogBuffer();
|
||||||
|
newSocket.emit('gameStarted', { // Используем 'gameStarted' для восстановления, как ожидает клиент
|
||||||
|
gameId: this.id,
|
||||||
|
yourPlayerId: playerIdRole,
|
||||||
|
initialGameState: this.gameState,
|
||||||
|
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: logForReconnectedPlayer,
|
||||||
|
clientConfig: { ...GAME_CONFIG }
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherPlayerSocket = Object.values(this.players).find(p =>
|
||||||
|
p.id !== playerIdRole &&
|
||||||
|
p.socket && p.socket.connected &&
|
||||||
|
!p.isTemporarilyDisconnected
|
||||||
|
)?.socket;
|
||||||
|
|
||||||
|
if (otherPlayerSocket) {
|
||||||
|
otherPlayerSocket.emit('playerReconnected', {
|
||||||
|
playerId: playerIdRole,
|
||||||
|
playerName: reconnectedPlayerName
|
||||||
|
});
|
||||||
|
// Логи, которые могли накопиться для другого игрока (например, сообщение о реконнекте), уйдут со следующим broadcastGameStateUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Игра возобновлена после переподключения ${identifier} (роль: ${playerIdRole}). Отправка gameStateUpdate всем и перезапуск таймера.`);
|
||||||
|
this.broadcastGameStateUpdate(); // Отправит оставшиеся логи
|
||||||
|
|
||||||
|
const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||||
|
const otherPlayerEntry = Object.values(this.players).find(p => p.id !== playerIdRole); // Проверяем другого игрока в целом
|
||||||
|
|
||||||
|
// Таймер запускаем, если сейчас ход реконнектнувшегося ИЛИ если другой игрок активен (не isTemporarilyDisconnected)
|
||||||
|
if (currentTurnPlayerRole === playerIdRole || (otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected)) {
|
||||||
|
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||||||
|
} else {
|
||||||
|
console.log(`[GameInstance ${this.id}] Другой игрок (${otherPlayerEntry?.id}) отключен, таймер хода не запускается после реконнекта ${playerIdRole}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
|
||||||
|
console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) пытается переподключиться, но не был помечен как отключенный.`);
|
||||||
|
if (playerEntry.socket.id !== newSocket.id) {
|
||||||
|
newSocket.emit('gameError', {message: "Вы уже активно подключены к этой игре."});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.warn(`[GameInstance ${this.id}] Не удалось найти игрока ${identifier} (роль: ${playerIdRole}) для переподключения, или он не был помечен как отключенный.`);
|
||||||
|
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию в этой игре.'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearReconnectTimer(playerIdRole) {
|
||||||
|
if (this.reconnectTimers[playerIdRole]) {
|
||||||
|
clearTimeout(this.reconnectTimers[playerIdRole]);
|
||||||
|
delete this.reconnectTimers[playerIdRole];
|
||||||
|
console.log(`[GameInstance ${this.id}] Таймер реконнекта для роли ${playerIdRole} очищен.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllReconnectTimers() {
|
||||||
|
console.log(`[GameInstance ${this.id}] Очистка ВСЕХ таймеров реконнекта.`);
|
||||||
|
for (const roleId in this.reconnectTimers) {
|
||||||
|
this.clearReconnectTimer(roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isGameEffectivelyPaused() {
|
||||||
|
if (this.mode === 'pvp') {
|
||||||
|
// Игра на паузе, если хотя бы один из ДВУХ ожидаемых игроков временно отключен
|
||||||
|
const player1 = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
|
const player2 = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||||
|
return (player1?.isTemporarilyDisconnected || false) || (player2?.isTemporarilyDisconnected || false);
|
||||||
|
} else if (this.mode === 'ai') {
|
||||||
|
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
|
return humanPlayer?.isTemporarilyDisconnected ?? (this.playerCount === 0 && Object.keys(this.players).length > 0);
|
||||||
|
}
|
||||||
|
return false; // По умолчанию не на паузе
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeGame() {
|
||||||
|
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков: ${this.playerCount}. Всего записей в players: ${Object.keys(this.players).length}`);
|
||||||
|
if (this.mode === 'ai' && this.playerCount === 1) {
|
||||||
|
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
|
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
|
||||||
|
this.opponentCharacterKey = 'balard';
|
||||||
|
} else if (this.mode === 'pvp') {
|
||||||
|
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
|
const p2Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||||
|
|
||||||
|
if (p1Info) this.playerCharacterKey = p1Info.chosenCharacterKey;
|
||||||
|
else this.playerCharacterKey = null;
|
||||||
|
|
||||||
|
if (p2Info) this.opponentCharacterKey = p2Info.chosenCharacterKey;
|
||||||
|
else this.opponentCharacterKey = null;
|
||||||
|
|
||||||
|
if (this.playerCount === 1 && p1Info && !p2Info) {
|
||||||
|
// this.opponentCharacterKey остается null
|
||||||
|
} else if (this.playerCount === 2 && (!p1Info || !p2Info)) {
|
||||||
|
console.error(`[GameInstance ${this.id}] Ошибка инициализации PvP: playerCount=2, но один из игроков не найден как активный.`);
|
||||||
|
return false;
|
||||||
|
} else if (this.playerCount < 2 && !p1Info) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Инициализация PvP без активного первого игрока. playerCharacterKey будет ${this.playerCharacterKey}.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации! Активных: ${this.playerCount}`); return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
|
||||||
|
let opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
|
||||||
|
const isOpponentDefined = !!this.opponentCharacterKey;
|
||||||
|
|
||||||
|
if (!playerData && (this.mode === 'ai' || (this.mode === 'pvp' && this.playerCount > 0))) {
|
||||||
|
this._handleCriticalError('init_player_data_fail', 'Ошибка загрузки данных основного игрока при инициализации.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isOpponentDefined && !opponentData) {
|
||||||
|
this._handleCriticalError('init_opponent_data_fail', 'Ошибка загрузки данных оппонента при инициализации.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gameState = {
|
this.gameState = {
|
||||||
player: this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities),
|
player: playerData ?
|
||||||
opponent: isOpponentDefined ?
|
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities) :
|
||||||
|
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []),
|
||||||
|
opponent: isOpponentDefined && opponentData ?
|
||||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
|
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
|
||||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []), // Плейсхолдер
|
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока 2...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []),
|
||||||
isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true,
|
isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true,
|
||||||
isGameOver: false, turnNumber: 1, gameMode: this.mode
|
isGameOver: false, turnNumber: 1, gameMode: this.mode
|
||||||
};
|
};
|
||||||
@ -149,14 +385,14 @@ class GameInstance {
|
|||||||
this.logBuffer = [];
|
this.logBuffer = [];
|
||||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
const pCharKey = this.gameState.player.characterKey;
|
const pCharKey = this.gameState.player.characterKey;
|
||||||
const oCharKey = this.gameState.opponent.characterKey; // Нужен ключ оппонента для контекста
|
const oCharKey = this.gameState.opponent.characterKey;
|
||||||
if ((pCharKey === 'elena' || pCharKey === 'almagest') && oCharKey) {
|
if ((pCharKey === 'elena' || pCharKey === 'almagest') && oCharKey) {
|
||||||
const opponentFullDataForTaunt = dataUtils.getCharacterData(oCharKey); // Получаем полные данные оппонента
|
const opponentFullDataForTaunt = dataUtils.getCharacterData(oCharKey);
|
||||||
const startTaunt = gameLogic.getRandomTaunt(pCharKey, 'battleStart', {}, GAME_CONFIG, opponentFullDataForTaunt, this.gameState);
|
const startTaunt = gameLogic.getRandomTaunt(pCharKey, 'battleStart', {}, GAME_CONFIG, opponentFullDataForTaunt, this.gameState);
|
||||||
if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Готовность к старту: ${isOpponentDefined}`);
|
console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Готовность к старту (isOpponentDefined): ${isOpponentDefined}`);
|
||||||
return isOpponentDefined;
|
return isOpponentDefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +404,7 @@ class GameInstance {
|
|||||||
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
||||||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
||||||
};
|
};
|
||||||
(abilities || []).forEach(ability => { // Добавлена проверка abilities
|
(abilities || []).forEach(ability => {
|
||||||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||||||
fighterState.abilityCooldowns[ability.id] = 0;
|
fighterState.abilityCooldowns[ability.id] = 0;
|
||||||
}
|
}
|
||||||
@ -181,8 +417,12 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startGame() {
|
startGame() {
|
||||||
|
if (this.isGameEffectivelyPaused()) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка старта игры, но она на паузе из-за дисконнекта. Старт отложен.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.gameState || !this.gameState.opponent?.characterKey) {
|
if (!this.gameState || !this.gameState.opponent?.characterKey) {
|
||||||
this._handleCriticalError('start_game_not_ready', 'Попытка старта не полностью готовой игры.');
|
this._handleCriticalError('start_game_not_ready', 'Попытка старта не полностью готовой игры (нет оппонента).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[GameInstance ${this.id}] Запуск игры.`);
|
console.log(`[GameInstance ${this.id}] Запуск игры.`);
|
||||||
@ -191,14 +431,17 @@ class GameInstance {
|
|||||||
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
||||||
if (!pData || !oData) { this._handleCriticalError('start_char_data_fail', 'Ошибка данных персонажей при старте.'); return; }
|
if (!pData || !oData) { this._handleCriticalError('start_char_data_fail', 'Ошибка данных персонажей при старте.'); return; }
|
||||||
|
|
||||||
|
const initialLog = this.consumeLogBuffer();
|
||||||
|
|
||||||
Object.values(this.players).forEach(playerInfo => {
|
Object.values(this.players).forEach(playerInfo => {
|
||||||
if (playerInfo.socket?.connected) {
|
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
|
||||||
const dataForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
|
const dataForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
|
||||||
{ playerBaseStats: pData.baseStats, opponentBaseStats: oData.baseStats, playerAbilities: pData.abilities, opponentAbilities: oData.abilities } :
|
{ playerBaseStats: pData.baseStats, opponentBaseStats: oData.baseStats, playerAbilities: pData.abilities, opponentAbilities: oData.abilities } :
|
||||||
{ playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities };
|
{ playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities };
|
||||||
playerInfo.socket.emit('gameStarted', {
|
playerInfo.socket.emit('gameStarted', {
|
||||||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||||||
...dataForClient, log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
...dataForClient, log: [...initialLog],
|
||||||
|
clientConfig: { ...GAME_CONFIG }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -214,6 +457,12 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processPlayerAction(requestingSocketId, actionData) {
|
processPlayerAction(requestingSocketId, actionData) {
|
||||||
|
if (this.isGameEffectivelyPaused()) {
|
||||||
|
console.warn(`[GameInstance ${this.id}] Действие от сокета ${requestingSocketId}, но игра на паузе. Действие отклонено.`);
|
||||||
|
const playerInfo = this.players[requestingSocketId];
|
||||||
|
if (playerInfo?.socket) playerInfo.socket.emit('gameError', {message: "Действие невозможно: другой игрок отключен."});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.gameState || this.gameState.isGameOver) return;
|
if (!this.gameState || this.gameState.isGameOver) return;
|
||||||
const actingPlayerInfo = this.players[requestingSocketId];
|
const actingPlayerInfo = this.players[requestingSocketId];
|
||||||
if (!actingPlayerInfo) { console.error(`[GameInstance ${this.id}] Действие от неизвестного сокета ${requestingSocketId}`); return; }
|
if (!actingPlayerInfo) { console.error(`[GameInstance ${this.id}] Действие от неизвестного сокета ${requestingSocketId}`); return; }
|
||||||
@ -234,7 +483,7 @@ class GameInstance {
|
|||||||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail', 'Ошибка данных персонажа при действии.'); return; }
|
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail', 'Ошибка данных персонажа при действии.'); return; }
|
||||||
|
|
||||||
let actionValid = true;
|
let actionValid = true;
|
||||||
let tauntContextTargetData = defenderData; // Данные цели для контекста насмешек
|
let tauntContextTargetData = defenderData;
|
||||||
|
|
||||||
if (actionData.actionType === 'attack') {
|
if (actionData.actionType === 'attack') {
|
||||||
const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, tauntContextTargetData, this.gameState);
|
const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, tauntContextTargetData, this.gameState);
|
||||||
@ -253,7 +502,7 @@ class GameInstance {
|
|||||||
if (!ability) {
|
if (!ability) {
|
||||||
actionValid = false;
|
actionValid = false;
|
||||||
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
|
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
|
||||||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); // Перезапуск таймера
|
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
|
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
|
||||||
@ -275,17 +524,22 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.checkGameOver()) {
|
if (this.checkGameOver()) {
|
||||||
this.broadcastGameStateUpdate(); return;
|
return;
|
||||||
}
|
}
|
||||||
if (actionValid) {
|
if (actionValid) {
|
||||||
|
this.broadcastLogUpdate();
|
||||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||||
} else {
|
} else {
|
||||||
this.broadcastLogUpdate();
|
this.broadcastLogUpdate();
|
||||||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); // Перезапуск таймера
|
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchTurn() {
|
switchTurn() {
|
||||||
|
if (this.isGameEffectivelyPaused()) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка сменить ход, но игра на паузе. Смена хода отложена.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.gameState || this.gameState.isGameOver) return;
|
if (!this.gameState || this.gameState.isGameOver) return;
|
||||||
this.turnTimer.clear();
|
this.turnTimer.clear();
|
||||||
|
|
||||||
@ -302,7 +556,9 @@ class GameInstance {
|
|||||||
if (endingTurnActor.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActor);
|
if (endingTurnActor.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActor);
|
||||||
if (endingTurnActor.disabledAbilities?.length > 0 && endingTurnData.abilities) gameLogic.processDisabledAbilities(endingTurnActor.disabledAbilities, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG);
|
if (endingTurnActor.disabledAbilities?.length > 0 && endingTurnData.abilities) gameLogic.processDisabledAbilities(endingTurnActor.disabledAbilities, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG);
|
||||||
|
|
||||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
|
if (this.checkGameOver()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
|
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
|
||||||
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
|
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
|
||||||
@ -318,11 +574,16 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processAiTurn() {
|
processAiTurn() {
|
||||||
|
if (this.isGameEffectivelyPaused()) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка хода AI, но игра на паузе. Ход AI отложен.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
|
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
|
||||||
if (this.gameState && !this.gameState.isGameOver) this.switchTurn();
|
if (this.gameState && !this.gameState.isGameOver) this.switchTurn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.turnTimer.clear();
|
||||||
const attacker = this.gameState.opponent;
|
const attacker = this.gameState.opponent;
|
||||||
const defender = this.gameState.player;
|
const defender = this.gameState.player;
|
||||||
const attackerData = dataUtils.getCharacterData('balard');
|
const attackerData = dataUtils.getCharacterData('balard');
|
||||||
@ -333,7 +594,8 @@ class GameInstance {
|
|||||||
if (gameLogic.isCharacterFullySilenced(attacker, GAME_CONFIG)) {
|
if (gameLogic.isCharacterFullySilenced(attacker, GAME_CONFIG)) {
|
||||||
this.addToLog(`😵 ${attacker.name} под действием Безмолвия! Атакует в смятении.`, GAME_CONFIG.LOG_TYPE_EFFECT);
|
this.addToLog(`😵 ${attacker.name} под действием Безмолвия! Атакует в смятении.`, GAME_CONFIG.LOG_TYPE_EFFECT);
|
||||||
gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, defenderData);
|
gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, defenderData);
|
||||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
|
if (this.checkGameOver()) { return; }
|
||||||
|
this.broadcastLogUpdate();
|
||||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -347,27 +609,34 @@ class GameInstance {
|
|||||||
attacker.currentResource = Math.round(attacker.currentResource - aiDecision.ability.cost);
|
attacker.currentResource = Math.round(attacker.currentResource - aiDecision.ability.cost);
|
||||||
gameLogic.applyAbilityEffect(aiDecision.ability, attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
gameLogic.applyAbilityEffect(aiDecision.ability, attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
||||||
gameLogic.setAbilityCooldown(aiDecision.ability, attacker, GAME_CONFIG);
|
gameLogic.setAbilityCooldown(aiDecision.ability, attacker, GAME_CONFIG);
|
||||||
} // 'pass' уже залогирован в decideAiAction
|
}
|
||||||
|
|
||||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
|
if (this.checkGameOver()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.broadcastLogUpdate();
|
||||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkGameOver() {
|
checkGameOver() {
|
||||||
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
|
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
|
||||||
if (!this.gameState.player || !this.gameState.opponent?.characterKey) return false;
|
if (this.mode === 'pvp' && (!this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.mode === 'ai' && !this.gameState.player?.characterKey) return false;
|
||||||
|
|
||||||
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
|
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
|
||||||
if (gameOverResult.isOver) {
|
if (gameOverResult.isOver) {
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
this.turnTimer.clear();
|
this.turnTimer.clear();
|
||||||
|
this.clearAllReconnectTimers();
|
||||||
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
const winnerState = this.gameState[gameOverResult.winnerRole];
|
const winnerState = this.gameState[gameOverResult.winnerRole];
|
||||||
const loserState = this.gameState[gameOverResult.loserRole];
|
const loserState = this.gameState[gameOverResult.loserRole];
|
||||||
if (winnerState && (winnerState.characterKey === 'elena' || winnerState.characterKey === 'almagest') && loserState) {
|
if (winnerState && (winnerState.characterKey === 'elena' || winnerState.characterKey === 'almagest') && loserState) {
|
||||||
const loserFullData = dataUtils.getCharacterData(loserState.characterKey);
|
const loserFullData = dataUtils.getCharacterData(loserState.characterKey);
|
||||||
if (loserFullData) { // Убедимся, что данные проигравшего есть
|
if (loserFullData) {
|
||||||
const taunt = gameLogic.getRandomTaunt(winnerState.characterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, loserFullData, this.gameState);
|
const taunt = gameLogic.getRandomTaunt(winnerState.characterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, loserFullData, this.gameState);
|
||||||
if (taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
if (taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
@ -390,46 +659,117 @@ class GameInstance {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
endGameDueToDisconnect(disconnectedSocketId, disconnectedPlayerRole, disconnectedCharacterKey) {
|
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
|
||||||
if (this.gameState && !this.gameState.isGameOver) {
|
if (this.gameState && !this.gameState.isGameOver) {
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
this.turnTimer.clear();
|
this.turnTimer.clear();
|
||||||
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'opponent_disconnected',
|
const actualWinnerRole = winnerIfAny !== null ? winnerIfAny :
|
||||||
disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID, // winner
|
(disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
|
||||||
disconnectedPlayerRole // loser
|
|
||||||
|
const winnerExists = Object.values(this.players).some(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected) ||
|
||||||
|
(this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID && this.gameState.opponent?.characterKey);
|
||||||
|
|
||||||
|
const result = gameLogic.getGameOverResult(
|
||||||
|
this.gameState, GAME_CONFIG, this.mode, reason,
|
||||||
|
winnerExists ? actualWinnerRole : null,
|
||||||
|
disconnectedPlayerRole
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта. Победитель: ${result.winnerRole || 'Нет'}. Отключился: ${disconnectedPlayerRole}.`);
|
console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта/ухода. Причина: ${reason}. Победитель: ${result.winnerRole || 'Нет'}. Отключился/ушел: ${disconnectedPlayerRole}.`);
|
||||||
this.io.to(this.id).emit('gameOver', {
|
this.io.to(this.id).emit('gameOver', {
|
||||||
winnerId: result.winnerRole, reason: result.reason,
|
winnerId: result.winnerRole, reason: result.reason,
|
||||||
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
||||||
loserCharacterKey: disconnectedCharacterKey // Ключ того, кто отключился
|
loserCharacterKey: disconnectedCharacterKey
|
||||||
});
|
});
|
||||||
this.gameManager._cleanupGame(this.id, result.reason);
|
this.gameManager._cleanupGame(this.id, result.reason);
|
||||||
|
} else if (this.gameState && this.gameState.isGameOver) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка завершить игру из-за дисконнекта, но она уже завершена.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||||||
|
playerDidSurrender(surrenderingPlayerIdentifier) {
|
||||||
|
console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`);
|
||||||
|
|
||||||
|
if (!this.gameState || this.gameState.isGameOver) {
|
||||||
|
console.warn(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} попытался сдаться, но игра неактивна или уже завершена.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode !== 'pvp') {
|
||||||
|
console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} сдался в не-PvP игре. Просто завершаем игру, если это AI режим и игрок один.`);
|
||||||
|
if (this.mode === 'ai') {
|
||||||
|
const playerInfo = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||||||
|
if (playerInfo) {
|
||||||
|
this.endGameDueToDisconnect(playerInfo.id, playerInfo.chosenCharacterKey, "player_left_ai_game");
|
||||||
|
} else {
|
||||||
|
this.gameManager._cleanupGame(this.id, "surrender_ai_player_not_found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||||||
|
if (!surrenderedPlayerEntry) {
|
||||||
|
console.error(`[GameInstance ${this.id}] Не найден игрок с identifier ${surrenderingPlayerIdentifier} для обработки сдачи.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const surrenderedPlayerRole = surrenderedPlayerEntry.id;
|
||||||
|
const surrenderedPlayerName = this.gameState[surrenderedPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey || `Игрок (ID: ${surrenderingPlayerIdentifier})`;
|
||||||
|
const surrenderedPlayerCharKey = this.gameState[surrenderedPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey;
|
||||||
|
|
||||||
|
const winnerRole = surrenderedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
|
const winnerName = this.gameState[winnerRole]?.name || `Оппонент (Роль: ${winnerRole})`;
|
||||||
|
|
||||||
|
this.gameState.isGameOver = true;
|
||||||
|
this.turnTimer.clear();
|
||||||
|
this.clearAllReconnectTimers(); // Также очищаем таймеры реконнекта
|
||||||
|
|
||||||
|
const surrenderMessage = `🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`;
|
||||||
|
this.addToLog(surrenderMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
console.log(`[GameInstance ${this.id}] ${surrenderMessage}`);
|
||||||
|
|
||||||
|
const reasonForGameOver = "player_surrendered";
|
||||||
|
|
||||||
|
console.log(`[GameInstance ${this.id}] Игра ${this.id} завершена из-за сдачи игрока ${surrenderedPlayerName} (роль: ${surrenderedPlayerRole}). Победитель: ${winnerName} (роль: ${winnerRole}).`);
|
||||||
|
this.io.to(this.id).emit('gameOver', {
|
||||||
|
winnerId: winnerRole,
|
||||||
|
reason: reasonForGameOver,
|
||||||
|
finalGameState: this.gameState,
|
||||||
|
log: this.consumeLogBuffer(),
|
||||||
|
loserCharacterKey: surrenderedPlayerCharKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Вызываем cleanup в GameManager, чтобы удалить игру из активных списков
|
||||||
|
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||||||
|
this.gameManager._cleanupGame(this.id, reasonForGameOver);
|
||||||
|
} else {
|
||||||
|
console.error(`[GameInstance ${this.id}] CRITICAL: gameManager or _cleanupGame method not found after surrender!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
handleTurnTimeout() {
|
handleTurnTimeout() {
|
||||||
if (!this.gameState || this.gameState.isGameOver) return;
|
if (!this.gameState || this.gameState.isGameOver) return;
|
||||||
// this.turnTimer.clear(); // TurnTimer сам себя очистит при вызове onTimeout
|
|
||||||
|
|
||||||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||||
const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
|
|
||||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerPlayerRole, timedOutPlayerRole);
|
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerPlayerRole, timedOutPlayerRole);
|
||||||
|
|
||||||
if (!this.gameState[winnerPlayerRole]?.characterKey) { // Если победитель не определен (например, ожидание в PvP)
|
if (!this.gameState[winnerPlayerRole]?.characterKey) {
|
||||||
this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`);
|
this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.gameState.isGameOver = true;
|
||||||
this.gameState.isGameOver = true; // Устанавливаем здесь, т.к. getGameOverResult мог не знать, что игра уже окончена
|
this.clearAllReconnectTimers();
|
||||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
console.log(`[GameInstance ${this.id}] Таймаут хода для ${this.gameState[timedOutPlayerRole]?.name}. Победитель: ${this.gameState[winnerPlayerRole]?.name}.`);
|
console.log(`[GameInstance ${this.id}] Таймаут хода для ${this.gameState[timedOutPlayerRole]?.name}. Победитель: ${this.gameState[winnerPlayerRole]?.name}.`);
|
||||||
|
|
||||||
this.io.to(this.id).emit('gameOver', {
|
this.io.to(this.id).emit('gameOver', {
|
||||||
winnerId: result.winnerRole, reason: result.reason,
|
winnerId: result.winnerRole, reason: result.reason,
|
||||||
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
||||||
@ -442,6 +782,7 @@ class GameInstance {
|
|||||||
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`);
|
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`);
|
||||||
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
||||||
this.turnTimer.clear();
|
this.turnTimer.clear();
|
||||||
|
this.clearAllReconnectTimers();
|
||||||
this.addToLog(`Критическая ошибка сервера: ${logMessage}`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(`Критическая ошибка сервера: ${logMessage}`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
this.io.to(this.id).emit('gameOver', {
|
this.io.to(this.id).emit('gameOver', {
|
||||||
winnerId: null, reason: `server_error_${reasonCode}`,
|
winnerId: null, reason: `server_error_${reasonCode}`,
|
||||||
@ -455,18 +796,27 @@ class GameInstance {
|
|||||||
|
|
||||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
const logEntry = { message, type, timestamp: Date.now() };
|
||||||
|
this.logBuffer.push(logEntry);
|
||||||
}
|
}
|
||||||
consumeLogBuffer() {
|
consumeLogBuffer() {
|
||||||
const logs = [...this.logBuffer]; this.logBuffer = []; return logs;
|
const logs = [...this.logBuffer];
|
||||||
|
this.logBuffer = [];
|
||||||
|
return logs;
|
||||||
}
|
}
|
||||||
broadcastGameStateUpdate() {
|
broadcastGameStateUpdate() {
|
||||||
|
if (this.isGameEffectivelyPaused()) {
|
||||||
|
console.log(`[GameInstance ${this.id}] Попытка broadcastGameStateUpdate, но игра на паузе. Обновление не отправлено.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.gameState) return;
|
if (!this.gameState) return;
|
||||||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
const logsToSend = this.consumeLogBuffer();
|
||||||
|
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: logsToSend });
|
||||||
}
|
}
|
||||||
broadcastLogUpdate() {
|
broadcastLogUpdate() {
|
||||||
if (this.logBuffer.length > 0) {
|
if (this.logBuffer.length > 0) {
|
||||||
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
|
const logsToSend = this.consumeLogBuffer();
|
||||||
|
this.io.to(this.id).emit('logUpdate', { log: logsToSend });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user