bc/public/js/gameplay.js

339 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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 = '<p class="placeholder-text">Ошибка загрузки способностей.</p>';
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 = '<p class="placeholder-text">Нет доступных способностей.</p>';
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();
}