// /public/js/gameplay.js export function initGameplay(dependencies) { const { socket, clientState, ui } = dependencies; // Элементы управления боем обычно находятся внутри gameWrapper и управляются через ui.js, // но нам могут понадобиться ссылки на кнопки для привязки событий, если они не привязаны в ui.js // или если ui.js не экспортирует их напрямую. // В данном случае, attackButton и abilitiesGrid есть в client.js, так что получим их. // ui.elements из main.js содержит returnToMenuButton const { returnToMenuButton } = ui.elements; // Получаем ссылки на кнопки атаки и способностей напрямую, как было в client.js // или, если бы ui.js их экспортировал, можно было бы через window.gameUI.uiElements const attackButton = document.getElementById('button-attack'); const abilitiesGrid = document.getElementById('abilities-grid'); // --- Вспомогательные функции --- function enableGameControls(enableAttack = true, enableAbilities = true) { if (attackButton) attackButton.disabled = !enableAttack; if (abilitiesGrid) { // Предполагаем, что GAME_CONFIG доступен глобально или его нужно передать const config = window.GAME_CONFIG || {}; const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; }); } // Если кнопка блока есть и управляется отсюда // if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true; } function disableGameControls() { enableGameControls(false, false); } // Эта функция была в client.js, переносим сюда function initializeAbilityButtons() { if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) { if (abilitiesGrid) abilitiesGrid.innerHTML = '

Ошибка загрузки способностей.

'; return; } abilitiesGrid.innerHTML = ''; // Очищаем предыдущие кнопки const config = window.GAME_CONFIG; // Используем данные из clientState, которые были обновлены из событий сервера const abilitiesToDisplay = clientState.playerAbilitiesServer; const baseStatsForResource = clientState.playerBaseStatsServer; if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) { abilitiesGrid.innerHTML = '

Нет доступных способностей.

'; return; } const resourceName = baseStatsForResource.resourceName || "Ресурс"; const abilityButtonClass = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; abilitiesToDisplay.forEach(ability => { const button = document.createElement('button'); button.id = `ability-btn-${ability.id}`; button.classList.add(abilityButtonClass); button.dataset.abilityId = ability.id; let cooldown = ability.cooldown; // Это базовый КД из данных персонажа 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); button.addEventListener('click', handleAbilityButtonClick); abilitiesGrid.appendChild(button); }); const placeholder = abilitiesGrid.querySelector('.placeholder-text'); if (placeholder) placeholder.remove(); // После инициализации кнопок, их состояние (disabled/enabled) будет управляться window.gameUI.updateUI() } function handleAbilityButtonClick(event) { const abilityId = event.currentTarget.dataset.abilityId; if (clientState.isLoggedIn && clientState.isInGame && clientState.currentGameId && abilityId && clientState.currentGameState && !clientState.currentGameState.isGameOver) { // Перед отправкой действия можно добавить быструю проверку на клиенте (например, хватает ли ресурса), // но основная валидация все равно на сервере. socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId }); disableGameControls(); // Блокируем управление до ответа сервера или следующего хода } else { console.warn("Cannot perform ability action, invalid state:", { isLoggedIn: clientState.isLoggedIn, isInGame: clientState.isInGame, gameId: clientState.currentGameId, abilityId, gameState: clientState.currentGameState }); } } // --- Обработчики событий DOM --- if (attackButton) { attackButton.addEventListener('click', () => { if (clientState.isLoggedIn && clientState.isInGame && clientState.currentGameId && clientState.currentGameState && !clientState.currentGameState.isGameOver) { socket.emit('playerAction', { actionType: 'attack' }); disableGameControls(); // Блокируем управление до ответа сервера или следующего хода } else { console.warn("Cannot perform attack action, invalid state."); } }); } if (returnToMenuButton) { // Кнопка из модалки GameOver returnToMenuButton.addEventListener('click', () => { if (!clientState.isLoggedIn) { ui.showAuthScreen(); // Если как-то оказались здесь без логина return; } returnToMenuButton.disabled = true; // Блокируем на время перехода // ui.resetGameVariables(); // Вызывается в showGameSelectionScreen clientState.isInGame = false; // Устанавливаем, что мы больше не в игре disableGameControls(); // Деактивируем игровые контролы // window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } }); // Скрываем модалку (делается в showGameSelectionScreen) ui.showGameSelectionScreen(clientState.loggedInUsername); // Возвращаемся на экран выбора // Кнопка returnToMenuButton включится при следующем показе модалки GameOver (логика в ui.js или здесь при gameOver) }); } // --- Обработчики событий Socket.IO --- socket.on('gameStarted', (data) => { if (!clientState.isLoggedIn) return; // Игнорируем, если не залогинены console.log('[Gameplay] Game started:', data); // Обновляем состояние клиента clientState.currentGameId = data.gameId; clientState.myPlayerId = data.yourPlayerId; clientState.currentGameState = data.initialGameState; clientState.playerBaseStatsServer = data.playerBaseStats; clientState.opponentBaseStatsServer = data.opponentBaseStats; clientState.playerAbilitiesServer = data.playerAbilities; clientState.opponentAbilitiesServer = data.opponentAbilities; clientState.myCharacterKey = data.playerBaseStats?.characterKey; clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey; clientState.isInGame = true; if (data.clientConfig) { // Если сервер прислал конфиг // Важно: GAME_CONFIG используется в ui.js и других местах window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig }; } else if (!window.GAME_CONFIG) { // Базовый конфиг, если не пришел с сервера и не определен window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' /* ... другие важные ключи ... */ }; } ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js ui.showGameScreen(); // Показываем игровой экран initializeAbilityButtons(); // Инициализируем кнопки способностей с новыми данными // Очистка лога перед началом новой игры if (window.gameUI?.uiElements?.log?.list) { window.gameUI.uiElements.log.list.innerHTML = ''; } // Добавление начальных логов, если есть if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); } // Первичное обновление UI боевого экрана requestAnimationFrame(() => { if (window.gameUI && typeof window.gameUI.updateUI === 'function') { window.gameUI.updateUI(); } }); // ui.hideGameOverModal(); // Теперь делается в showGameScreen ui.setGameStatusMessage(""); // Очищаем общий статус // Таймер хода будет обновлен событием 'turnTimerUpdate' }); // Используется для восстановления состояния уже идущей игры (например, при реконнекте) socket.on('gameState', (data) => { if (!clientState.isLoggedIn) return; console.log('[Gameplay] Received full gameState (e.g. on reconnect):', data); // Обновляем состояние клиента (похоже на gameStarted) clientState.currentGameId = data.gameId; clientState.myPlayerId = data.yourPlayerId; clientState.currentGameState = data.gameState; // Используем gameState вместо initialGameState clientState.playerBaseStatsServer = data.playerBaseStats; clientState.opponentBaseStatsServer = data.opponentBaseStats; clientState.playerAbilitiesServer = data.playerAbilities; clientState.opponentAbilitiesServer = data.opponentAbilities; clientState.myCharacterKey = data.playerBaseStats?.characterKey; clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey; clientState.isInGame = true; // Устанавливаем, что мы в игре if (data.clientConfig) { window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig }; } else if (!window.GAME_CONFIG) { window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; } ui.updateGlobalWindowVariablesForUI(); if (!clientState.isInGame || document.querySelector('.game-wrapper').style.display === 'none') { ui.showGameScreen(); // Показываем игровой экран, если еще не там } initializeAbilityButtons(); // Переинициализируем кнопки способностей // Лог при 'gameState' может быть уже накопленным, очищаем и добавляем новый if (window.gameUI?.uiElements?.log?.list && data.log) { 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') { window.gameUI.updateUI(); } }); // ui.hideGameOverModal(); // Делается в showGameScreen // Таймер хода будет обновлен событием 'turnTimerUpdate' }); socket.on('gameStateUpdate', (data) => { if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return; clientState.currentGameState = data.gameState; ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js if (window.gameUI?.updateUI) window.gameUI.updateUI(); // Добавляем только новые логи, если они есть в этом частичном обновлении if (window.gameUI?.addToLog && data.log) { data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } // Логика включения/выключения контролов на основе gameState.isPlayerTurn и myPlayerId // обычно делается внутри window.gameUI.updateUI() }); socket.on('logUpdate', (data) => { if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return; if (window.gameUI?.addToLog && data.log) { data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } }); socket.on('gameOver', (data) => { if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) { // Если мы не в игре, но залогинены, запросим состояние (вдруг это старое событие) if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState'); else if (!clientState.isLoggedIn) ui.showAuthScreen(); // Если вообще не залогинены return; } const playerWon = data.winnerId === clientState.myPlayerId; clientState.currentGameState = data.finalGameState; // Обновляем состояние последним // clientState.isInGame = false; // НЕ СБРАСЫВАЕМ ЗДЕСЬ, чтобы UI показывал экран GameOver. Сбросится при выходе в меню. ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js if (window.gameUI?.updateUI) window.gameUI.updateUI(); // Обновляем панели в последний раз if (window.gameUI?.addToLog && data.log) { data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } if (window.gameUI?.showGameOver) { const oppKey = clientState.opponentBaseStatsServer?.characterKey; window.gameUI.showGameOver(playerWon, data.reason, oppKey, data); // ui.js покажет модалку } if (returnToMenuButton) returnToMenuButton.disabled = false; // Активируем кнопку "Вернуться в меню" ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли.")); // Обновляем UI таймера, чтобы показать "Конец" или скрыть if (window.gameUI?.updateTurnTimerDisplay) { window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode); } // Контролы должны быть заблокированы, т.к. игра окончена (ui.js->updateUI это сделает) }); socket.on('opponentDisconnected', (data) => { if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return; const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник'; if (window.gameUI?.addToLog) { window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system'); } // Если игра еще не окончена, сервер может дать время на переподключение или объявить победу if (clientState.currentGameState && !clientState.currentGameState.isGameOver) { ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true); disableGameControls(); // Блокируем управление, пока сервер не решит исход } }); socket.on('turnTimerUpdate', (data) => { if (!clientState.isInGame || !clientState.currentGameState || clientState.currentGameState.isGameOver) { // Если игра окончена или не в игре, обновляем таймер соответственно if (window.gameUI?.updateTurnTimerDisplay && !clientState.currentGameState?.isGameOver) { window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode); } return; } if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') { const config = window.GAME_CONFIG || {}; // Определяем, является ли текущий ход ходом этого клиента const isMyActualTurn = clientState.myPlayerId && clientState.currentGameState.isPlayerTurn === (clientState.myPlayerId === config.PLAYER_ID); window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode); } // Логика включения/выключения контролов на основе isMyActualTurn // обычно выполняется в window.gameUI.updateUI(), которая вызывается после gameStateUpdate. // Если turnTimerUpdate приходит отдельно и должен влиять на контролы, то нужно добавить: // if (isMyActualTurn) enableGameControls(); else disableGameControls(); // Но это может конфликтовать с логикой в updateUI(). Обычно updateUI() - главный источник правды. }); // Начальная деактивация игровых контролов при загрузке модуля disableGameControls(); }