Compare commits
2 Commits
52d4309774
...
c9e2567dcd
Author | SHA1 | Date | |
---|---|---|---|
c9e2567dcd | |||
24484df514 |
@ -130,6 +130,11 @@
|
||||
|
||||
<section id="controls-panel" class="controls-panel-new">
|
||||
<h3 id="turn-indicator">Ход: Игрок 1</h3>
|
||||
<!-- === ИЗМЕНЕНИЕ: Добавлен блок для таймера === -->
|
||||
<div id="turn-timer-container" class="turn-timer-display">
|
||||
<i class="fas fa-hourglass-half"></i> Время на ход: <span id="turn-timer">--</span>
|
||||
</div>
|
||||
<!-- === КОНЕЦ ИЗМЕНЕНИЯ === -->
|
||||
<div class="controls-layout">
|
||||
<div class="control-group basic-actions">
|
||||
<button id="button-attack" class="action-button basic" data-action="BASIC_ATTACK" title="Базовая атака"><i class="fas fa-shoe-prints"></i> Атака</button>
|
||||
@ -206,7 +211,6 @@
|
||||
<div id="game-over-screen" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<h2 id="result-message">Победа!</h2>
|
||||
<!-- ИЗМЕНЕНА КНОПКА ЗДЕСЬ: добавлен class="modal-action-button" -->
|
||||
<button id="return-to-menu-button" class="modal-action-button">
|
||||
<i class="fas fa-arrow-left"></i> В меню выбора игры
|
||||
</button>
|
||||
|
@ -7,17 +7,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- Состояние клиента ---
|
||||
let currentGameState = null;
|
||||
let myPlayerId = null; // Технический ID слота, который занимает ЭТОТ клиент ('player' или 'opponent')
|
||||
let myPlayerId = null;
|
||||
let myCharacterKey = null;
|
||||
let opponentCharacterKey = null;
|
||||
let currentGameId = null;
|
||||
let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент (приходят от сервера как data.playerBaseStats)
|
||||
let opponentBaseStatsServer = null; // Статы персонажа-оппонента этого клиента (приходят от сервера как data.opponentBaseStats)
|
||||
let playerBaseStatsServer = null;
|
||||
let opponentBaseStatsServer = null;
|
||||
let playerAbilitiesServer = null;
|
||||
let opponentAbilitiesServer = null;
|
||||
let isLoggedIn = false;
|
||||
let loggedInUsername = '';
|
||||
let isInGame = false; // ФЛАГ СОСТОЯНИЯ ИГРЫ
|
||||
let isInGame = false;
|
||||
|
||||
// --- DOM Элементы ---
|
||||
// Аутентификация
|
||||
@ -25,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const registerForm = document.getElementById('register-form');
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const authMessage = document.getElementById('auth-message');
|
||||
const statusContainer = document.getElementById('status-container'); // Добавим ссылку на контейнер
|
||||
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');
|
||||
@ -34,7 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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 joinPvPGameButton = document.getElementById('join-pvP-game'); // Опечатка в ID, должно быть 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');
|
||||
@ -48,6 +48,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const gameOverScreen = document.getElementById('game-over-screen');
|
||||
const abilitiesGrid = document.getElementById('abilities-grid');
|
||||
|
||||
// === ИЗМЕНЕНИЕ: DOM элемент для таймера ===
|
||||
const turnTimerSpan = document.getElementById('turn-timer'); // Элемент для отображения времени
|
||||
const turnTimerContainer = document.getElementById('turn-timer-container'); // Контейнер таймера для управления видимостью
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
|
||||
console.log('Client.js DOMContentLoaded. Initializing elements...');
|
||||
|
||||
@ -60,12 +64,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (gameSetupDiv) gameSetupDiv.style.display = 'none';
|
||||
if (gameWrapper) gameWrapper.style.display = 'none';
|
||||
hideGameOverModal();
|
||||
// setGameStatusMessage("Войдите или зарегистрируйтесь для начала игры."); // Это сообщение перенесено в setAuthMessage/начальный статус
|
||||
setAuthMessage("Ожидание подключения к серверу..."); // Начальный статус
|
||||
if (statusContainer) statusContainer.style.display = 'block'; // Убедимся, что статус виден
|
||||
setAuthMessage("Ожидание подключения к серверу...");
|
||||
if (statusContainer) statusContainer.style.display = 'block';
|
||||
isInGame = false;
|
||||
disableGameControls();
|
||||
resetGameVariables(); // Сбрасываем переменные игры при выходе на экран логина
|
||||
resetGameVariables();
|
||||
// === ИЗМЕНЕНИЕ: Скрываем таймер при выходе на экран аутентификации ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
}
|
||||
|
||||
function showGameSelectionScreen(username) {
|
||||
@ -73,13 +80,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (authSection) authSection.style.display = 'none';
|
||||
if (userInfoDiv) {
|
||||
userInfoDiv.style.display = 'block';
|
||||
if(loggedInUsernameSpan) loggedInUsernameSpan.textContent = username;
|
||||
if (loggedInUsernameSpan) loggedInUsernameSpan.textContent = username;
|
||||
}
|
||||
if (gameSetupDiv) gameSetupDiv.style.display = 'block';
|
||||
if (gameWrapper) gameWrapper.style.display = 'none';
|
||||
hideGameOverModal();
|
||||
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
|
||||
if (statusContainer) statusContainer.style.display = 'block'; // Убедимся, что статус виден
|
||||
if (statusContainer) statusContainer.style.display = 'block';
|
||||
socket.emit('requestPvPGameList');
|
||||
updateAvailableGamesList([]);
|
||||
if (gameIdInput) gameIdInput.value = '';
|
||||
@ -87,23 +94,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (elenaRadio) elenaRadio.checked = true;
|
||||
isInGame = false;
|
||||
disableGameControls();
|
||||
resetGameVariables(); // Сбрасываем переменные игры при выходе на экран выбора игры
|
||||
resetGameVariables();
|
||||
// === ИЗМЕНЕНИЕ: Скрываем таймер при выходе на экран выбора игры ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
}
|
||||
|
||||
function showGameScreen() {
|
||||
console.log('[UI] Showing Game Screen');
|
||||
hideGameOverModal();
|
||||
if (authSection) authSection.style.display = 'none';
|
||||
if (userInfoDiv) userInfoDiv.style.display = 'block';
|
||||
if (userInfoDiv) userInfoDiv.style.display = 'block'; // Оставляем видимым, чтобы видеть "Привет, username"
|
||||
if (gameSetupDiv) gameSetupDiv.style.display = 'none';
|
||||
if (gameWrapper) gameWrapper.style.display = 'flex';
|
||||
setGameStatusMessage(""); // Очищаем статус игры, т.к. теперь есть индикатор хода
|
||||
if (statusContainer) statusContainer.style.display = 'none'; // Скрываем статус контейнер в игре
|
||||
setGameStatusMessage("");
|
||||
if (statusContainer) statusContainer.style.display = 'none';
|
||||
isInGame = true;
|
||||
disableGameControls(); // Отключаем кнопки изначально, updateUI их включит при ходе
|
||||
disableGameControls();
|
||||
// === ИЗМЕНЕНИЕ: Показываем контейнер таймера, когда игра начинается ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'block';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Начальное значение
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
}
|
||||
|
||||
// <--- НОВАЯ ФУНКЦИЯ ДЛЯ СБРОСА ИГРОВЫХ ПЕРЕМЕННЫХ ---
|
||||
function resetGameVariables() {
|
||||
currentGameId = null;
|
||||
currentGameState = null;
|
||||
@ -114,14 +128,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
opponentBaseStatsServer = null;
|
||||
playerAbilitiesServer = null;
|
||||
opponentAbilitiesServer = null;
|
||||
|
||||
window.gameState = null;
|
||||
window.gameData = null;
|
||||
window.myPlayerId = null;
|
||||
// window.GAME_CONFIG = null; // Не сбрасываем, т.к. содержит общие вещи
|
||||
}
|
||||
// --- КОНЕЦ НОВОЙ ФУНКЦИИ ---
|
||||
|
||||
|
||||
function hideGameOverModal() {
|
||||
const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden';
|
||||
@ -135,7 +145,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.gameUI?.uiElements?.opponent?.panel) {
|
||||
const opponentPanel = window.gameUI.uiElements.opponent.panel;
|
||||
if (opponentPanel.classList.contains('dissolving')) {
|
||||
console.log('[Client.js DEBUG] Removing .dissolving from opponent panel during hideGameOverModal.');
|
||||
opponentPanel.classList.remove('dissolving');
|
||||
opponentPanel.style.opacity = '1';
|
||||
opponentPanel.style.transform = 'scale(1) translateY(0)';
|
||||
@ -150,10 +159,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
authMessage.className = isError ? 'error' : 'success';
|
||||
authMessage.style.display = message ? 'block' : 'none';
|
||||
}
|
||||
// Скрываем gameStatusMessage, если показываем authMessage
|
||||
if (message && gameStatusMessage) {
|
||||
gameStatusMessage.style.display = 'none';
|
||||
}
|
||||
if (message && gameStatusMessage) gameStatusMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
function setGameStatusMessage(message, isError = false) {
|
||||
@ -161,22 +167,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
gameStatusMessage.textContent = message;
|
||||
gameStatusMessage.style.display = message ? 'block' : 'none';
|
||||
gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
|
||||
if (statusContainer) statusContainer.style.display = message ? 'block' : 'none'; // Показываем контейнер статуса
|
||||
}
|
||||
// Скрываем authMessage, если показываем gameStatusMessage
|
||||
if (message && authMessage) {
|
||||
authMessage.style.display = 'none';
|
||||
if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
|
||||
}
|
||||
if (message && authMessage) authMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
function getSelectedCharacterKey() {
|
||||
let selectedKey = 'elena';
|
||||
if (pvpCharacterRadios) {
|
||||
pvpCharacterRadios.forEach(radio => {
|
||||
if (radio.checked) {
|
||||
selectedKey = radio.value;
|
||||
}
|
||||
});
|
||||
pvpCharacterRadios.forEach(radio => { if (radio.checked) selectedKey = radio.value; });
|
||||
}
|
||||
return selectedKey;
|
||||
}
|
||||
@ -185,9 +184,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (attackButton) attackButton.disabled = !enableAttack;
|
||||
if (abilitiesGrid) {
|
||||
const abilityButtonClass = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
|
||||
abilitiesGrid.querySelectorAll(`.${abilityButtonClass}`).forEach(button => {
|
||||
button.disabled = !enableAbilities;
|
||||
});
|
||||
abilitiesGrid.querySelectorAll(`.${abilityButtonClass}`).forEach(button => { button.disabled = !enableAbilities; });
|
||||
}
|
||||
if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true;
|
||||
}
|
||||
@ -196,64 +193,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
enableGameControls(false, false);
|
||||
}
|
||||
|
||||
|
||||
// --- Инициализация кнопок и обработчиков ---
|
||||
|
||||
// Инициализация кнопок и обработчиков
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const usernameInput = document.getElementById('register-username');
|
||||
const passwordInput = document.getElementById('register-password');
|
||||
if (usernameInput && passwordInput) {
|
||||
// Отключаем кнопки на время регистрации
|
||||
registerForm.querySelector('button').disabled = true;
|
||||
loginForm.querySelector('button').disabled = true;
|
||||
if (loginForm) loginForm.querySelector('button').disabled = true;
|
||||
socket.emit('register', { username: usernameInput.value, password: passwordInput.value });
|
||||
} else {
|
||||
setAuthMessage("Ошибка: поля ввода не найдены.", true);
|
||||
}
|
||||
} else { setAuthMessage("Ошибка: поля ввода не найдены.", true); }
|
||||
});
|
||||
}
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const usernameInput = document.getElementById('login-username');
|
||||
const passwordInput = document.getElementById('login-password');
|
||||
if (usernameInput && passwordInput) {
|
||||
// Отключаем кнопки на время логина
|
||||
registerForm.querySelector('button').disabled = true;
|
||||
if (registerForm) registerForm.querySelector('button').disabled = true;
|
||||
loginForm.querySelector('button').disabled = true;
|
||||
socket.emit('login', { username: usernameInput.value, password: passwordInput.value });
|
||||
} else {
|
||||
setAuthMessage("Ошибка: поля ввода не найдены.", true);
|
||||
}
|
||||
} else { setAuthMessage("Ошибка: поля ввода не найдены.", true); }
|
||||
});
|
||||
}
|
||||
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
// Отключаем кнопку выхода
|
||||
logoutButton.disabled = true;
|
||||
socket.emit('logout');
|
||||
// Сброс состояния и UI происходит по событию logoutResponse или gameNotFound/gameEnded после logout
|
||||
// Пока просто сбрасываем флаги и показываем Auth, т.к. сервер не присылает специальный logoutResponse
|
||||
isLoggedIn = false; loggedInUsername = '';
|
||||
resetGameVariables();
|
||||
isInGame = false;
|
||||
disableGameControls();
|
||||
resetGameVariables(); isInGame = false; disableGameControls();
|
||||
showAuthScreen();
|
||||
setGameStatusMessage("Вы вышли из системы."); // Используем gameStatusMessage для уведомления
|
||||
logoutButton.disabled = false; // Включаем кнопку после обработки (хотя она будет скрыта)
|
||||
setGameStatusMessage("Вы вышли из системы.");
|
||||
logoutButton.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (createAIGameButton) {
|
||||
createAIGameButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return;
|
||||
}
|
||||
// Отключаем кнопки настройки игры
|
||||
if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; }
|
||||
disableSetupButtons();
|
||||
socket.emit('createGame', { mode: 'ai', characterKey: 'elena' });
|
||||
setGameStatusMessage("Создание игры против AI...");
|
||||
@ -261,121 +239,91 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
if (createPvPGameButton) {
|
||||
createPvPGameButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return;
|
||||
}
|
||||
// Отключаем кнопки настройки игры
|
||||
if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; }
|
||||
disableSetupButtons();
|
||||
const selectedCharacter = getSelectedCharacterKey();
|
||||
socket.emit('createGame', { mode: 'pvp', characterKey: selectedCharacter });
|
||||
setGameStatusMessage(`Создание PvP игры за ${selectedCharacter === 'elena' ? 'Елену' : 'Альмагест'}...`);
|
||||
});
|
||||
}
|
||||
if (joinPvPGameButton && gameIdInput) {
|
||||
joinPvPGameButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return;
|
||||
}
|
||||
// Исправляем селектор для joinPvPGameButton, если ID в HTML был join-pvP-game
|
||||
const actualJoinPvPGameButton = document.getElementById('join-pvp-game') || document.getElementById('join-pvP-game');
|
||||
if (actualJoinPvPGameButton && gameIdInput) {
|
||||
actualJoinPvPGameButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; }
|
||||
const gameIdToJoin = gameIdInput.value.trim();
|
||||
if (gameIdToJoin) {
|
||||
// Отключаем кнопки настройки игры
|
||||
disableSetupButtons();
|
||||
socket.emit('joinGame', { gameId: gameIdToJoin });
|
||||
setGameStatusMessage(`Присоединение к игре ${gameIdToJoin}...`);
|
||||
} else {
|
||||
setGameStatusMessage("Пожалуйста, введите ID игры для присоединения.", true);
|
||||
}
|
||||
} else { setGameStatusMessage("Пожалуйста, введите ID игры для присоединения.", true); }
|
||||
});
|
||||
}
|
||||
if (findRandomPvPGameButton) {
|
||||
findRandomPvPGameButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); return;
|
||||
}
|
||||
// Отключаем кнопки настройки игры
|
||||
if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); return; }
|
||||
disableSetupButtons();
|
||||
const selectedCharacter = getSelectedCharacterKey();
|
||||
socket.emit('findRandomGame', { characterKey: selectedCharacter });
|
||||
setGameStatusMessage(`Поиск случайной PvP игры (предпочтение: ${selectedCharacter === 'elena' ? 'Елена' : 'Альмагест'})...`);
|
||||
});
|
||||
}
|
||||
// Функция для отключения кнопок на экране настройки игры
|
||||
function disableSetupButtons() {
|
||||
if(createAIGameButton) createAIGameButton.disabled = true;
|
||||
if(createPvPGameButton) createPvPGameButton.disabled = true;
|
||||
if(joinPvPGameButton) joinPvPGameButton.disabled = true;
|
||||
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = true;
|
||||
if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true);
|
||||
if (createAIGameButton) createAIGameButton.disabled = true;
|
||||
if (createPvPGameButton) createPvPGameButton.disabled = true;
|
||||
if (actualJoinPvPGameButton) actualJoinPvPGameButton.disabled = true;
|
||||
if (findRandomPvPGameButton) findRandomPvPGameButton.disabled = true;
|
||||
if (availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true);
|
||||
}
|
||||
// Функция для включения кнопок на экране настройки игры (вызывается после получения списка игр или сброса состояния)
|
||||
function enableSetupButtons() {
|
||||
if(createAIGameButton) createAIGameButton.disabled = false;
|
||||
if(createPvPGameButton) createPvPGameButton.disabled = false;
|
||||
if(joinPvPGameButton) joinPvPGameButton.disabled = false;
|
||||
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
|
||||
// Кнопки Join в списке игр включаются при обновлении списка (updateAvailableGamesList)
|
||||
if (createAIGameButton) createAIGameButton.disabled = false;
|
||||
if (createPvPGameButton) createPvPGameButton.disabled = false;
|
||||
if (actualJoinPvPGameButton) actualJoinPvPGameButton.disabled = false;
|
||||
if (findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
|
||||
}
|
||||
|
||||
|
||||
if (attackButton) {
|
||||
attackButton.addEventListener('click', () => {
|
||||
// Проверяем isInGame и другие флаги перед отправкой действия
|
||||
if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) {
|
||||
socket.emit('playerAction', { actionType: 'attack' });
|
||||
} else {
|
||||
console.warn('[Client] Попытка действия (атака) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame);
|
||||
disableGameControls(); // Гарантируем, что кнопки будут отключены
|
||||
// Если мы залогинены, но не в игре (isInGame=false), возможно, стоит вернуться в меню выбора игры
|
||||
disableGameControls();
|
||||
if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername);
|
||||
else if (!isLoggedIn) showAuthScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleAbilityButtonClick(event) {
|
||||
const button = event.currentTarget;
|
||||
const abilityId = button.dataset.abilityId;
|
||||
// Проверяем isInGame и другие флаги перед отправкой действия
|
||||
if (isLoggedIn && isInGame && currentGameId && abilityId && currentGameState && !currentGameState.isGameOver) {
|
||||
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
|
||||
} else {
|
||||
console.warn('[Client] Попытка действия (способность) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame);
|
||||
disableGameControls(); // Гарантируем, что кнопки будут отключены
|
||||
disableGameControls();
|
||||
if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername);
|
||||
else if (!isLoggedIn) showAuthScreen();
|
||||
}
|
||||
}
|
||||
|
||||
if (returnToMenuButton) {
|
||||
returnToMenuButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
showAuthScreen(); // Если каким-то образом кнопка активна без логина
|
||||
return;
|
||||
}
|
||||
// Отключаем кнопку возврата в меню
|
||||
if (!isLoggedIn) { showAuthScreen(); return; }
|
||||
returnToMenuButton.disabled = true;
|
||||
|
||||
console.log('[Client] Return to menu button clicked. Resetting game state and showing selection screen.');
|
||||
// Сбрасываем все переменные состояния игры и глобальные ссылки
|
||||
resetGameVariables();
|
||||
isInGame = false;
|
||||
disableGameControls(); // Убедимся, что игровые кнопки отключены
|
||||
hideGameOverModal(); // Убедимся, что модалка скрыта
|
||||
|
||||
showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора игры
|
||||
// Кнопки настройки игры будут включены в showGameSelectionScreen / updateAvailableGamesList
|
||||
resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
|
||||
showGameSelectionScreen(loggedInUsername);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeAbilityButtons() {
|
||||
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>';
|
||||
console.error('[Client.js] initializeAbilityButtons failed: abilitiesGrid, gameUI, or GAME_CONFIG not found.');
|
||||
return;
|
||||
}
|
||||
abilitiesGrid.innerHTML = '';
|
||||
const config = window.GAME_CONFIG;
|
||||
|
||||
const abilitiesToDisplay = playerAbilitiesServer;
|
||||
const baseStatsForResource = playerBaseStatsServer;
|
||||
|
||||
@ -391,41 +339,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
button.id = `ability-btn-${ability.id}`;
|
||||
button.classList.add(abilityButtonClass);
|
||||
button.dataset.abilityId = ability.id;
|
||||
|
||||
let descriptionText = ability.description;
|
||||
|
||||
let cooldown = ability.cooldown;
|
||||
let cooldownText = "";
|
||||
if (typeof cooldown === 'number' && cooldown > 0) {
|
||||
cooldownText = ` (КД: ${cooldown} х.)`;
|
||||
}
|
||||
|
||||
let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${descriptionText || 'Нет описания'}`;
|
||||
let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
|
||||
let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
|
||||
button.setAttribute('title', title);
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.classList.add('ability-name'); 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);
|
||||
|
||||
const nameSpan = document.createElement('span'); nameSpan.classList.add('ability-name'); 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);
|
||||
|
||||
abilitiesGrid.appendChild(button);
|
||||
});
|
||||
const placeholder = abilitiesGrid.querySelector('.placeholder-text');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
// Кнопки инициализированы, updateUI будет управлять их disabled состоянием
|
||||
}
|
||||
|
||||
function updateAvailableGamesList(games) {
|
||||
@ -438,208 +363,125 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`;
|
||||
const joinBtn = document.createElement('button');
|
||||
joinBtn.textContent = 'Присоединиться';
|
||||
joinBtn.dataset.gameId = game.id;
|
||||
joinBtn.textContent = 'Присоединиться'; joinBtn.dataset.gameId = game.id;
|
||||
joinBtn.addEventListener('click', (e) => {
|
||||
if (!isLoggedIn) {
|
||||
setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return;
|
||||
}
|
||||
// Отключаем кнопки настройки игры перед присоединением
|
||||
if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; }
|
||||
disableSetupButtons();
|
||||
socket.emit('joinGame', { gameId: e.target.dataset.gameId });
|
||||
});
|
||||
li.appendChild(joinBtn);
|
||||
ul.appendChild(li);
|
||||
li.appendChild(joinBtn); ul.appendChild(li);
|
||||
}
|
||||
});
|
||||
availableGamesDiv.appendChild(ul);
|
||||
// Включаем кнопки JOIN в списке
|
||||
availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = false);
|
||||
} else {
|
||||
availableGamesDiv.innerHTML += '<p>Нет доступных игр. Создайте свою!</p>';
|
||||
}
|
||||
enableSetupButtons(); // Включаем основные кнопки создания игры после обновления списка
|
||||
} else { availableGamesDiv.innerHTML += '<p>Нет доступных игр. Создайте свою!</p>'; }
|
||||
enableSetupButtons();
|
||||
}
|
||||
|
||||
// --- Обработчики событий Socket.IO ---
|
||||
socket.on('connect', () => {
|
||||
console.log('[Client] Socket connected to server! Socket ID:', socket.id);
|
||||
// При подключении, если залогинен, запросить состояние игры.
|
||||
// Это нужно ТОЛЬКО для восстановления игры, если клиент был в игре и переподключился.
|
||||
if (isLoggedIn) {
|
||||
console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state.`);
|
||||
socket.emit('requestGameState');
|
||||
} else {
|
||||
// Если не залогинен, показываем экран аутентификации
|
||||
showAuthScreen();
|
||||
}
|
||||
} else { showAuthScreen(); }
|
||||
});
|
||||
|
||||
// Обработка registerResponse - теперь включает включение кнопок форм
|
||||
socket.on('registerResponse', (data) => {
|
||||
setAuthMessage(data.message, !data.success);
|
||||
if (data.success && registerForm) registerForm.reset();
|
||||
// Включаем кнопки форм обратно
|
||||
if(registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if(loginForm) loginForm.querySelector('button').disabled = false;
|
||||
if (registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if (loginForm) loginForm.querySelector('button').disabled = false;
|
||||
});
|
||||
|
||||
// Обработка loginResponse - Ключевое изменение здесь
|
||||
socket.on('loginResponse', (data) => {
|
||||
setAuthMessage(data.message, !data.success);
|
||||
if (data.success) {
|
||||
isLoggedIn = true;
|
||||
loggedInUsername = data.username;
|
||||
setAuthMessage(""); // Очищаем сообщение аутентификации
|
||||
|
||||
// --- ИЗМЕНЕНИЕ: СРАЗУ ПОКАЗЫВАЕМ ЭКРАН ВЫБОРА ИГРЫ ---
|
||||
// Не ждем gameNotFound или gameState. Сразу переходим.
|
||||
isLoggedIn = true; loggedInUsername = data.username; setAuthMessage("");
|
||||
showGameSelectionScreen(data.username);
|
||||
// enableSetupButtons() вызывается внутри showGameSelectionScreen / updateAvailableGamesList
|
||||
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||
|
||||
} else {
|
||||
isLoggedIn = false;
|
||||
loggedInUsername = '';
|
||||
// Включаем кнопки форм обратно при ошибке логина
|
||||
if(registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if(loginForm) loginForm.querySelector('button').disabled = false;
|
||||
isLoggedIn = false; loggedInUsername = '';
|
||||
if (registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if (loginForm) loginForm.querySelector('button').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// gameNotFound теперь обрабатывается иначе для залогиненных vs не залогиненных
|
||||
socket.on('gameNotFound', (data) => {
|
||||
console.log('[Client] Game not found response:', data?.message);
|
||||
|
||||
// Сбрасываем игровые переменные, если они были установлены (например, после дисконнекта в игре)
|
||||
resetGameVariables();
|
||||
isInGame = false;
|
||||
disableGameControls(); // Убеждаемся, что игровые кнопки отключены
|
||||
hideGameOverModal(); // Убеждаемся, что модалка скрыта
|
||||
resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none'; // Скрываем таймер
|
||||
|
||||
if (isLoggedIn) {
|
||||
// Если залогинен, и игра не найдена, это НОРМАЛЬНОЕ состояние, если он не был в игре.
|
||||
// Просто показываем экран выбора игры. Сообщение может быть информационным, а не ошибкой.
|
||||
showGameSelectionScreen(loggedInUsername);
|
||||
// Сообщение: "Игровая сессия не найдена" может быть показано, но как статус, не ошибка.
|
||||
// Можно сделать его менее тревожным или вовсе не показывать.
|
||||
// setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.", false); // Информационный статус
|
||||
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); // Сбрасываем на стандартное сообщение
|
||||
enableSetupButtons(); // Включаем кнопки настройки игры
|
||||
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
|
||||
enableSetupButtons();
|
||||
} else {
|
||||
// Если не залогинен и получил gameNotFound (что странно), сбрасываем и показываем логин
|
||||
showAuthScreen();
|
||||
setAuthMessage(data?.message || "Пожалуйста, войдите, чтобы начать новую игру.", false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('[Client] Disconnected from server:', reason);
|
||||
setGameStatusMessage(`Отключено от сервера: ${reason}. Пожалуйста, обновите страницу.`, true);
|
||||
|
||||
// Отключаем игровые кнопки, чтобы предотвратить отправку действий
|
||||
disableGameControls();
|
||||
|
||||
// НЕ сбрасываем игровые переменные немедленно.
|
||||
// Если мы были в игре (isInGame=true), возможно, сервер пришлет gameOver или gameNotFound позже.
|
||||
// Если game over придет, его обработчик покажет модалку и включит кнопку "В меню".
|
||||
// Если gameNotFound придет, его обработчик сбросит переменные и переключит UI.
|
||||
// Если ничего не придет, страница может зависнуть.
|
||||
// В продакшене тут может быть таймер на принудительный сброс и возврат в меню.
|
||||
|
||||
// Если мы не были в игре (например, на экране выбора игры), просто показываем статус.
|
||||
if (!isInGame) {
|
||||
// Остаемся на текущем экране (выбора игры или логина) и показываем статус дисконнекта
|
||||
// UI уже настроен showGameSelectionScreen или showAuthScreen
|
||||
}
|
||||
// === ИЗМЕНЕНИЕ: При дисконнекте останавливаем таймер (если он виден) ===
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = 'Отключено';
|
||||
// Не скрываем контейнер, чтобы было видно сообщение "Отключено"
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
});
|
||||
|
||||
// Обработка gameStarted - без изменений
|
||||
socket.on('gameStarted', (data) => {
|
||||
if (!isLoggedIn) {
|
||||
console.warn('[Client] Ignoring gameStarted: Not logged in.');
|
||||
return;
|
||||
}
|
||||
if (!isLoggedIn) { console.warn('[Client] Ignoring gameStarted: Not logged in.'); return; }
|
||||
console.log('[Client] Event "gameStarted" received:', data);
|
||||
|
||||
if (window.gameUI?.uiElements?.opponent?.panel) {
|
||||
const opponentPanel = window.gameUI.uiElements.opponent.panel;
|
||||
if (opponentPanel.classList.contains('dissolving')) {
|
||||
console.log('[Client.js DEBUG] Removing .dissolving from opponent panel before new game start.');
|
||||
opponentPanel.classList.remove('dissolving');
|
||||
opponentPanel.style.opacity = '1';
|
||||
opponentPanel.style.transform = 'scale(1) translateY(0)';
|
||||
opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)';
|
||||
}
|
||||
}
|
||||
currentGameId = data.gameId; myPlayerId = data.yourPlayerId; currentGameState = data.initialGameState;
|
||||
playerBaseStatsServer = data.playerBaseStats; opponentBaseStatsServer = data.opponentBaseStats;
|
||||
playerAbilitiesServer = data.playerAbilities; opponentAbilitiesServer = data.opponentAbilities;
|
||||
myCharacterKey = playerBaseStatsServer?.characterKey; opponentCharacterKey = opponentBaseStatsServer?.characterKey;
|
||||
|
||||
// Убедимся, что игровые переменные обновлены (на случай, если игра началась сразу после логина без requestGameState)
|
||||
currentGameId = data.gameId;
|
||||
myPlayerId = data.yourPlayerId;
|
||||
currentGameState = data.initialGameState;
|
||||
playerBaseStatsServer = data.playerBaseStats;
|
||||
opponentBaseStatsServer = data.opponentBaseStats;
|
||||
playerAbilitiesServer = data.playerAbilities;
|
||||
opponentAbilitiesServer = data.opponentAbilities;
|
||||
myCharacterKey = playerBaseStatsServer?.characterKey;
|
||||
opponentCharacterKey = opponentBaseStatsServer?.characterKey;
|
||||
|
||||
if (data.clientConfig) {
|
||||
window.GAME_CONFIG = { ...data.clientConfig };
|
||||
console.log('[Client.js gameStarted] Received clientConfig from server.');
|
||||
} else if (!window.GAME_CONFIG) {
|
||||
if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig };
|
||||
else if (!window.GAME_CONFIG) {
|
||||
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
|
||||
console.warn('[Client.js gameStarted] No clientConfig received from server. Using fallback.');
|
||||
}
|
||||
|
||||
window.gameState = currentGameState;
|
||||
window.gameData = {
|
||||
playerBaseStats: playerBaseStatsServer,
|
||||
opponentBaseStats: opponentBaseStatsServer,
|
||||
playerAbilities: playerAbilitiesServer,
|
||||
opponentAbilities: opponentAbilitiesServer
|
||||
};
|
||||
window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer };
|
||||
window.myPlayerId = myPlayerId;
|
||||
|
||||
showGameScreen(); // Показываем игровой экран (ставит isInGame = true)
|
||||
initializeAbilityButtons(); // Инициализируем кнопки способностей
|
||||
|
||||
if (window.gameUI?.uiElements?.log?.list) {
|
||||
window.gameUI.uiElements.log.list.innerHTML = '';
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
|
||||
console.log('[Client] Calling gameUI.updateUI() after gameStarted.');
|
||||
window.gameUI.updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
hideGameOverModal();
|
||||
setGameStatusMessage(""); // Скрываем статус сообщение, если видим игровой экран
|
||||
hideGameOverModal(); setGameStatusMessage("");
|
||||
});
|
||||
|
||||
// Обработка gameStateUpdate - без изменений (проверяет isLoggedIn и isInGame)
|
||||
socket.on('gameStateUpdate', (data) => {
|
||||
if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) {
|
||||
console.warn('[Client] Ignoring gameStateUpdate: Not logged in or not in game context.');
|
||||
return;
|
||||
}
|
||||
currentGameState = data.gameState;
|
||||
window.gameState = currentGameState;
|
||||
|
||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
|
||||
window.gameUI.updateUI();
|
||||
}
|
||||
currentGameState = data.gameState; window.gameState = currentGameState;
|
||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI();
|
||||
if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
|
||||
data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка logUpdate - без изменений (проверяет isLoggedIn и isInGame)
|
||||
socket.on('logUpdate', (data) => {
|
||||
if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) {
|
||||
console.warn('[Client] Ignoring logUpdate: Not logged in or not in game context.');
|
||||
@ -650,66 +492,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка gameOver - без изменений (сбрасывает gameState в конце для UI, но переменные игры не сбрасывает сразу)
|
||||
socket.on('gameOver', (data) => {
|
||||
if (!isLoggedIn || !currentGameId || !window.GAME_CONFIG) {
|
||||
console.warn('[Client] Ignoring gameOver: Not logged in or currentGameId is null/stale.');
|
||||
// Если игра окончена, но состояние клиента было некорректным, попробуем сбросить его
|
||||
if (!currentGameId && isLoggedIn) socket.emit('requestGameState'); // Попробуем запросить состояние
|
||||
if (!currentGameId && isLoggedIn) socket.emit('requestGameState');
|
||||
else if (!isLoggedIn) showAuthScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Client gameOver] Received for game ${currentGameId}. My technical slot ID (myPlayerId): ${myPlayerId}, Winner's slot ID from server (data.winnerId): ${data.winnerId}`);
|
||||
const playerWon = data.winnerId === myPlayerId;
|
||||
console.log(`[Client gameOver] Calculated playerWon for this client: ${playerWon}`);
|
||||
|
||||
currentGameState = data.finalGameState;
|
||||
window.gameState = currentGameState;
|
||||
|
||||
currentGameState = data.finalGameState; window.gameState = currentGameState;
|
||||
console.log('[Client gameOver] Final GameState:', currentGameState);
|
||||
|
||||
if (window.gameData) {
|
||||
console.log(`[Client gameOver] For ui.js, myName: ${window.gameData.playerBaseStats?.name}, opponentName: ${window.gameData.opponentBaseStats?.name}`);
|
||||
}
|
||||
|
||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
|
||||
window.gameUI.updateUI();
|
||||
}
|
||||
if (window.gameData) console.log(`[Client gameOver] For ui.js, myName: ${window.gameData.playerBaseStats?.name}, opponentName: ${window.gameData.opponentBaseStats?.name}`);
|
||||
if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI();
|
||||
if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
|
||||
data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
|
||||
}
|
||||
|
||||
if (window.gameUI && typeof window.gameUI.showGameOver === 'function') {
|
||||
const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey;
|
||||
window.gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal, data);
|
||||
}
|
||||
|
||||
if (returnToMenuButton) {
|
||||
returnToMenuButton.disabled = false; // Включаем кнопку "В меню" в модалке
|
||||
}
|
||||
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||
setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
||||
|
||||
// isInGame остается true, пока не нажмут "В меню"
|
||||
// disableGameControls() уже вызвано через updateUI из-за isGameOver
|
||||
// === ИЗМЕНЕНИЕ: При gameOver скрываем таймер или показываем "Игра окончена" ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Оставляем видимым
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = 'Конец';
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
});
|
||||
|
||||
// Обработка waitingForOpponent - без изменений
|
||||
socket.on('waitingForOpponent', () => {
|
||||
if (!isLoggedIn) return;
|
||||
setGameStatusMessage("Ожидание присоединения оппонента...");
|
||||
disableGameControls(); // Отключаем кнопки, пока ждем
|
||||
// Включаем кнопки настройки игры после попытки создания/присоединения к ожидающей игре
|
||||
// чтобы игрок мог отменить или попробовать другое
|
||||
enableSetupButtons();
|
||||
// Однако, если игрок создал игру, кнопки "Создать" должны быть отключены,
|
||||
// а если он искал и создал, то тоже.
|
||||
// Возможно, лучше отключать кнопки создания/поиска, оставляя только "Присоединиться" по ID или отмену.
|
||||
// Для простоты пока включаем все, кроме кнопок боя.
|
||||
// disableSetupButtons(); // Лучше оставить их отключенными до gameStarted или gameNotFound
|
||||
disableGameControls();
|
||||
enableSetupButtons(); // Можно оставить возможность отменить, если долго ждет
|
||||
// === ИЗМЕНЕНИЕ: При ожидании оппонента таймер неактивен ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
});
|
||||
|
||||
// Обработка opponentDisconnected - без изменений (проверяет isLoggedIn и isInGame)
|
||||
socket.on('opponentDisconnected', (data) => {
|
||||
if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) {
|
||||
console.warn('[Client] Ignoring opponentDisconnected: Not logged in or not in game context.');
|
||||
@ -717,84 +539,91 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
|
||||
const disconnectedCharacterName = data.disconnectedCharacterName || 'Противник';
|
||||
const disconnectedCharacterKey = data.disconnectedCharacterKey || 'unknown';
|
||||
|
||||
if (window.gameUI && typeof window.gameUI.addToLog === 'function') {
|
||||
window.gameUI.addToLog(`🔌 Противник (${disconnectedCharacterName}) отключился.`, systemLogType);
|
||||
}
|
||||
|
||||
if (currentGameState && !currentGameState.isGameOver) {
|
||||
setGameStatusMessage(`Противник (${disconnectedCharacterName}) отключился. Ожидание завершения игры сервером...`, true);
|
||||
disableGameControls(); // Отключаем кнопки немедленно
|
||||
disableGameControls();
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка gameError - без изменений
|
||||
socket.on('gameError', (data) => {
|
||||
console.error('[Client] Server error:', data.message);
|
||||
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
|
||||
|
||||
// Если в игре, добавляем в лог и отключаем кнопки
|
||||
if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof window.gameUI.addToLog === 'function') {
|
||||
window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, systemLogType);
|
||||
disableGameControls(); // Отключаем кнопки при ошибке
|
||||
setGameStatusMessage(`Ошибка в игре: ${data.message}.`, true);
|
||||
// Возможно, тут нужно вернуть игрока в меню после небольшой задержки?
|
||||
// setTimeout(() => {
|
||||
// if (isLoggedIn && isInGame) { // Проверяем, что все еще в игре после задержки
|
||||
// alert("Произошла ошибка. Вы будете возвращены в меню выбора игры."); // Сообщение пользователю
|
||||
// // Симулируем нажатие кнопки "В меню"
|
||||
// if (returnToMenuButton && !returnToMenuButton.disabled) {
|
||||
// returnToMenuButton.click();
|
||||
// } else {
|
||||
// // Если кнопка "В меню" отключена или не найдена, сбрасываем вручную
|
||||
// resetGameVariables(); isInGame = false; showGameSelectionScreen(loggedInUsername);
|
||||
// }
|
||||
// }
|
||||
// }, 3000); // Задержка перед возвратом
|
||||
} else {
|
||||
// Ошибка вне контекста игры
|
||||
setGameStatusMessage(`❌ Ошибка игры: ${data.message}`, true);
|
||||
// Сбрасываем состояние, если ошибка пришла не в игре
|
||||
resetGameVariables();
|
||||
isInGame = false;
|
||||
disableGameControls();
|
||||
|
||||
if(isLoggedIn && loggedInUsername) {
|
||||
showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора игры
|
||||
} else {
|
||||
showAuthScreen(); // Возвращаемся на экран логина
|
||||
}
|
||||
}
|
||||
// Включаем кнопки форм/настройки игры после обработки ошибки
|
||||
if (!isLoggedIn) { // Если на экране логина
|
||||
if(registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if(loginForm) loginForm.querySelector('button').disabled = false;
|
||||
} else if (!isInGame) { // Если на экране выбора игры
|
||||
enableSetupButtons();
|
||||
setGameStatusMessage(`Ошибка в игре: ${data.message}.`, true);
|
||||
} else {
|
||||
setGameStatusMessage(`❌ Ошибка игры: ${data.message}`, true);
|
||||
resetGameVariables(); isInGame = false; disableGameControls();
|
||||
if (isLoggedIn && loggedInUsername) showGameSelectionScreen(loggedInUsername);
|
||||
else showAuthScreen();
|
||||
}
|
||||
if (!isLoggedIn) {
|
||||
if (registerForm) registerForm.querySelector('button').disabled = false;
|
||||
if (loginForm) loginForm.querySelector('button').disabled = false;
|
||||
} else if (!isInGame) { enableSetupButtons(); }
|
||||
});
|
||||
|
||||
|
||||
socket.on('availablePvPGamesList', (games) => {
|
||||
if (!isLoggedIn) return;
|
||||
updateAvailableGamesList(games); // updateAvailableGamesList включает кнопки Join и основные кнопки создания
|
||||
updateAvailableGamesList(games);
|
||||
});
|
||||
|
||||
socket.on('noPendingGamesFound', (data) => {
|
||||
if (!isLoggedIn) return;
|
||||
// Это информационное сообщение, когда игрок искал игру и создал новую
|
||||
// currentGameId и myPlayerId должны быть установлены событием 'gameCreated'
|
||||
setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас, ожидайте оппонента.");
|
||||
updateAvailableGamesList([]); // Очищаем список, т.к. мы теперь в ожидающей игре
|
||||
isInGame = false; // Пока ждем, не в активной игре
|
||||
disableGameControls(); // Кнопки боя отключены
|
||||
// Кнопки настройки игры должны оставаться отключенными, пока ждем игрока
|
||||
disableSetupButtons();
|
||||
updateAvailableGamesList([]);
|
||||
isInGame = false; disableGameControls(); disableSetupButtons();
|
||||
// === ИЗМЕНЕНИЕ: При ожидании оппонента (создана новая игра) таймер неактивен ===
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
});
|
||||
|
||||
// === ИЗМЕНЕНИЕ: Обработчик события обновления таймера ===
|
||||
socket.on('turnTimerUpdate', (data) => {
|
||||
if (!isInGame || !currentGameState || currentGameState.isGameOver) {
|
||||
// Если игра не активна, или уже завершена, или нет состояния, игнорируем обновление таймера
|
||||
if (turnTimerContainer && !currentGameState?.isGameOver) turnTimerContainer.style.display = 'none'; // Скрываем, если не game over
|
||||
if (turnTimerSpan && !currentGameState?.isGameOver) turnTimerSpan.textContent = '--';
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnTimerSpan && turnTimerContainer) {
|
||||
if (data.remainingTime === null || data.remainingTime === undefined) {
|
||||
// Сервер сигнализирует, что таймер неактивен (например, ход AI)
|
||||
turnTimerContainer.style.display = 'block'; // Контейнер может быть видимым
|
||||
// Определяем, чей ход, чтобы показать соответствующее сообщение
|
||||
const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID);
|
||||
|
||||
if (!data.isPlayerTurn && currentGameState.gameMode === 'ai') { // Ход AI
|
||||
turnTimerSpan.textContent = 'Ход ИИ';
|
||||
turnTimerSpan.classList.remove('low-time');
|
||||
} else if (!isMyActualTurn && currentGameState.gameMode === 'pvp' && !data.isPlayerTurn !== (myPlayerId === GAME_CONFIG.PLAYER_ID)) { // Ход оппонента в PvP
|
||||
turnTimerSpan.textContent = 'Ход оппонента';
|
||||
turnTimerSpan.classList.remove('low-time');
|
||||
} else { // Ход текущего игрока, но сервер прислал null - странно, но покажем '--'
|
||||
turnTimerSpan.textContent = '--';
|
||||
turnTimerSpan.classList.remove('low-time');
|
||||
}
|
||||
} else {
|
||||
turnTimerContainer.style.display = 'block'; // Убедимся, что контейнер виден
|
||||
const seconds = Math.ceil(data.remainingTime / 1000);
|
||||
turnTimerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
|
||||
// Добавляем/удаляем класс для предупреждения, если времени мало
|
||||
if (seconds <= 10) { // Например, 10 секунд - порог
|
||||
turnTimerSpan.classList.add('low-time');
|
||||
} else {
|
||||
turnTimerSpan.classList.remove('low-time');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
|
||||
// --- Изначальное состояние UI при загрузке страницы ---
|
||||
// При загрузке страницы всегда начинаем с Auth.
|
||||
showAuthScreen();
|
||||
});
|
661
public/js/ui.js
661
public/js/ui.js
@ -5,7 +5,7 @@
|
||||
(function() {
|
||||
// --- DOM Элементы ---
|
||||
const uiElements = {
|
||||
player: { // Панель для персонажа, которым управляет ЭТОТ клиент
|
||||
player: {
|
||||
panel: document.getElementById('player-panel'),
|
||||
name: document.getElementById('player-name'),
|
||||
avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
|
||||
@ -14,10 +14,9 @@
|
||||
status: document.getElementById('player-status'),
|
||||
effectsContainer: document.getElementById('player-effects'),
|
||||
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
|
||||
// ИСПРАВЛЕНО: Селектор для списка дебаффов игрока
|
||||
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
|
||||
},
|
||||
opponent: { // Панель для персонажа-противника ЭТОГО клиента
|
||||
opponent: {
|
||||
panel: document.getElementById('opponent-panel'),
|
||||
name: document.getElementById('opponent-name'),
|
||||
avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
|
||||
@ -25,16 +24,18 @@
|
||||
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
|
||||
status: document.getElementById('opponent-status'),
|
||||
effectsContainer: document.getElementById('opponent-effects'),
|
||||
// ИСПРАВЛЕНО: Селектор для списка баффов оппонента
|
||||
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
|
||||
// ИСПРАВЛЕНО: Селектор для списка дебаффов оппонента
|
||||
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
|
||||
},
|
||||
controls: {
|
||||
turnIndicator: document.getElementById('turn-indicator'),
|
||||
buttonAttack: document.getElementById('button-attack'),
|
||||
buttonBlock: document.getElementById('button-block'), // Защита пока не активна
|
||||
buttonBlock: document.getElementById('button-block'),
|
||||
abilitiesGrid: document.getElementById('abilities-grid'),
|
||||
// === ИЗМЕНЕНИЕ: Добавлены элементы таймера ===
|
||||
turnTimerContainer: document.getElementById('turn-timer-container'),
|
||||
turnTimerSpan: document.getElementById('turn-timer')
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
},
|
||||
log: {
|
||||
list: document.getElementById('log-list'),
|
||||
@ -58,26 +59,17 @@
|
||||
const li = document.createElement('li');
|
||||
li.textContent = message;
|
||||
const config = window.GAME_CONFIG || {};
|
||||
// Формируем класс для лога на основе типа (используем константы из конфига или фоллбэк)
|
||||
const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
|
||||
li.className = logTypeClass;
|
||||
logListElement.appendChild(li);
|
||||
// Прокрутка лога вниз
|
||||
requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
|
||||
}
|
||||
|
||||
function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
|
||||
const elements = uiElements[panelRole]; // 'player' или 'opponent'
|
||||
const elements = uiElements[panelRole];
|
||||
const config = window.GAME_CONFIG || {};
|
||||
|
||||
// Базовая проверка наличия необходимых элементов и данных
|
||||
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
|
||||
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
|
||||
if (elements && elements.panel && elements.panel.style.display !== 'none') {
|
||||
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
|
||||
// elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных
|
||||
}
|
||||
// ВАЖНО: Очистить содержимое панели, если данных нет.
|
||||
if (elements) {
|
||||
if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? '<i class="fas fa-question icon-player"></i> Ожидание данных...' : '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
|
||||
if(elements.hpText) elements.hpText.textContent = 'N/A';
|
||||
@ -90,33 +82,25 @@
|
||||
if(panelRole === 'opponent' && uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
|
||||
if(panelRole === 'player' && uiElements.playerResourceBarContainer) uiElements.playerResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
||||
if(panelRole === 'opponent' && uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
||||
if(elements.panel) elements.panel.style.opacity = '0.5'; // Затемняем
|
||||
if(elements.panel) elements.panel.style.opacity = '0.5';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (elements.panel) elements.panel.style.opacity = '1'; // Делаем видимой, если данные есть
|
||||
if (elements.panel) elements.panel.style.opacity = '1';
|
||||
|
||||
|
||||
// Обновление имени и иконки персонажа
|
||||
if (elements.name) {
|
||||
let iconClass = 'fa-question'; // Иконка по умолчанию
|
||||
let iconClass = 'fa-question';
|
||||
const characterKey = fighterBaseStats.characterKey;
|
||||
|
||||
// Определяем класс иконки в зависимости от персонажа
|
||||
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; }
|
||||
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } // Используем специфичный класс для цвета
|
||||
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
|
||||
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; }
|
||||
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ }
|
||||
|
||||
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } // Для Баларда тоже специфичный
|
||||
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
|
||||
if (isControlledByThisClient) nameHtml += " (Вы)";
|
||||
elements.name.innerHTML = nameHtml;
|
||||
}
|
||||
|
||||
// Обновление аватара
|
||||
if (elements.avatar && fighterBaseStats.avatarPath) {
|
||||
elements.avatar.src = fighterBaseStats.avatarPath;
|
||||
// Обновляем рамку аватара в зависимости от персонажа
|
||||
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
|
||||
elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`);
|
||||
} else if (elements.avatar) {
|
||||
@ -124,243 +108,172 @@
|
||||
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
|
||||
}
|
||||
|
||||
|
||||
// Обновление полос здоровья и ресурса
|
||||
const maxHp = Math.max(1, fighterBaseStats.maxHp);
|
||||
const maxRes = Math.max(1, fighterBaseStats.maxResource);
|
||||
const currentHp = Math.max(0, fighterState.currentHp);
|
||||
const currentRes = Math.max(0, fighterState.currentResource);
|
||||
|
||||
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
|
||||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`; // Здоровье округляем
|
||||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
|
||||
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
|
||||
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`; // Ресурс не округляем
|
||||
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`;
|
||||
|
||||
|
||||
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
|
||||
const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
|
||||
const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
|
||||
|
||||
if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
|
||||
resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy');
|
||||
let resourceClass = 'mana'; let iconClass = 'fa-flask';
|
||||
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
|
||||
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } // или fa-wand-magic-sparkles, fa-star-half-alt и т.д.
|
||||
else { console.warn(`updateFighterPanelUI: Unknown resource name "${fighterBaseStats.resourceName}" for icon/color.`); iconClass = 'fa-question-circle'; }
|
||||
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
|
||||
resourceBarContainerToUpdate.classList.add(resourceClass);
|
||||
resourceIconElementToUpdate.className = `fas ${iconClass}`;
|
||||
}
|
||||
|
||||
// Обновление статуса (Готов/Защищается)
|
||||
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
|
||||
elements.status.textContent = statusText;
|
||||
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
|
||||
|
||||
|
||||
// Обновление подсветки и рамки панели (в зависимости от персонажа)
|
||||
if (elements.panel) {
|
||||
let borderColorVar = 'var(--panel-border)';
|
||||
elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
|
||||
|
||||
if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; }
|
||||
else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; }
|
||||
else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; }
|
||||
else { console.warn(`updateFighterPanelUI: Unknown character key "${fighterBaseStats.characterKey}" for panel border color.`); }
|
||||
|
||||
|
||||
let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень
|
||||
let glowColorVar = 'rgba(0, 0, 0, 0.4)';
|
||||
if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
|
||||
// В твоем CSS --panel-glow-opponent используется для обоих Баларда и Альмагест
|
||||
else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
|
||||
|
||||
else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; // Отдельный цвет для Альмагест
|
||||
else if (fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
|
||||
elements.panel.style.borderColor = borderColorVar;
|
||||
elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует HTML для списка эффектов.
|
||||
* @param {Array<object>} effectsArray - Массив объектов эффектов, УЖЕ отфильтрованных и отсортированных.
|
||||
* @returns {string} HTML-строка для отображения списка эффектов.
|
||||
*/
|
||||
function generateEffectsHTML(effectsArray) {
|
||||
const config = window.GAME_CONFIG || {};
|
||||
if (!effectsArray || effectsArray.length === 0) return 'Нет';
|
||||
|
||||
// ВАЖНО: Сортировка теперь выполняется ВНЕ этой функции (в updateEffectsUI)
|
||||
|
||||
return effectsArray.map(eff => {
|
||||
let effectClasses = config.CSS_CLASS_EFFECT || 'effect'; // Базовый класс для всех эффектов
|
||||
let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
|
||||
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
|
||||
const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
|
||||
|
||||
// Добавляем специфичные классы для стилизации по типу эффекта
|
||||
// Логика определения класса должна соответствовать логике разделения на баффы/дебаффы
|
||||
if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || eff.type === config.ACTION_TYPE_DISABLE) {
|
||||
// Эффекты полного безмолвия, заглушения абилок или типа DISABLE
|
||||
effectClasses += ' effect-stun'; // Класс для стана/безмолвия (красный/желтый)
|
||||
} else if (eff.grantsBlock) { // Эффекты, дающие блок
|
||||
effectClasses += ' effect-block'; // Класс для эффектов блока (синий)
|
||||
} else if (eff.type === config.ACTION_TYPE_DEBUFF) { // Явные дебаффы (например, сжигание ресурса)
|
||||
effectClasses += ' effect-debuff'; // Класс для ослаблений (красноватый)
|
||||
} else if (eff.type === config.ACTION_TYPE_BUFF) { // Явные баффы (например, усиление атаки)
|
||||
effectClasses += ' effect-buff'; // Класс для усилений (зеленый)
|
||||
} else if (eff.type === config.ACTION_TYPE_HEAL) { // Эффекты лечения (HoT)
|
||||
effectClasses += ' effect-buff'; // HoT стилизуем как бафф (зеленый)
|
||||
}
|
||||
// Если есть другие типы (DoT, Drain и т.п.), которые не входят в эти категории,
|
||||
// их нужно добавить или стилизовать как info.
|
||||
// DoT можно стилизовать как effect-debuff or effect-damage, Drain as effect-debuff.
|
||||
// Например: else if (eff.type === config.ACTION_TYPE_DAMAGE) { effectClasses += ' effect-debuff'; } // DoT как дебафф
|
||||
// else if (eff.type === config.ACTION_TYPE_DRAIN) { effectClasses += ' effect-debuff'; } // Drain как дебафф
|
||||
else {
|
||||
//console.warn(`generateEffectsHTML: Эффект ID "${eff.id}" с типом "${eff.type}" не имеет специфичного класса стилизации.`);
|
||||
effectClasses += ' effect-info'; // Класс по умолчанию или информационный (серый/синий)
|
||||
}
|
||||
|
||||
if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || eff.type === config.ACTION_TYPE_DISABLE) effectClasses += ' effect-stun';
|
||||
else if (eff.grantsBlock) effectClasses += ' effect-block';
|
||||
else if (eff.type === config.ACTION_TYPE_DEBUFF) effectClasses += ' effect-debuff';
|
||||
else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) effectClasses += ' effect-buff';
|
||||
else effectClasses += ' effect-info';
|
||||
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
function updateEffectsUI(currentGameState) {
|
||||
if (!currentGameState || !window.GAME_CONFIG) { return; }
|
||||
if (!currentGameState || !window.GAME_CONFIG) return;
|
||||
const mySlotId = window.myPlayerId;
|
||||
const config = window.GAME_CONFIG;
|
||||
if (!mySlotId) { return; }
|
||||
|
||||
if (!mySlotId) return;
|
||||
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
|
||||
|
||||
const myState = currentGameState[mySlotId];
|
||||
const opponentState = currentGameState[opponentSlotId];
|
||||
|
||||
// --- Логика сортировки эффектов (для использования как для баффов, так и для дебаффов) ---
|
||||
// Сортируем эффекты по типу: сначала позитивные, потом негативные, потом контроля
|
||||
const typeOrder = {
|
||||
[config.ACTION_TYPE_BUFF]: 1,
|
||||
grantsBlock: 2,
|
||||
[config.ACTION_TYPE_HEAL]: 3, // HoT эффекты
|
||||
[config.ACTION_TYPE_DEBUFF]: 4, // DoT, ресурсные дебаффы
|
||||
[config.ACTION_TYPE_DISABLE]: 5 // Silence, Stun
|
||||
// Добавьте другие типы, если нужно сортировать
|
||||
};
|
||||
const typeOrder = { [config.ACTION_TYPE_BUFF]: 1, grantsBlock: 2, [config.ACTION_TYPE_HEAL]: 3, [config.ACTION_TYPE_DEBUFF]: 4, [config.ACTION_TYPE_DISABLE]: 5 };
|
||||
const sortEffects = (a, b) => {
|
||||
// Определяем порядок для эффекта A
|
||||
let orderA = typeOrder[a.type] || 99;
|
||||
if (a.grantsBlock) orderA = typeOrder.grantsBlock;
|
||||
// isFullSilence и playerSilencedOn_X - это эффекты типа DISABLE, но их можно поставить выше в приоритете дебаффов
|
||||
if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||
// Добавьте сюда другие специфичные проверки, если нужно изменить стандартный порядок по типу
|
||||
|
||||
// Определяем порядок для эффекта B
|
||||
let orderB = typeOrder[b.type] || 99;
|
||||
if (b.grantsBlock) orderB = typeOrder.grantsBlock;
|
||||
if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||
|
||||
return (orderA || 99) - (orderB || 99); // Сортируем по порядку
|
||||
let orderA = typeOrder[a.type] || 99; if (a.grantsBlock) orderA = typeOrder.grantsBlock; if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||
let orderB = typeOrder[b.type] || 99; if (b.grantsBlock) orderB = typeOrder.grantsBlock; if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||
return (orderA || 99) - (orderB || 99);
|
||||
};
|
||||
// --- Конец логики сортировки ---
|
||||
|
||||
|
||||
// --- Обработка эффектов Игрока (My Player) ---
|
||||
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
|
||||
const myBuffs = [];
|
||||
const myDebuffs = [];
|
||||
|
||||
// ИСПРАВЛЕНО: Проходим по массиву activeEffects один раз и пушим в нужный список
|
||||
const myBuffs = []; const myDebuffs = [];
|
||||
myState.activeEffects.forEach(e => {
|
||||
// Определяем, является ли эффект баффом
|
||||
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
|
||||
|
||||
// Определяем, является ли эффект дебаффом
|
||||
// Учитываем типы DEBUFF, DISABLE, а также специфические флаги/ID для полного безмолвия и заглушения конкретных абилок
|
||||
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
|
||||
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('playerSilencedOn_');
|
||||
|
||||
// Добавляем эффект в соответствующий список (каждый эффект должен попасть только в один)
|
||||
if (isBuff) {
|
||||
myBuffs.push(e);
|
||||
} else if (isDebuff) {
|
||||
myDebuffs.push(e);
|
||||
} else {
|
||||
// Если эффект не попал ни в одну категорию (например, новый тип?)
|
||||
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Игрока.`);
|
||||
myDebuffs.push(e); // Добавим в дебаффы по умолчанию
|
||||
}
|
||||
if (isBuff) myBuffs.push(e); else if (isDebuff) myDebuffs.push(e); else myDebuffs.push(e);
|
||||
});
|
||||
|
||||
// Сортируем списки баффов и дебаффов перед генерацией HTML
|
||||
myBuffs.sort(sortEffects);
|
||||
myDebuffs.sort(sortEffects);
|
||||
|
||||
myBuffs.sort(sortEffects); myDebuffs.sort(sortEffects);
|
||||
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
|
||||
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
|
||||
|
||||
} else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
|
||||
// Если нет активных эффектов или состояния, очищаем списки
|
||||
uiElements.player.buffsList.innerHTML = 'Нет';
|
||||
uiElements.player.debuffsList.innerHTML = 'Нет';
|
||||
uiElements.player.buffsList.innerHTML = 'Нет'; uiElements.player.debuffsList.innerHTML = 'Нет';
|
||||
}
|
||||
|
||||
|
||||
// --- Обработка эффектов Оппонента (Opponent Player) ---
|
||||
// Логика аналогична игроку, но условия дебаффов могут немного отличаться
|
||||
// (например, префикс ID заглушения абилок)
|
||||
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
|
||||
const opponentBuffs = [];
|
||||
const opponentDebuffs = [];
|
||||
|
||||
// ИСПРАВЛЕНО: Проходим по массиву activeEffects оппонента один раз и пушим в нужный список
|
||||
const opponentBuffs = []; const opponentDebuffs = [];
|
||||
opponentState.activeEffects.forEach(e => {
|
||||
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
|
||||
|
||||
// Определяем, является ли эффект дебаффом для ОППОНЕНТА
|
||||
// Учитываем типы DEBUFF, DISABLE, isFullSilence.
|
||||
// id.startsWith('playerSilencedOn_') специфично для игрока,
|
||||
// id.startsWith('effect_') используется для дебаффов, наложенных на цель (например, Seal of Weakness)
|
||||
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
|
||||
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('effect_');
|
||||
// Если у оппонента есть свои специфичные эффекты заглушения с другим префиксом, его тоже нужно добавить сюда.
|
||||
|
||||
if (isBuff) {
|
||||
opponentBuffs.push(e);
|
||||
} else if (isDebuff) {
|
||||
opponentDebuffs.push(e);
|
||||
} else {
|
||||
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Оппонента.`);
|
||||
opponentDebuffs.push(e); // Добавим в дебаффы по умолчанию
|
||||
}
|
||||
if (isBuff) opponentBuffs.push(e); else if (isDebuff) opponentDebuffs.push(e); else opponentDebuffs.push(e);
|
||||
});
|
||||
|
||||
// Сортируем списки баффов и дебаффов оппонента
|
||||
opponentBuffs.sort(sortEffects);
|
||||
opponentDebuffs.sort(sortEffects);
|
||||
|
||||
opponentBuffs.sort(sortEffects); opponentDebuffs.sort(sortEffects);
|
||||
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
|
||||
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
|
||||
|
||||
} else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
|
||||
// Если нет активных эффектов или состояния оппонента, очищаем списки
|
||||
uiElements.opponent.buffsList.innerHTML = 'Нет';
|
||||
uiElements.opponent.debuffsList.innerHTML = 'Нет';
|
||||
uiElements.opponent.buffsList.innerHTML = 'Нет'; uiElements.opponent.debuffsList.innerHTML = 'Нет';
|
||||
}
|
||||
}
|
||||
|
||||
// === ИЗМЕНЕНИЕ: Новая функция для обновления таймера ===
|
||||
/**
|
||||
* Обновляет отображение таймера хода.
|
||||
* @param {number|null} remainingTimeMs - Оставшееся время в миллисекундах, или null если таймер неактивен.
|
||||
* @param {boolean} isCurrentPlayerActualTurn - Флаг, является ли текущий ход ходом этого клиента.
|
||||
* @param {string} gameMode - Режим игры ('ai' или 'pvp').
|
||||
*/
|
||||
function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) {
|
||||
const timerSpan = uiElements.controls.turnTimerSpan;
|
||||
const timerContainer = uiElements.controls.turnTimerContainer;
|
||||
const config = window.GAME_CONFIG || {};
|
||||
|
||||
if (!timerSpan || !timerContainer) return;
|
||||
|
||||
if (window.gameState && window.gameState.isGameOver) {
|
||||
timerContainer.style.display = 'block'; // Может быть 'flex' или другой, в зависимости от CSS
|
||||
timerSpan.textContent = 'Конец';
|
||||
timerSpan.classList.remove('low-time');
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingTimeMs === null || remainingTimeMs === undefined) {
|
||||
timerContainer.style.display = 'block';
|
||||
timerSpan.classList.remove('low-time');
|
||||
if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { // Предполагаем, что если не ход игрока в AI, то ход AI
|
||||
timerSpan.textContent = 'Ход ИИ';
|
||||
} else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) {
|
||||
timerSpan.textContent = 'Ход оппонента';
|
||||
} else { // Ход текущего игрока, но нет времени (например, ожидание первого хода)
|
||||
timerSpan.textContent = '--';
|
||||
}
|
||||
} else {
|
||||
timerContainer.style.display = 'block';
|
||||
const seconds = Math.ceil(remainingTimeMs / 1000);
|
||||
timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
|
||||
if (seconds <= 10 && isCurrentPlayerActualTurn) { // Предупреждение только если это мой ход
|
||||
timerSpan.classList.add('low-time');
|
||||
} else {
|
||||
timerSpan.classList.remove('low-time');
|
||||
}
|
||||
}
|
||||
}
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
|
||||
|
||||
function updateUI() {
|
||||
const currentGameState = window.gameState; // Глобальное состояние игры
|
||||
const gameDataGlobal = window.gameData; // Глобальные данные ( статы, абилки ) для этого клиента
|
||||
const configGlobal = window.GAME_CONFIG; // Глобальный конфиг
|
||||
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
|
||||
const currentGameState = window.gameState;
|
||||
const gameDataGlobal = window.gameData;
|
||||
const configGlobal = window.GAME_CONFIG;
|
||||
const myActualPlayerId = window.myPlayerId;
|
||||
|
||||
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
|
||||
// console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
|
||||
// Сбрасываем UI панелей, если данные отсутствуют
|
||||
updateFighterPanelUI('player', null, null, true);
|
||||
updateFighterPanelUI('opponent', null, null, false);
|
||||
// Скрываем/очищаем остальные элементы UI игры
|
||||
if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `<span>Ожидание данных...</span>`;
|
||||
if(uiElements.controls.turnIndicator) uiElements.controls.turnIndicator.textContent = "Ожидание данных...";
|
||||
if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true;
|
||||
if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
|
||||
if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = '<p class="placeholder-text">Загрузка способностей...</p>';
|
||||
|
||||
// === ИЗМЕНЕНИЕ: Сбрасываем таймер, если нет данных ===
|
||||
if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none';
|
||||
if (uiElements.controls.turnTimerSpan) {
|
||||
uiElements.controls.turnTimerSpan.textContent = '--';
|
||||
uiElements.controls.turnTimerSpan.classList.remove('low-time');
|
||||
}
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
return;
|
||||
}
|
||||
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
|
||||
@ -368,313 +281,150 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем, чей сейчас ход по ID слота
|
||||
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
|
||||
// Определяем ID слота оппонента для этого клиента
|
||||
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
|
||||
|
||||
// Обновление панели "моего" персонажа
|
||||
const myStateInGameState = currentGameState[myActualPlayerId];
|
||||
const myBaseStatsForUI = gameDataGlobal.playerBaseStats; // playerBaseStats в gameData - это всегда статы персонажа этого клиента
|
||||
if (myStateInGameState && myBaseStatsForUI) {
|
||||
updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
|
||||
} else {
|
||||
updateFighterPanelUI('player', null, null, true); // Нет данных, показываем состояние ожидания
|
||||
}
|
||||
const myBaseStatsForUI = gameDataGlobal.playerBaseStats;
|
||||
if (myStateInGameState && myBaseStatsForUI) updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
|
||||
else updateFighterPanelUI('player', null, null, true);
|
||||
|
||||
// Обновление панели "моего оппонента"
|
||||
const opponentStateInGameState = currentGameState[opponentActualSlotId];
|
||||
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента
|
||||
|
||||
// Если игра окончена и игрок победил, возможно, панель оппонента уже анимирована на исчезновение.
|
||||
// Не сбрасываем ее opacity/transform здесь, если она в состоянии dissolving.
|
||||
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats;
|
||||
const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving');
|
||||
|
||||
if (opponentStateInGameState && opponentBaseStatsForUI) {
|
||||
// Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это
|
||||
// Но не если она активно в анимации растворения (dissolving)
|
||||
if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || (uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) )) {
|
||||
// console.log("[UI UPDATE DEBUG] Opponent panel not fully visible/dissolving but game not over. Restoring opacity/transform.");
|
||||
const panel = uiElements.opponent.panel;
|
||||
if (panel.classList.contains('dissolving')) {
|
||||
panel.classList.remove('dissolving');
|
||||
panel.style.transition = 'none'; // Отключаем переход временно
|
||||
panel.offsetHeight; // Trigger reflow
|
||||
panel.style.opacity = '1';
|
||||
panel.style.transform = 'scale(1) translateY(0)';
|
||||
panel.style.transition = ''; // Восстанавливаем переход
|
||||
} else {
|
||||
panel.style.opacity = '1';
|
||||
panel.style.transform = 'scale(1) translateY(0)'; // В случае если просто opacity < 1
|
||||
}
|
||||
panel.classList.remove('dissolving'); panel.style.transition = 'none'; panel.offsetHeight;
|
||||
panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; panel.style.transition = '';
|
||||
} else { panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; }
|
||||
} else if (uiElements.opponent.panel && !isOpponentPanelDissolving) {
|
||||
uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные и не растворяется
|
||||
uiElements.opponent.panel.style.opacity = '1';
|
||||
}
|
||||
updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
|
||||
} else {
|
||||
// Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель и очищаем.
|
||||
// Но не сбрасываем opacity/transform, если она активно в анимации растворения
|
||||
if (!isOpponentPanelDissolving) {
|
||||
updateFighterPanelUI('opponent', null, null, false); // Нет данных, показываем состояние ожидания/пустоты
|
||||
} else {
|
||||
// Если панель растворяется, не обновляем ее содержимое и оставляем текущие стили opacity/transform
|
||||
console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
|
||||
}
|
||||
|
||||
if (!isOpponentPanelDissolving) updateFighterPanelUI('opponent', null, null, false);
|
||||
else console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
|
||||
}
|
||||
|
||||
|
||||
// Обновление эффектов
|
||||
updateEffectsUI(currentGameState);
|
||||
|
||||
// Обновление заголовка игры ( Имя1 vs Имя2)
|
||||
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
|
||||
const myName = gameDataGlobal.playerBaseStats.name; // Имя моего персонажа
|
||||
const opponentName = gameDataGlobal.opponentBaseStats.name; // Имя моего оппонента
|
||||
const myKey = gameDataGlobal.playerBaseStats.characterKey;
|
||||
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
||||
|
||||
let myClass = 'title-player';
|
||||
if (myKey === 'elena') myClass = 'title-enchantress';
|
||||
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||
else if (myKey === 'balard') myClass = 'title-knight'; // Вдруг AI Балард в PvP
|
||||
|
||||
let opponentClass = 'title-opponent';
|
||||
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
|
||||
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
|
||||
else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
||||
|
||||
const myName = gameDataGlobal.playerBaseStats.name; const opponentName = gameDataGlobal.opponentBaseStats.name;
|
||||
const myKey = gameDataGlobal.playerBaseStats.characterKey; const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
||||
let myClass = 'title-player'; let opponentClass = 'title-opponent';
|
||||
if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress'; else if (myKey === 'balard') myClass = 'title-knight';
|
||||
if (opponentKey === 'elena') opponentClass = 'title-enchantress'; else if (opponentKey === 'almagest') opponentClass = 'title-sorceress'; else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
||||
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
|
||||
} else if (uiElements.gameHeaderTitle) {
|
||||
// Обновление заголовка в режиме ожидания
|
||||
const myName = gameDataGlobal.playerBaseStats?.name || 'Игрок 1';
|
||||
const myKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||
let myClass = 'title-player';
|
||||
if (myKey === 'elena') myClass = 'title-enchantress';
|
||||
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||
|
||||
const myName = gameDataGlobal.playerBaseStats?.name || 'Игрок 1'; const myKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||
let myClass = 'title-player'; if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Ожидание игрока...</span>`;
|
||||
}
|
||||
|
||||
|
||||
// Управление активностью кнопок и индикатор хода
|
||||
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
|
||||
const isGameActive = !currentGameState.isGameOver;
|
||||
const myCharacterState = currentGameState[myActualPlayerId];
|
||||
|
||||
// Обновление индикатора хода
|
||||
if (uiElements.controls.turnIndicator) {
|
||||
if (isGameActive) {
|
||||
const currentTurnActor = currentGameState.isPlayerTurn ? currentGameState.player : currentGameState.opponent;
|
||||
uiElements.controls.turnIndicator.textContent = `Ход ${currentGameState.turnNumber}: ${currentTurnActor?.name || 'Неизвестно'}`;
|
||||
// Управляем цветом индикатора хода
|
||||
if (currentTurnActor?.id === myActualPlayerId) {
|
||||
uiElements.controls.turnIndicator.style.color = 'var(--turn-color)'; // Свой ход - желтый
|
||||
} else {
|
||||
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)'; // Ход противника - приглушенный
|
||||
}
|
||||
uiElements.controls.turnIndicator.style.color = (currentTurnActor?.id === myActualPlayerId) ? 'var(--turn-color)' : 'var(--text-muted)';
|
||||
} else {
|
||||
uiElements.controls.turnIndicator.textContent = "Игра окончена"; // Или можно скрыть его
|
||||
uiElements.controls.turnIndicator.textContent = "Игра окончена";
|
||||
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Кнопка атаки
|
||||
if (uiElements.controls.buttonAttack) {
|
||||
// Кнопка атаки активна, если это ход этого клиента и игра активна (полное безмолвие не блокирует базовую атаку)
|
||||
// ИСПРАВЛЕНО: Убрана проверка !isFullySilenced из условия disabled для базовой атаки
|
||||
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
||||
|
||||
// Управление классом для подсветки бафнутой атаки
|
||||
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||
let attackBuffId = null;
|
||||
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
|
||||
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
|
||||
|
||||
if (attackBuffId && myCharacterState && myCharacterState.activeEffects) {
|
||||
// Проверяем, есть ли активный "отложенный" бафф (isDelayed=true) на атакующем,
|
||||
// который готов сработать на следующую атаку.
|
||||
const isAttackBuffReady = myCharacterState.activeEffects.some(
|
||||
eff => (eff.id === attackBuffId || eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK)
|
||||
&& eff.isDelayed // Явно проверяем, что это отложенный бафф
|
||||
&& eff.turnsLeft > 0 // Эффект должен еще действовать
|
||||
&& !eff.justCast // Не должен быть наложен в этом ходу, чтобы сработать НА ЭТОМ ходу
|
||||
);
|
||||
|
||||
// Подсветка активна, если бафф готов И это ход этого клиента И игра активна
|
||||
// Подсветка не зависит от безмолвия, т.к. атака возможна и под безмолвием.
|
||||
const isAttackBuffReady = myCharacterState.activeEffects.some(eff => (eff.id === attackBuffId || eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK) && eff.isDelayed && eff.turnsLeft > 0 && !eff.justCast);
|
||||
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
|
||||
} else {
|
||||
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
|
||||
}
|
||||
} else { uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed'); }
|
||||
}
|
||||
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
|
||||
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
|
||||
|
||||
// Кнопки способностей
|
||||
const actingPlayerState = myCharacterState; // Состояние моего персонажа
|
||||
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа (с точки зрения клиента)
|
||||
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; // Имя ресурса моего персонажа
|
||||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние оппонента этого клиента
|
||||
const actingPlayerState = myCharacterState;
|
||||
const actingPlayerAbilities = gameDataGlobal.playerAbilities;
|
||||
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
|
||||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
|
||||
|
||||
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
|
||||
// Получаем актуальное состояние способности из actingPlayerState (которое пришло с сервера)
|
||||
const abilityId = button.dataset.abilityId;
|
||||
const abilityDataFromGameData = actingPlayerAbilities?.find(ab => ab.id === abilityId);
|
||||
|
||||
// Если игра неактивна, нет данных о бойце, способностях или ресурсе, дизейблим кнопку.
|
||||
if (!(button instanceof HTMLButtonElement) || !isGameActive || !canThisClientAct || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !abilityDataFromGameData) {
|
||||
if(button instanceof HTMLButtonElement) button.disabled = true;
|
||||
if (button instanceof HTMLButtonElement) button.disabled = true;
|
||||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
||||
return; // Пропускаем дальнейшую логику обновления кнопки, если она должна быть disabled по базовым условиям
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем условия доступности способности из актуального состояния игры (actingPlayerState)
|
||||
const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost;
|
||||
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0; // Проверяем КД по ID способности из актуального состояния
|
||||
// Под полным безмолвием
|
||||
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0;
|
||||
const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||||
// Под специфическим заглушением этой способности (ищем в disabledAbilities актуального состояния)
|
||||
const isAbilitySpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
|
||||
const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced; // Считается заглушенным, если под полным или специфическим безмолвием
|
||||
// Определяем длительность безмолвия для отображения (берем из специфического, если есть, иначе из полного)
|
||||
const silenceTurnsLeft = isAbilitySpecificallySilenced
|
||||
? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0)
|
||||
: (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
|
||||
|
||||
|
||||
// Нельзя кастовать бафф, если он уже активен (для баффов, которые не стакаются)
|
||||
const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced;
|
||||
const silenceTurnsLeft = isAbilitySpecificallySilenced ? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0) : (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
|
||||
const isBuffAlreadyActive = abilityDataFromGameData.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === abilityId);
|
||||
|
||||
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
|
||||
const isTargetedDebuffAbility = abilityId === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || abilityId === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF;
|
||||
const effectIdForDebuff = 'effect_' + abilityId; // Ищем эффект с префиксом effect_ на цели (оппоненте)
|
||||
const effectIdForDebuff = 'effect_' + abilityId;
|
||||
const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff);
|
||||
|
||||
|
||||
// Кнопка способности активна, если:
|
||||
// - Это ход этого клиента (проверено выше: canThisClientAct)
|
||||
// - Игра активна (проверено выше: isGameActive)
|
||||
// - Достаточно ресурса
|
||||
// - Бафф не активен (если это бафф)
|
||||
// - Не на кулдауне
|
||||
// - Не под безмолвием (полным или специфическим) <--- ЭТО УСЛОВИЕ ОСТАЕТСЯ ДЛЯ СПОСОБНОСТЕЙ
|
||||
// - Дебафф не активен на цели (если это такой дебафф)
|
||||
button.disabled = !hasEnoughResource ||
|
||||
isBuffAlreadyActive ||
|
||||
isSilenced || // Способности БЛОКИРУЮТСЯ полным безмолвием
|
||||
isOnCooldown ||
|
||||
isDebuffAlreadyOnTarget;
|
||||
|
||||
|
||||
// Управление классами для стилизации кнопки (применяются независимо от окончательного disabled состояния)
|
||||
button.disabled = !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDebuffAlreadyOnTarget;
|
||||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
||||
|
||||
if (isOnCooldown) {
|
||||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||
if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[abilityId]}`; cooldownDisplay.style.display = 'block'; }
|
||||
} else if (isSilenced) {
|
||||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
|
||||
if (cooldownDisplay) {
|
||||
const icon = isGenerallySilenced ? '🔕' : '🔇'; // Иконка для полного/частичного безмолвия
|
||||
cooldownDisplay.textContent = `${icon} ${silenceTurnsLeft}`;
|
||||
cooldownDisplay.style.display = 'block';
|
||||
}
|
||||
if (cooldownDisplay) { const icon = isGenerallySilenced ? '🔕' : '🔇'; cooldownDisplay.textContent = `${icon} ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; }
|
||||
} else {
|
||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Скрываем, если нет ни КД, ни безмолвия
|
||||
|
||||
// Добавляем классы для визуальной обратной связи, ЕСЛИ кнопка НЕ задизейблена по КД или Безмолвию
|
||||
// (т.е. эти классы показывают *другие* причины, по которым кнопка может быть disabled)
|
||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
||||
if (!isOnCooldown && !isSilenced) {
|
||||
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource);
|
||||
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive);
|
||||
// Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс для стилизации
|
||||
// button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление title (всплывающей подсказки) - показываем полную информацию
|
||||
// Используем abilityDataFromGameData для базовой информации
|
||||
let titleText = `${abilityDataFromGameData.name} (${abilityDataFromGameData.cost} ${actingPlayerResourceName})`;
|
||||
let descriptionTextFull = abilityDataFromGameData.description; // Используем описание, пришедшее с сервера
|
||||
|
||||
// Если есть функция описания, используем ее
|
||||
let descriptionTextFull = abilityDataFromGameData.description;
|
||||
if (typeof abilityDataFromGameData.descriptionFunction === 'function') {
|
||||
// Передаем конфиг и статы оппонента (цели) для генерации описания
|
||||
const opponentBaseStatsForDesc = gameDataGlobal.opponentBaseStats; // Статы оппонента этого клиента
|
||||
const opponentBaseStatsForDesc = gameDataGlobal.opponentBaseStats;
|
||||
descriptionTextFull = abilityDataFromGameData.descriptionFunction(configGlobal, opponentBaseStatsForDesc);
|
||||
}
|
||||
|
||||
|
||||
if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`;
|
||||
|
||||
// Добавляем информацию об исходном КД из данных способности
|
||||
let abilityBaseCooldown = abilityDataFromGameData.cooldown;
|
||||
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) {
|
||||
titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
|
||||
}
|
||||
|
||||
// Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо
|
||||
if (isOnCooldown) {
|
||||
titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
|
||||
}
|
||||
if (isSilenced) {
|
||||
titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
|
||||
}
|
||||
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
|
||||
if (isOnCooldown) titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
|
||||
if (isSilenced) titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
|
||||
if (isBuffAlreadyActive) {
|
||||
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId); // Ищем активный эффект по ID способности
|
||||
// Если бафф имеет свойство 'justCast' и наложен в этом ходу, он не "готов" сработать на ЭТОМ ходу.
|
||||
// Это может быть важно для тултипа, если нужно отличать "только что наложен" от "готов к следующему действию".
|
||||
// Для "Силы Природы" (isDelayed=true) состояние "активен" означает "готов сработать на следующую атаку".
|
||||
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
|
||||
const isDelayedBuffReady = isBuffAlreadyActive && activeEffect && activeEffect.isDelayed && !activeEffect.justCast && activeEffect.turnsLeft > 0;
|
||||
|
||||
if (isDelayedBuffReady) {
|
||||
titleText += ` | Эффект активен и сработает при следующей базовой атаке (${activeEffect.turnsLeft} х.)`;
|
||||
} else if (isBuffAlreadyActive) {
|
||||
titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}. Нельзя применить повторно.`;
|
||||
}
|
||||
if (isDelayedBuffReady) titleText += ` | Эффект активен и сработает при следующей базовой атаке (${activeEffect.turnsLeft} х.)`;
|
||||
else if (isBuffAlreadyActive) titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}. Нельзя применить повторно.`;
|
||||
}
|
||||
if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) {
|
||||
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
|
||||
titleText += ` | Эффект уже наложен на ${gameDataGlobal.opponentBaseStats?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
|
||||
}
|
||||
if (!hasEnoughResource) {
|
||||
titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.cost})`;
|
||||
}
|
||||
|
||||
|
||||
if (!hasEnoughResource) titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.cost})`;
|
||||
button.setAttribute('title', titleText);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает модальное окно конца игры.
|
||||
* @param {boolean} playerWon - Флаг, выиграл ли игрок, управляющий этим клиентом.
|
||||
* @param {string} [reason=""] - Причина завершения игры.
|
||||
* @param {string|null} opponentCharacterKeyFromClient - Ключ персонажа оппонента с т.з. клиента.
|
||||
* @param {object} [data=null] - Полный объект данных из события gameOver (включает disconnectedCharacterName и т.д.)
|
||||
*/
|
||||
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) {
|
||||
const config = window.GAME_CONFIG || {};
|
||||
const clientSpecificGameData = window.gameData; // Используем gameData, сохраненное в client.js
|
||||
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
|
||||
const clientSpecificGameData = window.gameData;
|
||||
const currentActualGameState = window.gameState;
|
||||
const gameOverScreenElement = uiElements.gameOver.screen;
|
||||
|
||||
console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`);
|
||||
console.log(`[UI.JS DEBUG] captured currentActualGameState?.isGameOver at call time: ${currentActualGameState?.isGameOver}`); // Log state at call time
|
||||
console.log(`[UI.JS DEBUG] Opponent Character Key (from client via param): ${opponentCharacterKeyFromClient}`);
|
||||
console.log(`[UI.JS DEBUG] My Character Name (from window.gameData): ${clientSpecificGameData?.playerBaseStats?.name}`);
|
||||
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
|
||||
console.log(`[UI.JS DEBUG] Full game over data received:`, data);
|
||||
|
||||
|
||||
if (!gameOverScreenElement) {
|
||||
console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found.");
|
||||
return;
|
||||
}
|
||||
if (!gameOverScreenElement) { console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found."); return; }
|
||||
|
||||
const resultMsgElement = uiElements.gameOver.message;
|
||||
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
|
||||
@ -683,144 +433,79 @@
|
||||
if (resultMsgElement) {
|
||||
let winText = `Победа! ${myNameForResult} празднует!`;
|
||||
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
|
||||
// === ИЗМЕНЕНИЕ: Добавляем обработку причины 'turn_timeout' ===
|
||||
if (reason === 'opponent_disconnected') {
|
||||
let disconnectedName = "Противник";
|
||||
// Если в данных gameOver есть имя отключившегося персонажа, используем его
|
||||
if (data && data.disconnectedCharacterName) {
|
||||
disconnectedName = data.disconnectedCharacterName;
|
||||
} else {
|
||||
// Фоллбэк на имя оппонента с точки зрения клиента
|
||||
disconnectedName = opponentNameForResult;
|
||||
}
|
||||
|
||||
let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult;
|
||||
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
|
||||
// В PvP, если оппонент отключился, а текущий игрок проиграл (что странно, но возможно),
|
||||
// сообщение о поражении может быть стандартным или специфичным.
|
||||
// В AI режиме, если игрок отключился, нет формального победителя AI.
|
||||
// Пусть будет стандартное поражение, если playerWon === false
|
||||
if (!playerWon) {
|
||||
// Возможно, специфичный текст для дисконнекта, когда ты проиграл?
|
||||
// loseText = `Игра завершена из-за отключения ${disconnectedName}. Вы проиграли.`
|
||||
} else if (reason === 'turn_timeout') {
|
||||
// Если текущий игрок (чей ход был) проиграл по таймауту
|
||||
if (!playerWon) { // playerWon здесь будет false, если победил оппонент (т.е. мой таймаут)
|
||||
loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`;
|
||||
} else { // Если я победил, потому что у оппонента истекло время
|
||||
winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`;
|
||||
}
|
||||
} else if (reason === 'hp_zero') {
|
||||
// Стандартное завершение по HP - тексты определены выше
|
||||
}
|
||||
// Добавьте обработку других причин завершения, если они будут
|
||||
else {
|
||||
// Неизвестная причина завершения
|
||||
winText = `Игра окончена. Победа! (${reason})`;
|
||||
loseText = `Игра окончена. Поражение. (${reason})`;
|
||||
}
|
||||
|
||||
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
resultMsgElement.textContent = playerWon ? winText : loseText;
|
||||
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
|
||||
}
|
||||
|
||||
const opponentPanelElement = uiElements.opponent.panel;
|
||||
if (opponentPanelElement) {
|
||||
// Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки
|
||||
// и не должна применяться сейчас.
|
||||
opponentPanelElement.classList.remove('dissolving');
|
||||
opponentPanelElement.style.transition = 'none'; // Временно отключаем transition
|
||||
opponentPanelElement.offsetHeight; // Trigger reflow to apply style instantly
|
||||
|
||||
// Используем characterKey проигравшего (переданный из GameInstance),
|
||||
// так как анимация растворения должна быть специфична для проигравшего персонажа,
|
||||
// который может быть Балардом или Альмагест.
|
||||
opponentPanelElement.style.transition = 'none'; opponentPanelElement.offsetHeight;
|
||||
const loserCharacterKeyForDissolve = data?.loserCharacterKey;
|
||||
|
||||
// Применяем анимацию растворения только если игра окончена, игрок победил,
|
||||
// и проигравший был Балардом или Альмагест (у которых есть эта анимация).
|
||||
// Исключаем случай дисконнекта, если анимация должна быть только при "убийстве" по HP.
|
||||
// В текущем CSS анимация растворения не зависит от причины, но зависит от класса 'dissolving'.
|
||||
// Добавляем класс, если игра окончена, игрок победил, и проигравший персонаж - Балард или Альмагест.
|
||||
// Если игра окончена И игрок проиграл И оппонент был Балардом/Альмагест, но игрок проиграл, анимация растворения НЕ применяется к панели оппонента.
|
||||
// Поэтому условие playerWon && ... корректно.
|
||||
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) {
|
||||
// Проверяем, является ли проигравший (т.е. оппонент этого клиента) Балардом или Альмагест
|
||||
if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') {
|
||||
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel.`);
|
||||
opponentPanelElement.classList.add('dissolving');
|
||||
// Убеждаемся, что панель станет полностью прозрачной и сместится после анимации.
|
||||
// Конечные стили (opacity: 0, transform) могут быть заданы в CSS для класса .dissolving,
|
||||
// но их можно также установить здесь после добавления класса для гарантии.
|
||||
opponentPanelElement.style.opacity = '0'; // Конечный стиль для transition
|
||||
// opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Конечный стиль для transition, если нужен
|
||||
opponentPanelElement.style.opacity = '0';
|
||||
} else {
|
||||
console.log(`[UI.JS DEBUG] NOT adding .dissolving (loser key mismatch: ${loserCharacterKeyForDissolve}).`);
|
||||
// Если анимация не применяется, убеждаемся, что панель полностью видна
|
||||
opponentPanelElement.style.opacity = '1';
|
||||
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||
opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||
}
|
||||
} else {
|
||||
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}.`);
|
||||
// Если игра не окончена или игрок проиграл, убеждаемся, что панель полностью видна
|
||||
opponentPanelElement.style.opacity = '1';
|
||||
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||
opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||
}
|
||||
opponentPanelElement.style.transition = ''; // Восстанавливаем transition после установки начальных/конечных стилей
|
||||
|
||||
|
||||
opponentPanelElement.style.transition = '';
|
||||
}
|
||||
|
||||
// Показываем модальное окно конца игры с небольшой задержкой
|
||||
// Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState в момент срабатывания setTimeout
|
||||
setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { // Use distinct names in timeout
|
||||
console.log("[UI.JS DEBUG] Timeout callback fired for showGameOver.");
|
||||
console.log("[UI.JS DEBUG] State object received in timeout:", finalStateInTimeout); // Check the whole object
|
||||
console.log("[UI.JS DEBUG] isGameOver in state (TIMEOUT):", finalStateInTimeout?.isGameOver); // Check property
|
||||
console.log("[UI.JS DEBUG] playerWon flag (TIMEOUT):", wonInTimeout); // Check playerWon flag passed
|
||||
|
||||
|
||||
// Проверяем условия для показа модального окна: элемент существует И состояние игры помечено как оконченное
|
||||
// ИСПРАВЛЕНО: Убрана проверка gameOverScreenElement.offsetParent !== null
|
||||
setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => {
|
||||
if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) {
|
||||
console.log(`[UI.JS DEBUG] Modal SHOW condition met: gameOverScreenElement exists, finalState exists, isGameOver is true.`);
|
||||
// Убеждаемся, что modal не имеет display: none перед запуском transition opacity
|
||||
if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
|
||||
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
|
||||
}
|
||||
// Применяем display: flex (или другой нужный) только один раз, если нужно
|
||||
if(window.getComputedStyle(gameOverScreenElement).display === 'none') {
|
||||
gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal
|
||||
}
|
||||
gameOverScreenElement.style.opacity = '0'; // Start from hidden opacity
|
||||
|
||||
if(window.getComputedStyle(gameOverScreenElement).display === 'none') gameOverScreenElement.style.display = 'flex';
|
||||
gameOverScreenElement.style.opacity = '0';
|
||||
requestAnimationFrame(() => {
|
||||
console.log("[UI.JS DEBUG] RequestAnimationFrame callback fired, animating modal.");
|
||||
// Animate to visible
|
||||
gameOverScreenElement.style.opacity = '1';
|
||||
if (uiElements.gameOver.modalContent) {
|
||||
uiElements.gameOver.modalContent.style.transition = 'transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out'; // Убеждаемся, что transition включен
|
||||
uiElements.gameOver.modalContent.style.transition = 'transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out';
|
||||
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
|
||||
uiElements.gameOver.modalContent.style.opacity = '1';
|
||||
// uiElements.gameOver.modalContent.style.transition = ''; // Можно и так, если не отключали ранее
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`[UI.JS DEBUG] Modal SHOW condition NOT met.`);
|
||||
console.log(`[UI.JS DEBUG] Details: gameOverScreenElement=${!!gameOverScreenElement}, finalState=${!!finalStateInTimeout}, finalState?.isGameOver=${finalStateInTimeout?.isGameOver}. Hiding modal.`); // More details
|
||||
// Убеждаемся, что модалка скрыта, если условия не выполняются
|
||||
if (gameOverScreenElement) {
|
||||
// Ensure transition is off when hiding instantly
|
||||
gameOverScreenElement.style.transition = 'none';
|
||||
if (uiElements.gameOver.modalContent) uiElements.gameOver.modalContent.style.transition = 'none';
|
||||
|
||||
gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
|
||||
gameOverScreenElement.style.opacity = '0';
|
||||
if (uiElements.gameOver.modalContent) {
|
||||
uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
|
||||
uiElements.gameOver.modalContent.style.opacity = '0';
|
||||
}
|
||||
// Trigger reflow to ensure transition is off before hiding
|
||||
gameOverScreenElement.offsetHeight;
|
||||
}
|
||||
}
|
||||
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // Pass captured state and other values
|
||||
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data);
|
||||
}
|
||||
|
||||
|
||||
// Экспортируем функции UI для использования в client.js
|
||||
window.gameUI = { uiElements, addToLog, updateUI, showGameOver };
|
||||
window.gameUI = {
|
||||
uiElements,
|
||||
addToLog,
|
||||
updateUI,
|
||||
showGameOver,
|
||||
// === ИЗМЕНЕНИЕ: Экспортируем функцию обновления таймера ===
|
||||
updateTurnTimerDisplay
|
||||
// === КОНЕЦ ИЗМЕНЕНИЯ ===
|
||||
};
|
||||
})();
|
@ -75,6 +75,12 @@
|
||||
|
||||
/* Фиксированная высота лог-панели (для десктопа) */
|
||||
--log-panel-fixed-height: 280px;
|
||||
|
||||
/* === ИЗМЕНЕНИЕ: Переменные для таймера === */
|
||||
--timer-text-color: var(--turn-color); /* Цвет текста таймера (золотой) */
|
||||
--timer-icon-color: #b0c4de; /* Цвет иконки таймера (светло-голубой) */
|
||||
--timer-low-time-color: var(--damage-color); /* Цвет текста, когда времени мало (красный) */
|
||||
/* === КОНЕЦ ИЗМЕНЕНИЯ === */
|
||||
}
|
||||
|
||||
/* --- Базовые Стили и Сброс --- */
|
||||
@ -442,21 +448,24 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Стили для имен персонажей в заголовке */
|
||||
.title-enchantress {
|
||||
.title-enchantress { /* Елена */
|
||||
color: var(--accent-player);
|
||||
}
|
||||
|
||||
/* Елена */
|
||||
.title-knight {
|
||||
.title-knight { /* Балард */
|
||||
color: var(--accent-opponent);
|
||||
}
|
||||
|
||||
/* Балард */
|
||||
.title-sorceress {
|
||||
.title-sorceress { /* Альмагест */
|
||||
color: var(--accent-almagest);
|
||||
}
|
||||
/* Общие классы для заголовка, если будут использоваться из JS для динамической смены */
|
||||
.title-player { /* Игрок 1 (может быть Елена или Альмагест) */
|
||||
/* Динамически устанавливается цвет через JS или более специфичные классы выше */
|
||||
}
|
||||
.title-opponent { /* Игрок 2 (может быть Балард, Елена или Альмагест) */
|
||||
/* Динамически устанавливается цвет через JS или более специфичные классы выше */
|
||||
}
|
||||
|
||||
|
||||
/* Альмагест */
|
||||
.separator i {
|
||||
color: var(--text-light);
|
||||
font-size: 0.8em;
|
||||
@ -499,11 +508,9 @@ label[for="char-almagest"] i {
|
||||
.fighter-panel.panel-elena {
|
||||
border-color: var(--accent-player);
|
||||
}
|
||||
|
||||
.fighter-panel.panel-almagest {
|
||||
border-color: var(--accent-almagest);
|
||||
}
|
||||
|
||||
.fighter-panel.panel-balard {
|
||||
border-color: var(--accent-opponent);
|
||||
}
|
||||
@ -528,17 +535,17 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Цвета иконок в имени персонажа */
|
||||
.fighter-name .icon-player {
|
||||
color: var(--accent-player);
|
||||
.fighter-name .icon-player { /* Общая иконка для слота игрока (может быть Елена или Альмагест) */
|
||||
/* Цвет будет установлен специфичным классом .icon-elena или .icon-almagest */
|
||||
}
|
||||
.fighter-name .icon-opponent { /* Общая иконка для слота оппонента (может быть Балард, Елена или Альмагест) */
|
||||
/* Цвет будет установлен специфичным классом */
|
||||
}
|
||||
/* Специфичные иконки */
|
||||
.fighter-name .icon-elena { color: var(--accent-player); }
|
||||
.fighter-name .icon-almagest { color: var(--accent-almagest); }
|
||||
.fighter-name .icon-balard { color: var(--accent-opponent); }
|
||||
|
||||
.fighter-name .icon-opponent {
|
||||
color: var(--accent-opponent);
|
||||
}
|
||||
|
||||
.fighter-name .icon-almagest {
|
||||
color: var(--accent-almagest);
|
||||
}
|
||||
|
||||
.character-visual {
|
||||
flex-shrink: 0;
|
||||
@ -555,17 +562,9 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Стили рамок аватаров (для JS) */
|
||||
.avatar-image.avatar-elena {
|
||||
border-color: var(--accent-player);
|
||||
}
|
||||
|
||||
.avatar-image.avatar-almagest {
|
||||
border-color: var(--accent-almagest);
|
||||
}
|
||||
|
||||
.avatar-image.avatar-balard {
|
||||
border-color: var(--accent-opponent);
|
||||
}
|
||||
.avatar-image.avatar-elena { border-color: var(--accent-player); }
|
||||
.avatar-image.avatar-almagest { border-color: var(--accent-almagest); }
|
||||
.avatar-image.avatar-balard { border-color: var(--accent-opponent); }
|
||||
|
||||
|
||||
.panel-content {
|
||||
@ -593,21 +592,10 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Цвета иконок ресурсов (для JS) */
|
||||
.stat-bar-container.health .bar-icon {
|
||||
color: var(--hp-color);
|
||||
}
|
||||
|
||||
.stat-bar-container.mana .bar-icon {
|
||||
color: var(--mana-color);
|
||||
}
|
||||
|
||||
.stat-bar-container.stamina .bar-icon {
|
||||
color: var(--stamina-color);
|
||||
}
|
||||
|
||||
.stat-bar-container.dark-energy .bar-icon {
|
||||
color: var(--dark-energy-color);
|
||||
}
|
||||
.stat-bar-container.health .bar-icon { color: var(--hp-color); }
|
||||
.stat-bar-container.mana .bar-icon { color: var(--mana-color); }
|
||||
.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
|
||||
.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
|
||||
|
||||
|
||||
.bar-wrapper {
|
||||
@ -653,21 +641,10 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Цвета Заливки Баров */
|
||||
.health .bar-fill {
|
||||
background-color: var(--hp-color);
|
||||
}
|
||||
|
||||
.mana .bar-fill {
|
||||
background-color: var(--mana-color);
|
||||
}
|
||||
|
||||
.stamina .bar-fill {
|
||||
background-color: var(--stamina-color);
|
||||
}
|
||||
|
||||
.dark-energy .bar-fill {
|
||||
background-color: var(--dark-energy-color);
|
||||
}
|
||||
.health .bar-fill { background-color: var(--hp-color); }
|
||||
.mana .bar-fill { background-color: var(--mana-color); }
|
||||
.stamina .bar-fill { background-color: var(--stamina-color); }
|
||||
.dark-energy .bar-fill { background-color: var(--dark-energy-color); }
|
||||
|
||||
|
||||
/* Статус и Эффекты */
|
||||
@ -735,13 +712,8 @@ label[for="char-almagest"] i {
|
||||
text-align: center; /* Для иконок */
|
||||
}
|
||||
|
||||
.effect-category .icon-effects-buff {
|
||||
color: var(--heal-color);
|
||||
}
|
||||
|
||||
.effect-category .icon-effects-debuff {
|
||||
color: var(--damage-color);
|
||||
}
|
||||
.effect-category .icon-effects-buff { color: var(--heal-color); }
|
||||
.effect-category .icon-effects-debuff { color: var(--damage-color); }
|
||||
|
||||
.effect-list {
|
||||
display: inline;
|
||||
@ -765,32 +737,11 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Цвета рамок и текста для разных типов эффектов */
|
||||
.effect-buff {
|
||||
border-color: var(--heal-color);
|
||||
color: var(--heal-color);
|
||||
}
|
||||
|
||||
.effect-debuff {
|
||||
border-color: var(--damage-color);
|
||||
color: var(--damage-color);
|
||||
}
|
||||
|
||||
.effect-stun {
|
||||
border-color: var(--turn-color);
|
||||
color: var(--turn-color);
|
||||
}
|
||||
|
||||
/* Для безмолвия/стана */
|
||||
.effect-block {
|
||||
border-color: var(--block-color);
|
||||
color: var(--block-color);
|
||||
}
|
||||
|
||||
/* Для эффектов блока */
|
||||
.effect-info {
|
||||
border-color: var(--text-muted);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.effect-buff { border-color: var(--heal-color); color: var(--heal-color); }
|
||||
.effect-debuff { border-color: var(--damage-color); color: var(--damage-color); }
|
||||
.effect-stun { border-color: var(--turn-color); color: var(--turn-color); } /* Для безмолвия/стана */
|
||||
.effect-block { border-color: var(--block-color); color: var(--block-color); } /* Для эффектов блока */
|
||||
.effect-info { border-color: var(--text-muted); color: var(--text-muted); }
|
||||
|
||||
|
||||
/* --- Панель Управления --- */
|
||||
@ -805,12 +756,46 @@ label[for="char-almagest"] i {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 10px; /* Базовый отступ */
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* === ИЗМЕНЕНИЕ: Стили для таймера хода === */
|
||||
.turn-timer-display {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: var(--timer-text-color);
|
||||
margin-top: -5px; /* Небольшой отрицательный отступ, чтобы быть ближе к индикатору хода */
|
||||
margin-bottom: 10px; /* Отступ снизу перед кнопками */
|
||||
padding: 5px;
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
border-radius: 4px;
|
||||
border-top: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.turn-timer-display i {
|
||||
color: var(--timer-icon-color);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#turn-timer { /* Сам текст таймера */
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
min-width: 35px; /* Чтобы не прыгал layout при смене 0:0X на -- */
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#turn-timer.low-time { /* Класс для стилизации, когда времени мало */
|
||||
color: var(--timer-low-time-color);
|
||||
animation: pulse-timer-warning 1s infinite ease-in-out;
|
||||
}
|
||||
/* === КОНЕЦ ИЗМЕНЕНИЯ === */
|
||||
|
||||
|
||||
.controls-layout {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@ -1119,26 +1104,10 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Стили для типов логов (классы добавляются JS) */
|
||||
.log-damage {
|
||||
color: var(--damage-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-heal {
|
||||
color: var(--heal-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-block {
|
||||
color: var(--block-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #b0c4de;
|
||||
}
|
||||
|
||||
/* Светло-голубой для общей информации */
|
||||
.log-damage { color: var(--damage-color); font-weight: 500; }
|
||||
.log-heal { color: var(--heal-color); font-weight: 500; }
|
||||
.log-block { color: var(--block-color); font-style: italic; }
|
||||
.log-info { color: #b0c4de; } /* Светло-голубой для общей информации */
|
||||
.log-turn {
|
||||
font-weight: bold;
|
||||
color: var(--turn-color);
|
||||
@ -1148,14 +1117,12 @@ label[for="char-almagest"] i {
|
||||
font-size: 1.05em;
|
||||
display: block; /* Чтобы занимал всю строку */
|
||||
}
|
||||
|
||||
.log-system {
|
||||
font-weight: bold;
|
||||
color: var(--system-color);
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.log-effect {
|
||||
font-style: italic;
|
||||
color: var(--effect-color);
|
||||
@ -1264,14 +1231,18 @@ label[for="char-almagest"] i {
|
||||
|
||||
/* Анимация пульсации красной рамки (для нехватки ресурса) */
|
||||
@keyframes pulse-red-border {
|
||||
0%, 100% {
|
||||
border-color: var(--damage-color);
|
||||
}
|
||||
50% {
|
||||
border-color: #ffb3b3;
|
||||
}
|
||||
0%, 100% { border-color: var(--damage-color); }
|
||||
50% { border-color: #ffb3b3; }
|
||||
}
|
||||
|
||||
/* === ИЗМЕНЕНИЕ: Анимация для таймера, когда времени мало === */
|
||||
@keyframes pulse-timer-warning {
|
||||
0%, 100% { color: var(--timer-low-time-color); transform: scale(1); }
|
||||
50% { color: #ff6347; transform: scale(1.05); } /* Томатный цвет для пульсации */
|
||||
}
|
||||
/* === КОНЕЦ ИЗМЕНЕНИЯ === */
|
||||
|
||||
|
||||
/* Анимация вспышки при касте (добавляется JS классом) */
|
||||
@keyframes flash-effect {
|
||||
0%, 100% {
|
||||
@ -1291,61 +1262,40 @@ label[for="char-almagest"] i {
|
||||
}
|
||||
|
||||
/* Применение анимации каста к панели (добавляется через JS) */
|
||||
/* Пример: #player-panel.is-casting-heal */
|
||||
[class*="is-casting-"] {
|
||||
animation: flash-effect var(--cast-duration) ease-out;
|
||||
/* Сохраняем исходные значения для возврата в keyframes */
|
||||
/* JS должен будет установить --initial-box-shadow и --initial-border-color */
|
||||
/* Или, определяем их здесь для известных панелей */
|
||||
}
|
||||
|
||||
/* Цвета для разных кастов (переменные для keyframes flash-effect) */
|
||||
#player-panel.is-casting-heal, #opponent-panel.is-casting-heal {
|
||||
--flash-color-outer: rgba(144, 238, 144, 0.7);
|
||||
--flash-color-inner: rgba(144, 238, 144, 0.4);
|
||||
--flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4);
|
||||
--flash-border-color: var(--heal-color);
|
||||
--initial-border-color: var(--accent-player); /* Для панели игрока Елена */
|
||||
}
|
||||
#player-panel.is-casting-fireball, #opponent-panel.is-casting-fireball {
|
||||
--flash-color-outer: rgba(255, 100, 100, 0.7);
|
||||
--flash-color-inner: rgba(255, 100, 100, 0.4);
|
||||
--flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4);
|
||||
--flash-border-color: var(--damage-color);
|
||||
--initial-border-color: var(--accent-player); /* Для панели игрока Елена */
|
||||
}
|
||||
/* Пример для Альмагест */
|
||||
#player-panel.is-casting-shadowBolt, #opponent-panel.is-casting-shadowBolt {
|
||||
--flash-color-outer: rgba(138, 43, 226, 0.6);
|
||||
--flash-color-inner: rgba(138, 43, 226, 0.3);
|
||||
#player-panel.is-casting-shadowBolt, #opponent-panel.is-casting-shadowBolt { /* Для Альмагест */
|
||||
--flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3);
|
||||
--flash-border-color: var(--dark-energy-color);
|
||||
--initial-border-color: var(--accent-almagest); /* Для панели игрока Альмагест */
|
||||
}
|
||||
/* Пример для Баларда (если он когда-либо будет кастовать с анимацией) */
|
||||
#opponent-panel.is-casting-darkPatronage {
|
||||
--flash-color-outer: rgba(100, 100, 100, 0.7);
|
||||
--flash-color-inner: rgba(100, 100, 100, 0.4);
|
||||
--flash-border-color: #888;
|
||||
--initial-border-color: var(--accent-opponent); /* Для панели оппонента Балард */
|
||||
}
|
||||
/* Добавить для других способностей и персонажей */
|
||||
/* Пример:
|
||||
#player-panel.is-casting-naturesStrength { ... }
|
||||
#opponent-panel.is-casting-darkPatronage { ... }
|
||||
*/
|
||||
|
||||
|
||||
/* Анимация тряски при получении урона */
|
||||
@keyframes shake-opponent {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
transform: translateX(-4px) rotate(-0.5deg);
|
||||
}
|
||||
20%, 40%, 60%, 80% {
|
||||
transform: translateX(4px) rotate(0.5deg);
|
||||
}
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) rotate(-0.5deg); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(4px) rotate(0.5deg); }
|
||||
}
|
||||
|
||||
/* Применение анимации тряски к панели противника (добавляется JS классом) */
|
||||
#opponent-panel.is-shaking {
|
||||
animation: shake-opponent var(--shake-duration) cubic-bezier(.36, .07, .19, .97) both;
|
||||
/* Дополнительные свойства для лучшей производительности анимации */
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
@ -1353,32 +1303,19 @@ label[for="char-almagest"] i {
|
||||
|
||||
/* Анимация растворения панели противника (добавляется JS классом) */
|
||||
#opponent-panel.dissolving {
|
||||
/* Начальные стили перед анимацией задаются JS, конечные - здесь */
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
/* Длительность анимации берется из переменной */
|
||||
transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in;
|
||||
pointer-events: none; /* Нельзя взаимодействовать во время исчезновения */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Состояние после завершения анимации dissolving, если класс остается */
|
||||
/* #opponent-panel.dissolved-state { opacity: 0; transform: scale(0.9) translateY(20px); } */
|
||||
|
||||
|
||||
/* Анимация короткой тряски (например, при промахе?) */
|
||||
@keyframes shake-short {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-3px); }
|
||||
50% { transform: translateX(3px); }
|
||||
75% { transform: translateX(-3px); }
|
||||
}
|
||||
|
||||
.shake-short {
|
||||
@ -1389,296 +1326,80 @@ label[for="char-almagest"] i {
|
||||
/* --- Отзывчивость (Медиа-запросы) --- */
|
||||
@media (max-width: 900px) {
|
||||
body {
|
||||
height: auto; /* Позволяем body расти по контенту */
|
||||
overflow-y: auto; /* Включаем прокрутку для body, если нужно */
|
||||
padding: 5px 0; /* Уменьшаем отступы */
|
||||
font-size: 15px;
|
||||
justify-content: flex-start; /* Чтобы контент не пытался всегда быть по центру */
|
||||
height: auto; overflow-y: auto;
|
||||
padding: 5px 0; font-size: 15px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.auth-game-setup-wrapper {
|
||||
max-height: none; /* Убираем ограничение по высоте, body будет скроллиться */
|
||||
}
|
||||
|
||||
.game-wrapper {
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.battle-arena-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.player-column,
|
||||
.opponent-column {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.fighter-panel,
|
||||
.controls-panel-new,
|
||||
.battle-log-new {
|
||||
min-height: auto; /* Убираем min-height, пусть контент определяет */
|
||||
height: auto; /* Высота по контенту */
|
||||
padding: 10px;
|
||||
flex-grow: 0; /* Панели не должны растягиваться */
|
||||
flex-shrink: 1; /* Но могут сжиматься, если нужно */
|
||||
}
|
||||
|
||||
.controls-panel-new {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Сохраняем для удобства клика */
|
||||
.battle-log-new {
|
||||
height: auto;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* Лог тоже по контенту */
|
||||
#log-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* Ограничиваем высоту списка логов */
|
||||
.abilities-grid {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.abilities-grid::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Убираем псевдоэлемент, т.к. нет скролла */
|
||||
.ability-list,
|
||||
.controls-layout {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.fighter-name {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Восстанавливаем отступ, если был убран */
|
||||
.stat-bar-container .bar-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.effects-area,
|
||||
.effect {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
#turn-indicator {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.action-button.basic {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.abilities-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ability-button {
|
||||
font-size: 0.75em;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.ability-button .ability-name {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ability-button .ability-desc {
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 25px 30px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal-content h2#result-message {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.modal-action-button {
|
||||
font-size: 1em;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
/* Адаптируем кнопку в модалке */
|
||||
/* Стили для auth-game-setup на планшетах */
|
||||
#game-setup {
|
||||
max-width: 95%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#game-setup h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
#game-setup h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#game-setup button {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#game-setup input[type="text"] {
|
||||
width: calc(100% - 90px);
|
||||
max-width: 200px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#available-games-list {
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.character-selection label {
|
||||
margin: 0 10px;
|
||||
font-size: 1em;
|
||||
.auth-game-setup-wrapper { max-height: none; }
|
||||
.game-wrapper { padding: 5px; gap: 5px; height: auto; }
|
||||
.game-header h1 { font-size: 1.5em; }
|
||||
.battle-arena-container { flex-direction: column; height: auto; overflow: visible; }
|
||||
.player-column, .opponent-column { width: 100%; height: auto; overflow: visible; }
|
||||
.fighter-panel, .controls-panel-new, .battle-log-new {
|
||||
min-height: auto; height: auto; padding: 10px;
|
||||
flex-grow: 0; flex-shrink: 1;
|
||||
}
|
||||
.controls-panel-new { min-height: 200px; }
|
||||
.battle-log-new { height: auto; min-height: 150px; }
|
||||
#log-list { max-height: 200px; }
|
||||
.abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; }
|
||||
.abilities-grid::after { display: none; }
|
||||
.ability-list, .controls-layout { overflow: visible; }
|
||||
.fighter-name { font-size: 1.3em; }
|
||||
.panel-content { margin-top: 10px; }
|
||||
.stat-bar-container .bar-icon { font-size: 1.2em; }
|
||||
.bar { height: 18px; }
|
||||
.effects-area, .effect { font-size: 0.85em; }
|
||||
#turn-indicator { font-size: 1.2em; margin-bottom: 8px; } /* Уменьшен отступ, т.к. есть таймер */
|
||||
/* === ИЗМЕНЕНИЕ: Адаптивность таймера === */
|
||||
.turn-timer-display { font-size: 0.85em; margin-bottom: 8px; padding: 4px; }
|
||||
#turn-timer { font-size: 1em; }
|
||||
/* === КОНЕЦ ИЗМЕНЕНИЯ === */
|
||||
.action-button.basic { font-size: 0.8em; padding: 8px 4px; }
|
||||
.abilities-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; padding: 8px; }
|
||||
.ability-button { font-size: 0.75em; padding: 5px; }
|
||||
.ability-button .ability-name { margin-bottom: 2px; }
|
||||
.ability-button .ability-desc { font-size: 0.65em; }
|
||||
.modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
|
||||
.modal-content h2#result-message { font-size: 1.8em; }
|
||||
.modal-action-button { font-size: 1em; padding: 10px 20px; }
|
||||
#game-setup { max-width: 95%; padding: 15px; }
|
||||
#game-setup h2 { font-size: 1.6em; }
|
||||
#game-setup h3 { font-size: 1.1em; }
|
||||
#game-setup button { padding: 8px 12px; font-size: 0.9em; }
|
||||
#game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px; }
|
||||
#available-games-list { max-height: 180px; }
|
||||
.character-selection label { margin: 0 10px; font-size: 1em; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.fighter-name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.abilities-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(65px, 1fr));
|
||||
gap: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.ability-button {
|
||||
font-size: 0.7em;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.ability-button .ability-name {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.ability-button .ability-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Скрываем описание на маленьких экранах */
|
||||
#log-list {
|
||||
font-size: 0.8em;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content h2#result-message {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.modal-action-button {
|
||||
font-size: 0.9em;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* Адаптируем кнопку в модалке */
|
||||
/* Стили для auth-game-setup на мобильных */
|
||||
.auth-game-setup-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#game-setup {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#game-setup h2 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
#game-setup button {
|
||||
padding: 7px 10px;
|
||||
font-size: 0.85em;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
body { font-size: 14px; }
|
||||
.game-header h1 { font-size: 1.3em; }
|
||||
.fighter-name { font-size: 1.2em; }
|
||||
.abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; }
|
||||
.ability-button { font-size: 0.7em; padding: 4px; }
|
||||
.ability-button .ability-name { margin-bottom: 1px; }
|
||||
.ability-button .ability-desc { display: none; }
|
||||
#log-list { font-size: 0.8em; max-height: 150px; }
|
||||
.modal-content { padding: 20px; }
|
||||
.modal-content h2#result-message { font-size: 1.6em; }
|
||||
.modal-action-button { font-size: 0.9em; padding: 8px 16px; }
|
||||
.auth-game-setup-wrapper { padding: 15px; }
|
||||
#game-setup { padding: 10px; }
|
||||
#game-setup h2 { font-size: 1.4em; }
|
||||
#game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; }
|
||||
#game-setup input[type="text"],
|
||||
#game-setup button {
|
||||
/* Делаем кнопки и инпуты в game-setup блочными для лучшего отображения на мобильных */
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#game-setup input[type="text"] {
|
||||
max-width: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
#game-setup div>input[type="text"]+button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Отступ для кнопки после инпута */
|
||||
#available-games-list {
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
#available-games-list li button {
|
||||
font-size: 0.75em;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.character-selection {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.character-selection label {
|
||||
margin: 0 5px 5px 5px;
|
||||
font-size: 0.9em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Лейблы в столбик */
|
||||
.character-selection label i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#game-setup button { display: block; width: 100%; margin-left: 0; margin-right: 0; }
|
||||
#game-setup input[type="text"] { max-width: none; margin-bottom: 10px; }
|
||||
#game-setup div>input[type="text"]+button { margin-top: 5px; }
|
||||
#available-games-list { max-height: 120px; }
|
||||
#available-games-list li button { font-size: 0.75em; padding: 5px 8px; }
|
||||
.character-selection { padding: 10px; }
|
||||
.character-selection label { margin: 0 5px 5px 5px; font-size: 0.9em; display: block; }
|
||||
.character-selection label i { margin-right: 5px; }
|
||||
/* === ИЗМЕНЕНИЕ: Адаптивность таймера для мобильных === */
|
||||
#turn-indicator { font-size: 1.1em; }
|
||||
.turn-timer-display { font-size: 0.8em; margin-top: -3px; margin-bottom: 6px; }
|
||||
#turn-timer { font-size: 0.95em; }
|
||||
/* === КОНЕЦ ИЗМЕНЕНИЯ === */
|
||||
}
|
@ -25,6 +25,11 @@ const GAME_CONFIG = {
|
||||
// BALARD_BLEED_DURATION: 2,
|
||||
// BALARD_BLEED_COOLDOWN: 3,
|
||||
|
||||
// --- Таймер Хода ---
|
||||
TURN_DURATION_SECONDS: 60, // Длительность хода в секундах
|
||||
TURN_DURATION_MS: 60 * 1000, // Длительность хода в миллисекундах
|
||||
TIMER_UPDATE_INTERVAL_MS: 1000, // Интервал обновления таймера на клиенте (в мс)
|
||||
|
||||
// --- Идентификаторы и Типы ---
|
||||
PLAYER_ID: 'player', // Технический идентификатор для слота 'Игрок 1'
|
||||
OPPONENT_ID: 'opponent', // Технический идентификатор для слота 'Игрок 2' / 'Противник'
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,115 +1,56 @@
|
||||
// /server_modules/gameManager.js
|
||||
const { v4: uuidv4 } = require('uuid'); // Убедитесь, что uuidv4 установлен: npm install uuid
|
||||
const GameInstance = require('./gameInstance'); // Убедитесь, что GameInstance экспортируется из gameInstance.js
|
||||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient и данных персонажей
|
||||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const GameInstance = require('./gameInstance');
|
||||
const gameData = require('./data');
|
||||
const GAME_CONFIG = require('./config');
|
||||
|
||||
class GameManager {
|
||||
constructor(io) {
|
||||
this.io = io; // Ссылка на Socket.IO сервер для широковещательных рассылок
|
||||
this.games = {}; // { gameId: GameInstance } - Все активные или ожидающие игры
|
||||
this.userIdentifierToGameId = {}; // { userId|socketId: gameId } - Какому пользователю какая игра соответствует (более стабильно, чем socket.id)
|
||||
this.pendingPvPGames = []; // [gameId] - ID PvP игр, ожидающих второго игрока
|
||||
|
||||
// Навешиваем обработчик события 'gameOver' на Socket.IO сервер
|
||||
// Это событие исходит от экземпляра GameInstance при завершении игры (по HP или дисконнекту)
|
||||
// Мы слушаем его здесь, чтобы GameManager мог очистить ссылки.
|
||||
// Примечание: Это событие отправляется всем в комнате игры. GameManager слушает его через io.sockets.sockets.on,
|
||||
// но удобнее слушать его на уровне io, если возможно, или добавить специальный emit из GameInstance.
|
||||
// Текущая архитектура (GameInstance напрямую вызывает io.to(...).emit('gameOver', ...)) уже рабочая.
|
||||
// GameManager сам должен отреагировать на завершение, проверяя gameState.isGameOver после каждого действия/хода.
|
||||
// Или GameInstance должен вызвать специальный метод GameManager при gameOver.
|
||||
// Давайте сделаем GameInstance вызывать метод GameManager при gameOver.
|
||||
this.io = io;
|
||||
this.games = {}; // { gameId: GameInstance }
|
||||
this.userIdentifierToGameId = {}; // { userId|socketId: gameId }
|
||||
this.pendingPvPGames = []; // [gameId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет предыдущую ожидающую игру пользователя, если таковая существует.
|
||||
* Это предотвращает создание множества пустых игр одним пользователем.
|
||||
* @param {string} currentSocketId - ID текущего сокета.
|
||||
* @param {string|number} identifier - userId или socketId пользователя.
|
||||
* @param {string|null} excludeGameId - ID игры, которую НЕ нужно удалять (например, если пользователь присоединяется к своей же игре).
|
||||
*/
|
||||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||||
// Ищем игру по идентификатору пользователя
|
||||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||||
|
||||
// Проверяем, что нашли игру, она не исключена, и она все еще существует в списке игр
|
||||
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||||
const gameToRemove = this.games[oldPendingGameId];
|
||||
// Проверяем, что игра является ожидающей PvP игрой с одним игроком
|
||||
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||
// Проверяем, что этот пользователь является владельцем этой ожидающей игры
|
||||
// Владелец в pendingPvPGames - это всегда тот, кто ее создал (первый игрок в слоте PLAYER_ID)
|
||||
const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
|
||||
// Проверяем, что владелец игры существует и его идентификатор совпадает
|
||||
if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) {
|
||||
console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
||||
|
||||
// Используем централизованную функцию очистки
|
||||
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game');
|
||||
|
||||
// Оповещаем клиентов об обновленном списке игр (уже внутри _cleanupGame)
|
||||
// this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else {
|
||||
// Если игра не соответствует критериям ожидающей игры, но идентификатор был связан с ней,
|
||||
// это может означать, что игра уже началась или была завершена.
|
||||
// Просто очищаем ссылку, если она не ведет в исключенную игру.
|
||||
// Идентификатор должен был быть очищен из userIdentifierToGameId при старте или завершении игры.
|
||||
// На всякий случай убеждаемся, что мы не удаляем ссылку на игру, к которой только что присоединились.
|
||||
if (this.userIdentifierToGameId[identifier] !== excludeGameId) {
|
||||
console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если oldPendingGameId не найдена, или она равна excludeGameId, ничего не делаем.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Создает новую игру.
|
||||
* @param {object} socket - Сокет игрока, создающего игру.
|
||||
* @param {string} [mode='ai'] - Режим игры ('ai' или 'pvp').
|
||||
* @param {string} [chosenCharacterKey='elena'] - Выбранный персонаж для первого игрока в PvP.
|
||||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||
*/
|
||||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
||||
// Удаляем старые ожидающие игры этого пользователя, прежде чем создавать новую
|
||||
this._removePreviousPendingGames(socket.id, identifier);
|
||||
|
||||
// Проверяем, не находится ли пользователь уже в какой-то игре (активной или ожидающей)
|
||||
// Проверяем наличие ссылки на игру по идентификатору пользователя
|
||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`);
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||
// Можно попробовать отправить состояние текущей игры пользователю
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const gameId = uuidv4(); // Генерируем уникальный ID для игры
|
||||
// Передаем ссылку на GameManager в GameInstance, чтобы он мог вызвать _notifyGameEnded
|
||||
const game = new GameInstance(gameId, this.io, mode, this); // <-- ПЕРЕДАЕМ GameManager
|
||||
game.ownerIdentifier = identifier; // Сохраняем идентификатор создателя
|
||||
this.games[gameId] = game; // Добавляем игру в список активных игр
|
||||
|
||||
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали при создании
|
||||
const gameId = uuidv4();
|
||||
const game = new GameInstance(gameId, this.io, mode, this);
|
||||
game.ownerIdentifier = identifier;
|
||||
this.games[gameId] = game;
|
||||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||||
|
||||
// Добавляем игрока в созданный экземпляр игры, передавая идентификатор
|
||||
// GameInstance.addPlayer принимает socket, chosenCharacterKey, identifier
|
||||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||||
this.userIdentifierToGameId[identifier] = gameId;
|
||||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`);
|
||||
|
||||
// Уведомляем игрока, что игра создана, и передаем его технический ID слота
|
||||
const assignedPlayerId = game.players[socket.id]?.id; // ID слота все еще берем из playerInfo по socket.id
|
||||
const assignedPlayerId = game.players[socket.id]?.id;
|
||||
if (!assignedPlayerId) {
|
||||
// Если по какой-то причине не удалось назначить ID игрока, удаляем игру и отправляем ошибку
|
||||
// Используем централизованную функцию очистки
|
||||
this._cleanupGame(gameId, 'player_add_failed');
|
||||
console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' });
|
||||
@ -117,228 +58,131 @@ class GameManager {
|
||||
}
|
||||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||||
|
||||
|
||||
// --- Логика старта игры ---
|
||||
// Если игра AI и теперь с 1 игроком, или PvP и теперь с 2 игроками, запускаем ее немедленно
|
||||
if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
|
||||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||
const isInitialized = game.initializeGame();
|
||||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||
game.startGame(); // Запускаем игру
|
||||
if (isInitialized) {
|
||||
game.startGame();
|
||||
} else {
|
||||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||||
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||||
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||||
this._cleanupGame(gameId, 'initialization_failed');
|
||||
}
|
||||
|
||||
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||||
// Идентификаторы игроков остаются связанными с игрой в userIdentifierToGameId до ее завершения.
|
||||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||||
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||||
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
|
||||
|
||||
} else if (mode === 'pvp' && game.playerCount === 1) {
|
||||
// Если игра PvP и ожидает второго игрока, добавляем ее в список ожидающих
|
||||
if (!this.pendingPvPGames.includes(gameId)) {
|
||||
this.pendingPvPGames.push(gameId); // Добавляем ID игры в список ожидающих
|
||||
this.pendingPvPGames.push(gameId);
|
||||
}
|
||||
// userIdentifierToGameId для создателя уже установлен выше
|
||||
|
||||
// Частичная инициализация gameState для отображения Player 1 на UI ожидания
|
||||
// initializeGame вызывается при playerCount === 1 в GameInstance
|
||||
game.initializeGame();
|
||||
|
||||
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||||
game.initializeGame(); // Частичная инициализация
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
// --- КОНЕЦ Логики старта игры ---
|
||||
|
||||
|
||||
} else {
|
||||
// Если не удалось добавить игрока в GameInstance (например, уже 2 игрока - хотя проверили выше), удаляем игру
|
||||
// Используем централизованную функцию очистки
|
||||
this._cleanupGame(gameId, 'player_add_failed');
|
||||
// GameInstance.addPlayer уже отправил ошибку клиенту
|
||||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Присоединяет игрока к существующей игре по ID.
|
||||
* @param {object} socket - Сокет игрока.
|
||||
* @param {string} gameId - ID игры, к которой нужно присоединиться.
|
||||
* @param {string|number} identifier - ID пользователя (userId).
|
||||
*/
|
||||
joinGame(socket, gameId, identifier) { // В joinGame всегда передается userId, т.к. PvP требует логина
|
||||
const game = this.games[gameId]; // Находим игру по ID
|
||||
|
||||
// Проверки перед присоединением
|
||||
joinGame(socket, gameId, identifier) {
|
||||
const game = this.games[gameId];
|
||||
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||||
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; }
|
||||
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
|
||||
// Проверка, не находится ли пользователь уже в какой-то игре
|
||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) {
|
||||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`);
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||
this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;} // Проверка на повторное присоединение по текущему сокету (хотя userIdentifierToGameId должен это предотвратить)
|
||||
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return; }
|
||||
|
||||
// Удаляем старые ожидающие игры этого пользователя, исключая текущую игру, к которой присоединяемся
|
||||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
||||
|
||||
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
||||
// GameInstance.addPlayer принимает socket, chosenCharacterKey (null для присоединения), identifier
|
||||
if (game.addPlayer(socket, null, identifier)) { // chosenCharacterKey для присоединяющегося игрока не нужен, передаем null
|
||||
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||||
if (game.addPlayer(socket, null, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameId;
|
||||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`);
|
||||
|
||||
// --- Логика старта игры ---
|
||||
// Если игра PvP и теперь с 2 игроками, запускаем ее немедленно
|
||||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||
const isInitialized = game.initializeGame();
|
||||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||
game.startGame(); // Запускаем игру
|
||||
if (isInitialized) {
|
||||
game.startGame();
|
||||
} else {
|
||||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||||
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||||
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||||
this._cleanupGame(gameId, 'initialization_failed');
|
||||
}
|
||||
|
||||
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||||
|
||||
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||||
// ownerIdentifier игры (идентификатор создателя) также остается.
|
||||
|
||||
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
// --- КОНЕЦ Логики старта игры ---
|
||||
|
||||
|
||||
} else {
|
||||
// Сообщение об ошибке отправляется из game.addPlayer
|
||||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ищет случайную ожидающую PvP игру и присоединяет игрока к ней.
|
||||
* Если подходящих игр нет, создает новую ожидающую игру.
|
||||
* @param {object} socket - Сокет игрока.
|
||||
* @param {string} [chosenCharacterKeyForCreation='elena'] - Выбранный персонаж, если придется создавать новую игру.
|
||||
* @param {string|number} identifier - ID пользователя (userId).
|
||||
*/
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { // В findRandomGame всегда передается userId
|
||||
// Удаляем старые ожидающие игры этого пользователя
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||||
this._removePreviousPendingGames(socket.id, identifier);
|
||||
|
||||
// Проверяем, не находится ли пользователь уже в какой-то игре
|
||||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на поиск.`);
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||
this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
let gameIdToJoin = null;
|
||||
// Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору для создания)
|
||||
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
||||
|
||||
// Ищем свободную игру в списке ожидающих
|
||||
for (const id of this.pendingPvPGames) {
|
||||
const pendingGame = this.games[id];
|
||||
// Проверяем, что игра существует, PvP, в ней только 1 игрок и это НЕ игра, которую создал сам текущий пользователь
|
||||
// Игрок не должен присоединяться к игре, которую создал сам.
|
||||
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); // В ожидающей игре всегда 1 игрок, он и есть players[0]
|
||||
const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||||
gameIdToJoin = id; // Нашли игру с предпочтительным оппонентом
|
||||
break; // Выходим из цикла, т.к. нашли лучший вариант
|
||||
gameIdToJoin = id;
|
||||
break;
|
||||
}
|
||||
// Если предпочтительного не нашли в этом цикле, сохраняем ID первой попавшейся (не своей) игры
|
||||
if (!gameIdToJoin) gameIdToJoin = id; // Сохраняем, но продолжаем искать предпочтительную
|
||||
if (!gameIdToJoin) gameIdToJoin = id;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameIdToJoin) {
|
||||
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
||||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`);
|
||||
this.joinGame(socket, gameIdToJoin, identifier); // Используем joinGame, т.к. логика присоединения одинакова
|
||||
this.joinGame(socket, gameIdToJoin, identifier);
|
||||
} else {
|
||||
// Если свободных игр нет, создаем новую с выбранным персонажем
|
||||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`);
|
||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); // Используем createGame
|
||||
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
||||
// userIdentifierToGameId уже обновлен в createGame
|
||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||||
socket.emit('noPendingGamesFound', {
|
||||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||||
gameId: this.userIdentifierToGameId[identifier], // ID только что созданной игры
|
||||
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
||||
gameId: this.userIdentifierToGameId[identifier],
|
||||
yourPlayerId: GAME_CONFIG.PLAYER_ID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перенаправляет действие игрока соответствующему экземпляру игры.
|
||||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||
* @param {object} actionData - Данные о действии.
|
||||
*/
|
||||
handlePlayerAction(identifier, actionData) { // Теперь принимаем identifier
|
||||
const gameId = this.userIdentifierToGameId[identifier]; // Находим ID игры по идентификатору пользователя
|
||||
const game = this.games[gameId]; // Находим экземпляр игры
|
||||
|
||||
handlePlayerAction(identifier, actionData) {
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
const game = this.games[gameId];
|
||||
if (game && game.players) {
|
||||
// Находим текущий сокет ID пользователя в списке игроков этой игры
|
||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
const currentSocketId = playerInfo?.socket?.id;
|
||||
|
||||
if (playerInfo && currentSocketId) {
|
||||
// Проверяем, что сокет с этим ID еще подключен.
|
||||
// Это дополнительная проверка, чтобы не обрабатывать действия от "зомби"-сокетов
|
||||
const actualSocket = this.io.sockets.sockets.get(currentSocketId);
|
||||
|
||||
if (actualSocket && actualSocket.connected) {
|
||||
// Передаем действие экземпляру игры, используя ТЕКУЩИЙ Socket ID
|
||||
game.processPlayerAction(currentSocketId, actionData); // processPlayerAction в GameInstance использует socketId
|
||||
game.processPlayerAction(currentSocketId, actionData);
|
||||
} else {
|
||||
// Если сокет не найден или не подключен, это может быть старое действие от отключившегося сокета
|
||||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`);
|
||||
// Не отправляем ошибку клиенту, так как он, вероятно, уже отключен или переподключается
|
||||
// Клиент получит gameNotFound при следующем запросе состояния или gameError, если игра еще активна
|
||||
}
|
||||
} else {
|
||||
// Игрок не найден в списке players этой игры по идентификатору
|
||||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`);
|
||||
// В таком случае, возможно, состояние userIdentifierToGameId некорректно.
|
||||
// Удаляем некорректную ссылку.
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// Оповещаем клиента, что игра не найдена (он должен будет запросить состояние)
|
||||
const playerSocket = this.io.sockets.sockets.get(identifier); // Попробуем найти сокет по идентификатору (если он был socket.id)
|
||||
if (!playerSocket && playerInfo?.socket) { // Если не нашли по identifier, попробуем по сокету из playerInfo
|
||||
playerSocket = playerInfo.socket;
|
||||
}
|
||||
const playerSocket = this.io.sockets.sockets.get(identifier) || playerInfo?.socket;
|
||||
if (playerSocket) {
|
||||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`);
|
||||
// Удаляем некорректную ссылку
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// Отправляем gameNotFound клиенту, если можем его найти (по identifier, если это socket.id)
|
||||
const playerSocket = this.io.sockets.sockets.get(identifier);
|
||||
if (playerSocket) {
|
||||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||||
@ -346,90 +190,69 @@ class GameManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает отключение сокета игрока.
|
||||
* Вызывается из bc.js при событии 'disconnect'.
|
||||
* @param {string} socketId - ID отключившегося сокета.
|
||||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||
*/
|
||||
handleDisconnect(socketId, identifier) { // Принимаем и socketId, и identifier
|
||||
// Ищем игру по идентификатору пользователя (более надежный способ после переподключения)
|
||||
handleDisconnect(socketId, identifier) {
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
const game = this.games[gameId];
|
||||
|
||||
// Если игра найдена и в ней есть игрок с этим идентификатором (или сокетом)
|
||||
if (game && game.players) {
|
||||
// Находим информацию об игроке по идентификатору
|
||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (playerInfo) {
|
||||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`);
|
||||
const disconnectedPlayerRole = playerInfo.id;
|
||||
const disconnectedCharacterKey = playerInfo.chosenCharacterKey;
|
||||
|
||||
// Удаляем игрока из экземпляра игры, передавая Socket ID, который отключился
|
||||
// GameInstance.removePlayer принимает socketId
|
||||
game.removePlayer(socketId); // Передаем socketId для удаления конкретного сокета
|
||||
game.removePlayer(socketId); // Удаляем именно этот сокет
|
||||
|
||||
// После удаления игрока из GameInstance, проверяем состояние игры и GameManager
|
||||
if (game.playerCount === 0) {
|
||||
// Если в игре больше нет игроков, удаляем ее из GameManager
|
||||
console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`);
|
||||
// Используем централизованную функцию очистки
|
||||
this._cleanupGame(gameId, 'player_count_zero_on_disconnect');
|
||||
|
||||
} else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||
// Если игра PvP, остался 1 игрок, и она еще не окончена (из-за дисконнекта),
|
||||
// возвращаем ее в список ожидающих.
|
||||
console.log(`[GameManager] Игра ${gameId} (PvP) теперь с 1 игроком после дисконнекта ${socketId} (идентификатор ${identifier}). Возвращаем в список ожидания.`);
|
||||
if (!this.pendingPvPGames.includes(gameId)) {
|
||||
this.pendingPvPGames.push(gameId);
|
||||
} else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
||||
// Если это PvP игра и остался 1 игрок, и игра НЕ была завершена дисконнектом этого игрока
|
||||
// (т.е. другой игрок еще в игре)
|
||||
// Тогда игра переходит в состояние ожидания.
|
||||
const remainingPlayerInfo = Object.values(game.players)[0]; // Единственный оставшийся игрок
|
||||
if (remainingPlayerInfo) {
|
||||
// Проверяем, что это не тот же игрок, что и отключившийся
|
||||
if (remainingPlayerInfo.identifier !== identifier) {
|
||||
game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey);
|
||||
// _cleanupGame будет вызван из endGameDueToDisconnect
|
||||
} else {
|
||||
// Отключился единственный оставшийся игрок в ожидающей игре.
|
||||
// _cleanupGame должен быть вызван.
|
||||
console.log(`[GameManager] Отключился единственный игрок ${identifier} из ожидающей PvP игры ${gameId}. Удаляем игру.`);
|
||||
this._cleanupGame(gameId, 'last_player_disconnected_from_pending');
|
||||
}
|
||||
} else {
|
||||
// Оставшегося игрока нет, хотя playerCount > 0 - это ошибка, очищаем.
|
||||
console.error(`[GameManager] Ошибка: playerCount > 0 в игре ${gameId}, но не найден оставшийся игрок после дисконнекта ${identifier}. Очищаем.`);
|
||||
this._cleanupGame(gameId, 'error_no_remaining_player');
|
||||
}
|
||||
// Удаляем ссылку на игру только для отключившегося идентификатора
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
|
||||
// ownerIdentifier игры (если был userId) останется тем же, даже если отключился владелец.
|
||||
// Это OK, ownerIdentifier используется для _removePreviousPendingGames.
|
||||
|
||||
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||||
} else if (game.gameState && !game.gameState.isGameOver) {
|
||||
// Если игра была активна (не ожидала) и еще не была завершена,
|
||||
// дисконнект одного из игроков завершает игру.
|
||||
game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey);
|
||||
// _cleanupGame будет вызван из endGameDueToDisconnect
|
||||
} else if (game.gameState?.isGameOver) {
|
||||
// Если игра была окончена (например, дисконнект приводил к gameOver),
|
||||
// просто удаляем ссылку на игру для отключившегося идентификатора.
|
||||
console.log(`[GameManager] Игрок ${identifier} отключился из завершенной игры ${gameId}. Удаляем ссылку.`);
|
||||
// Если игра уже была завершена до этого дисконнекта, просто удаляем ссылку на игру для отключившегося.
|
||||
console.log(`[GameManager] Игрок ${identifier} отключился из уже завершенной игры ${gameId}. Удаляем ссылку.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// _cleanupGame уже был вызван при завершении игры.
|
||||
} else {
|
||||
// Игра не пуста и не вернулась в ожидание (например, AI игра, где остался игрок,
|
||||
// или PvP игра с 2 игроками, где один отключился, а второй остался)
|
||||
// Ссылка userIdentifierToGameId[identifier] для отключившегося игрока должна быть удалена.
|
||||
// Другие случаи (например, AI игра, где игрок остался, или ошибка)
|
||||
console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
} else {
|
||||
// Игра найдена, но игрока с этим идентификатором или сокетом в game.players нет.
|
||||
// Это может означать, что сокет отключился, но запись игрока была удалена раньше,
|
||||
// или identifier некорректен.
|
||||
console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`);
|
||||
// Удаляем ссылку на игру для этого идентификатора, если она есть.
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// Проверяем, возможно, этот сокет был в другой игре по старой ссылке socketToGame (удалено),
|
||||
// или это просто отключившийся сокет без активной игры.
|
||||
}
|
||||
} else {
|
||||
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||||
console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`);
|
||||
// Убеждаемся, что ссылка userIdentifierToGameId[identifier] удалена
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Централизованная функция для очистки игры после ее завершения.
|
||||
* Удаляет экземпляр игры и все связанные с ней ссылки.
|
||||
* Вызывается из GameInstance при gameOver (по HP или дисконнекту).
|
||||
* @param {string} gameId - ID завершенной игры.
|
||||
* @param {string} reason - Причина завершения (для логирования).
|
||||
* @returns {boolean} true, если игра найдена и очищена, иначе false.
|
||||
*/
|
||||
_cleanupGame(gameId, reason = 'unknown_reason') { // <-- НОВЫЙ ПРИВАТНЫЙ МЕТОД
|
||||
_cleanupGame(gameId, reason = 'unknown_reason') {
|
||||
const game = this.games[gameId];
|
||||
if (!game) {
|
||||
console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`);
|
||||
@ -438,143 +261,94 @@ class GameManager {
|
||||
|
||||
console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`);
|
||||
|
||||
// Удаляем ссылку userIdentifierToGameId для всех игроков, которые были в этой игре
|
||||
// Перебираем players в GameInstance, чтобы получить идентификаторы
|
||||
// Очищаем таймеры, если они были активны
|
||||
if (typeof game.clearTurnTimer === 'function') {
|
||||
game.clearTurnTimer();
|
||||
}
|
||||
|
||||
Object.values(game.players).forEach(playerInfo => {
|
||||
if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) {
|
||||
delete this.userIdentifierToGameId[playerInfo.identifier];
|
||||
console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`);
|
||||
} else if (playerInfo && playerInfo.identifier) {
|
||||
console.warn(`[GameManager] User ${playerInfo.identifier} in game ${gameId} has incorrect userIdentifierToGameId reference.`);
|
||||
// Если ссылка некорректна, ничего не удаляем.
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем ID игры из списка ожидающих, если она там была
|
||||
const pendingIndex = this.pendingPvPGames.indexOf(gameId);
|
||||
if (pendingIndex > -1) {
|
||||
this.pendingPvPGames.splice(pendingIndex, 1);
|
||||
console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`);
|
||||
}
|
||||
|
||||
// Удаляем сам экземпляр игры
|
||||
delete this.games[gameId];
|
||||
console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`);
|
||||
|
||||
// Оповещаем клиентов об обновленном списке игр (может понадобиться, если удалена ожидающая игра)
|
||||
// Или если активная игра была удалена, и игроки вернутся в лобби.
|
||||
this.broadcastAvailablePvPGames();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Формирует список доступных для присоединения PvP игр для клиента.
|
||||
* @returns {Array<object>} Массив объектов с информацией об играх.
|
||||
*/
|
||||
getAvailablePvPGamesListForClient() {
|
||||
return this.pendingPvPGames
|
||||
.map(gameId => {
|
||||
const game = this.games[gameId];
|
||||
// Проверяем, что игра существует, это PvP, в ней 1 игрок, и она не окончена
|
||||
// gameState.isGameOver проверяется, чтобы исключить игры, которые могли завершиться сразу (очень маловероятно)
|
||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
||||
let firstPlayerUsername = 'Игрок';
|
||||
let firstPlayerCharacterName = '';
|
||||
|
||||
// Находим информацию о первом игроке (он всегда в слоте GAME_CONFIG.PLAYER_ID в ожидающей игре)
|
||||
const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
|
||||
if (firstPlayerInfo) {
|
||||
// Получаем имя пользователя из userData, если залогинен
|
||||
if (firstPlayerInfo.socket?.userData?.username) {
|
||||
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
||||
} else {
|
||||
// Если нет userData.username, используем часть identifier
|
||||
firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0,6)}`; // Приводим identifier к строке
|
||||
firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0, 6)}`;
|
||||
}
|
||||
|
||||
// Получаем имя персонажа из chosenCharacterKey
|
||||
const charKey = firstPlayerInfo.chosenCharacterKey;
|
||||
if (charKey) {
|
||||
// Используем _getCharacterBaseData напрямую, т.к. gameData доступен
|
||||
const charBaseStats = this._getCharacterBaseData(charKey);
|
||||
if (charBaseStats && charBaseStats.name) {
|
||||
firstPlayerCharacterName = charBaseStats.name;
|
||||
} else {
|
||||
//console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
|
||||
firstPlayerCharacterName = charKey; // В крайнем случае используем ключ
|
||||
firstPlayerCharacterName = charKey;
|
||||
}
|
||||
} else {
|
||||
//console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}.`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`);
|
||||
firstPlayerUsername = 'Неизвестный игрок'; // Если даже игрока не нашли в players
|
||||
firstPlayerUsername = 'Неизвестный игрок';
|
||||
}
|
||||
|
||||
// Формируем строку статуса для отображения в списке
|
||||
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
||||
if (firstPlayerCharacterName) {
|
||||
statusString += ` за ${firstPlayerCharacterName}`;
|
||||
}
|
||||
if (firstPlayerCharacterName) statusString += ` за ${firstPlayerCharacterName}`;
|
||||
statusString += `)`;
|
||||
|
||||
return {
|
||||
id: gameId, // Отправляем полный ID, но в списке UI показываем обрезанный
|
||||
status: statusString
|
||||
};
|
||||
return { id: gameId, status: statusString };
|
||||
}
|
||||
// Если игра не соответствует критериям ожидающей (например, пуста, заполнена, окончена), не включаем ее
|
||||
if (game && !this.pendingPvPGames.includes(gameId)) {
|
||||
// Если игра есть, но не в pendingPvPGames, она не должна тут обрабатываться.
|
||||
} else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) {
|
||||
// Игра с 1 игроком, но окончена или не инициализирована - не показывать
|
||||
} else if (game && game.playerCount === 2) {
|
||||
// Игра заполнена - не показывать
|
||||
} else if (game && game.playerCount === 0) {
|
||||
// Игра пуста - ее надо было удалить при дисконнекте последнего игрока.
|
||||
// Возможно, тут нужна очистка таких "потерянных" игр.
|
||||
if (game && !this.pendingPvPGames.includes(gameId)) { /* Game not pending */ }
|
||||
else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) { /* Game over or not initialized */ }
|
||||
else if (game && game.playerCount === 2) { /* Game full */ }
|
||||
else if (game && game.playerCount === 0) {
|
||||
console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`);
|
||||
delete this.games[gameId]; // Удаляем потерянную игру
|
||||
// Очистка из pendingPvPGames не нужна, т.к. она удаляется при playerCount === 0
|
||||
delete this.games[gameId];
|
||||
}
|
||||
|
||||
return null; // Исключаем игры, не соответствующие критериям или удаленные
|
||||
return null;
|
||||
})
|
||||
.filter(info => info !== null); // Удаляем null из результатов map
|
||||
.filter(info => info !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет обновленный список доступных PvP игр всем подключенным клиентам.
|
||||
*/
|
||||
broadcastAvailablePvPGames() {
|
||||
const availableGames = this.getAvailablePvPGamesListForClient();
|
||||
this.io.emit('availablePvPGamesList', availableGames);
|
||||
console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список активных игр для отладки на сервере.
|
||||
* @returns {Array<object>} Список объектов с краткой информацией об играх.
|
||||
*/
|
||||
getActiveGamesList() { // Для отладки на сервере
|
||||
getActiveGamesList() {
|
||||
return Object.values(this.games).map(game => {
|
||||
// Получаем имена персонажей из gameState, если игра инициализирована, иначе из chosenCharacterKey/default
|
||||
let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)');
|
||||
let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)');
|
||||
|
||||
// Проверяем наличие игроков в слотах, чтобы уточнить статус
|
||||
const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||
|
||||
if (!playerInSlot1) playerSlotCharName = 'Пусто';
|
||||
if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...'; // В PvP слоты могут быть пустыми
|
||||
if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)'; // В AI слоте оппонента всегда AI
|
||||
if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...';
|
||||
if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)';
|
||||
|
||||
return {
|
||||
id: game.id.substring(0,8), // Обрезанный ID для удобства
|
||||
id: game.id.substring(0, 8),
|
||||
mode: game.mode,
|
||||
playerCount: game.playerCount,
|
||||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)',
|
||||
@ -587,202 +361,115 @@ class GameManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает запрос клиента на gameState (например, при переподключении).
|
||||
* Находит игру пользователя по его идентификатору и отправляет ему актуальное состояние.
|
||||
* Также обновляет ссылку на сокет в GameInstance.
|
||||
* @param {object} socket - Сокет клиента, запросившего состояние.
|
||||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||
*/
|
||||
handleRequestGameState(socket, identifier) { // Принимаем socket и identifier
|
||||
// Ищем игру пользователя по его идентификатору
|
||||
handleRequestGameState(socket, identifier) {
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
let game = null;
|
||||
let game = gameId ? this.games[gameId] : null;
|
||||
|
||||
if (gameId) {
|
||||
game = this.games[gameId];
|
||||
}
|
||||
|
||||
// Если игра найдена и она существует, и в ней есть игрок с этим идентификатором
|
||||
if (game && game.players) {
|
||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (playerInfo) {
|
||||
// Проверяем, если игра окончена, не восстанавливаем состояние, а информируем
|
||||
if (game.gameState?.isGameOver) {
|
||||
console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`);
|
||||
// Удаляем ссылку на оконченную игру для этого пользователя
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// Отправляем gameNotFound, чтобы клиент вернулся в меню
|
||||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||||
return; // Прекращаем обработку
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`);
|
||||
|
||||
// --- Обновляем GameInstance: заменяем старый сокет на новый для этого игрока ---
|
||||
// Удаляем старую запись игрока по старому socket.id, если она есть и отличается
|
||||
const oldSocketId = playerInfo.socket?.id;
|
||||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
||||
console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`);
|
||||
delete game.players[oldSocketId]; // Удаляем запись по старому socketId
|
||||
// playerCount не уменьшаем/увеличиваем, т.к. это тот же игрок, просто сменил сокет
|
||||
// Удаляем ссылку на старый сокет по роли
|
||||
delete game.players[oldSocketId];
|
||||
if (game.playerSockets[playerInfo.id]?.id === oldSocketId) {
|
||||
delete game.playerSockets[playerInfo.id];
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем или обновляем запись для нового сокета, связывая его с существующим идентификатором игрока
|
||||
game.players[socket.id] = playerInfo; // Переиспользуем существующий объект playerInfo
|
||||
game.players[socket.id].socket = socket; // Обновляем объект сокета
|
||||
// Ensure the identifier and role are correct on the new socket entry
|
||||
game.players[socket.id].identifier = identifier; // Make sure identifier is set (уже должно быть, но на всякий случай)
|
||||
// playerInfo.id should already be correct (player/opponent role)
|
||||
|
||||
game.playerSockets[playerInfo.id] = socket; // Обновляем ссылку на сокет по роли
|
||||
|
||||
// Убеждаемся, что новый socket.id теперь связан с этой игрой в GameManager - НЕ НУЖНО, socketToGame удален
|
||||
// this.socketToGame[socket.id] = game.id;
|
||||
|
||||
|
||||
// Присоединяем новый сокет к комнате Socket.IO
|
||||
game.players[socket.id] = playerInfo;
|
||||
game.players[socket.id].socket = socket;
|
||||
game.playerSockets[playerInfo.id] = socket;
|
||||
socket.join(game.id);
|
||||
// --- КОНЕЦ Обновления сокета ---
|
||||
|
||||
|
||||
// Получаем данные персонажей с точки зрения этого клиента
|
||||
// playerInfo.chosenCharacterKey - это персонаж этого клиента
|
||||
const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey);
|
||||
// Определяем ключ персонажа оппонента с точки зрения этого клиента
|
||||
const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
const opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null; // Берем из gameState, т.к. там актуальное состояние слотов
|
||||
// Если оппонент еще не определен в gameState (PvP ожидание), используем playerCharacterKey/opponentCharacterKey из gameInstance
|
||||
// ВАЖНО: при переподключении к *активной* игре, gameState.opponent.characterKey ДОЛЖЕН БЫТЬ определен.
|
||||
// Если он null, это может быть PvP ожидание или некорректное состояние.
|
||||
let opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null;
|
||||
if (!opponentCharacterKeyForClient) {
|
||||
// Попробуем найти ключ из GameInstance properties (они устанавливаются при инициализации)
|
||||
const opponentSlotKeyInInstance = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.playerCharacterKey : game.opponentCharacterKey; // ИСПРАВЛЕНО: Логика получения ключа оппонента
|
||||
opponentCharacterKeyForClient = opponentSlotKeyInInstance;
|
||||
// Если даже из GameInstance properties ключ null, это точно PvP ожидание или критическая ошибка
|
||||
opponentCharacterKeyForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey;
|
||||
}
|
||||
const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient); // Данные оппонента с т.з. клиента
|
||||
|
||||
const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient);
|
||||
|
||||
if (playerCharDataForClient && opponentCharDataForClient && game.gameState) {
|
||||
// Проверяем, готово ли gameState к игре (определены оба бойца)
|
||||
const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2);
|
||||
const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...';
|
||||
|
||||
|
||||
socket.emit('gameState', {
|
||||
gameId: game.id,
|
||||
yourPlayerId: playerInfo.id, // ID слота этого клиента в игре
|
||||
gameState: game.gameState,
|
||||
playerBaseStats: playerCharDataForClient.baseStats, // Статы "моего" персонажа для клиента
|
||||
opponentBaseStats: opponentCharDataForClient.baseStats, // Статы "моего" оппонента для клиента
|
||||
playerAbilities: playerCharDataForClient.abilities, // Абилки "моего" персонажа для клиента
|
||||
opponentAbilities: opponentCharDataForClient.abilities, // Абилки "моего" оппонента для клиента
|
||||
log: game.consumeLogBuffer(), // Отправляем текущий лог и очищаем буфер игры
|
||||
clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
|
||||
gameId: game.id, yourPlayerId: playerInfo.id, gameState: game.gameState,
|
||||
playerBaseStats: playerCharDataForClient.baseStats, opponentBaseStats: opponentCharDataForClient.baseStats,
|
||||
playerAbilities: playerCharDataForClient.abilities, opponentAbilities: opponentCharDataForClient.abilities,
|
||||
log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`);
|
||||
|
||||
// Логика старта игры при переподключении (если она еще не началась)
|
||||
// Эта логика должна быть только для случая, когда переподключившийся игрок ЗАВЕРШАЕТ состав игры
|
||||
// (например, второй игрок в PvP переподключился к ожидающей игре).
|
||||
// Если игра уже началась, startGame не должен вызываться повторно.
|
||||
// Проверяем: игра не окончена, готова к игре (2 игрока или AI), и состояние оппонента НЕ БЫЛО определено до этого запроса (признак не полностью стартовавшей игры)
|
||||
if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) {
|
||||
console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect (Opponent state missing). Initializing/Starting.`);
|
||||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||
const isInitialized = game.initializeGame(); // Переинициализируем state полностью с обоими персонажами
|
||||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||
game.startGame(); // Запускаем игру (это отправит gameStarted всем, включая этого клиента)
|
||||
console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect. Initializing/Starting.`);
|
||||
const isInitialized = game.initializeGame();
|
||||
if (isInitialized) {
|
||||
game.startGame();
|
||||
} else {
|
||||
console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`);
|
||||
// Дополнительная обработка ошибки, возможно, уведомить игроков
|
||||
this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения. Не удалось инициализировать игру.' });
|
||||
// Если инициализация провалилась, игра в некорректном состоянии, нужно ее удалить
|
||||
this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения.' });
|
||||
this._cleanupGame(gameId, 'reconnect_initialization_failed');
|
||||
}
|
||||
}
|
||||
// Если игра уже активно идет (не окончена, не ожидание) и состояние оппонента БЫЛО определено,
|
||||
// то startGame не вызывается повторно. Клиент получит gameStateUpdate от обычного хода игры.
|
||||
// Если игра PvP ожидающая (1 игрок), startGame не вызывается, isGameReadyForPlay будет false.
|
||||
else if (!isGameReadyForPlay) {
|
||||
} else if (!isGameReadyForPlay) {
|
||||
console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`);
|
||||
// Если это ожидающая игра, убедимся, что клиент получает статус ожидания
|
||||
socket.emit('waitingForOpponent');
|
||||
} else if (game.gameState.isGameOver) {
|
||||
console.log(`[GameManager] Reconnected to game ${gameId} which is already over. Sending gameNotFound.`);
|
||||
// Если игра окончена, client.js должен по gameState.isGameOver показать модалку.
|
||||
// Но чтобы гарантировать возврат в меню при последующих запросах, лучше отправить gameNotFound.
|
||||
// Удаляем ссылку на оконченную игру для этого пользователя
|
||||
} else if (game.gameState.isGameOver) { // Повторная проверка, т.к. startGame мог завершить игру
|
||||
console.log(`[GameManager] Reconnected to game ${gameId} which is now over (after re-init). Sending gameNotFound.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
// Отправляем gameNotFound
|
||||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия завершилась во время переподключения.' });
|
||||
} else {
|
||||
// Переподключение к активной игре, которая уже полностью стартовала.
|
||||
console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`);
|
||||
// Важно: если игра активна, нужно отправить и текущее состояние таймера.
|
||||
// Это можно сделать, вызвав game.startTurnTimer() (он отправит update),
|
||||
// но только если это ход этого игрока и игра не AI (или ход игрока в AI).
|
||||
// Или добавить отдельный метод в GameInstance для отправки текущего состояния таймера.
|
||||
if (typeof game.startTurnTimer === 'function') { // Проверяем, что метод существует
|
||||
// Перезапуск таймера здесь может быть некорректным, если ход не этого игрока
|
||||
// Лучше, чтобы gameInstance сам отправлял 'turnTimerUpdate' при gameState
|
||||
// Либо добавить специальный метод в gameInstance для отправки текущего значения таймера
|
||||
// Пока оставим так, startTurnTimer сам проверит, чей ход.
|
||||
game.startTurnTimer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||
// Если данные для отправки некорректны, игра в некорректном состоянии, нужно ее удалить
|
||||
this._cleanupGame(gameId, 'reconnect_send_failed');
|
||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' });
|
||||
}
|
||||
|
||||
} else {
|
||||
// Игра найдена по идентификатору пользователя, но игрока с этим идентификатором нет в players этой игры.
|
||||
// Это очень странная ситуация, возможно, state userIdentifierToGameId некорректен.
|
||||
console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`);
|
||||
// Удаляем некорректную ссылку и отправляем gameNotFound
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' });
|
||||
}
|
||||
} else {
|
||||
// Игра не найдена по userIdentifierToGameId[identifier]
|
||||
console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`);
|
||||
socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' }); // Уведомляем клиента, что игра не найдена
|
||||
socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Вспомогательные функции для получения данных персонажа из data.js ---
|
||||
// Скопировано из gameInstance.js, т.к. gameManager тоже использует gameData напрямую
|
||||
/**
|
||||
* Получает базовые статы и список способностей для персонажа по ключу.
|
||||
* Эти функции предназначены для использования ВНУТРИ GameManager или GameInstance.
|
||||
* @param {string} key - Ключ персонажа ('elena', 'balard', 'almagest').
|
||||
* @returns {{baseStats: object, abilities: array}|null} Объект с базовыми статами и способностями, или null.
|
||||
*/
|
||||
_getCharacterData(key) {
|
||||
if (!key) { console.warn("GameManager::_getCharacterData called with null/undefined key."); return null; }
|
||||
switch (key) {
|
||||
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; // Балард использует opponentAbilities из data.js
|
||||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; // Альмагест использует almagestAbilities из data.js
|
||||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities };
|
||||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities };
|
||||
default: console.error(`GameManager::_getCharacterData: Unknown character key "${key}"`); return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Получает только базовые статы для персонажа по ключу.
|
||||
* @param {string} key - Ключ персонажа.
|
||||
* @returns {object|null} Базовые статы или null.
|
||||
*/
|
||||
_getCharacterBaseData(key) {
|
||||
const charData = this._getCharacterData(key);
|
||||
return charData ? charData.baseStats : null;
|
||||
}
|
||||
/**
|
||||
* Получает только список способностей для персонажа по ключу.
|
||||
* @param {string} key - Ключ персонажа.
|
||||
* @returns {array|null} Список способностей или null.
|
||||
*/
|
||||
_getCharacterAbilities(key) {
|
||||
const charData = this._getCharacterData(key);
|
||||
return charData ? charData.abilities : null;
|
||||
|
Loading…
x
Reference in New Issue
Block a user