bc/public/js/gameplay.js

449 lines
27 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;
const { returnToMenuButton } = ui.elements; // Предполагается, что это кнопка "Вернуться в меню" из модалки gameOver
const attackButton = document.getElementById('button-attack');
const abilitiesGrid = document.getElementById('abilities-grid');
// Инициализируем флаг в clientState, если он еще не существует (лучше делать в main.js при объявлении clientState)
if (typeof clientState.isActionInProgress === 'undefined') {
clientState.isActionInProgress = false;
}
// --- Вспомогательные функции ---
function enableGameControls(enableAttack = true, enableAbilities = true) {
const username = clientState.loggedInUsername || 'N/A';
// console.log(`[CLIENT ${username} GP] enableGameControls called. enableAttack: ${enableAttack}, enableAbilities: ${enableAbilities}, isActionInProgress: ${clientState.isActionInProgress}`);
if (clientState.isActionInProgress) {
if (attackButton) attackButton.disabled = true;
if (abilitiesGrid) {
const config = window.GAME_CONFIG || {};
const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = true; });
}
// console.log(`[CLIENT ${username} GP] Action is in progress, controls remain disabled by enableGameControls.`);
// Не вызываем updateUI здесь, чтобы не было рекурсии, если updateUI вызвал enableGameControls
return;
}
if (attackButton) attackButton.disabled = !enableAttack;
if (abilitiesGrid) {
const config = window.GAME_CONFIG || {};
const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; });
}
// console.log(`[CLIENT ${username} GP] Controls set by enableGameControls. Attack disabled: ${attackButton ? attackButton.disabled : 'N/A'}`);
// Обновление UI для 반영反映 состояния disabled должно происходить в gameUI.updateUI()
// или здесь, если gameUI.updateUI() не покрывает это полностью.
if (window.gameUI?.updateUI && clientState.currentGameState && !clientState.currentGameState.isGameOver) { // Обновляем только если в активной игре
requestAnimationFrame(() => window.gameUI.updateUI());
}
}
function disableGameControls() {
const username = clientState.loggedInUsername || 'N/A';
// console.log(`[CLIENT ${username} GP] disableGameControls called.`);
if (attackButton) attackButton.disabled = true;
if (abilitiesGrid) {
const config = window.GAME_CONFIG || {};
const cls = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = true; });
}
// Обновление UI для 반영反映 состояния disabled
if (window.gameUI?.updateUI && clientState.currentGameState) { // Обновляем, если есть gameState (даже если gameOver)
requestAnimationFrame(() => window.gameUI.updateUI());
}
}
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;
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; // Сохраняем 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(); // Удаляем плейсхолдер, если он был
}
function handleAbilityButtonClick(event) {
const abilityId = event.currentTarget.dataset.abilityId;
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] handleAbilityButtonClick. AbilityID: ${abilityId}, isActionInProgress: ${clientState.isActionInProgress}`);
if (clientState.isLoggedIn &&
clientState.isInGame &&
clientState.currentGameId &&
abilityId &&
clientState.currentGameState &&
!clientState.currentGameState.isGameOver &&
!clientState.isActionInProgress) {
console.log(`[CLIENT ${username} GP] Emitting playerAction (ability: ${abilityId}). Setting isActionInProgress = true.`);
clientState.isActionInProgress = true;
disableGameControls(); // Блокируем все контролы немедленно
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
} else {
console.warn(`[CLIENT ${username} GP] Cannot perform ability action. Conditions not met or action in progress. InGame: ${clientState.isInGame}, GameOver: ${clientState.currentGameState?.isGameOver}, ActionInProgress: ${clientState.isActionInProgress}, AbilityID: ${abilityId}`);
}
}
// --- Обработчики событий DOM ---
if (attackButton) {
attackButton.addEventListener('click', () => {
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Attack button clicked. isActionInProgress: ${clientState.isActionInProgress}`);
if (clientState.isLoggedIn &&
clientState.isInGame &&
clientState.currentGameId &&
clientState.currentGameState &&
!clientState.currentGameState.isGameOver &&
!clientState.isActionInProgress) {
console.log(`[CLIENT ${username} GP] Emitting playerAction (attack). Setting isActionInProgress = true.`);
clientState.isActionInProgress = true;
disableGameControls(); // Блокируем все контролы немедленно
socket.emit('playerAction', { actionType: 'attack' });
} else {
console.warn(`[CLIENT ${username} GP] Cannot perform attack action. Conditions not met or action in progress. InGame: ${clientState.isInGame}, GameOver: ${clientState.currentGameState?.isGameOver}, ActionInProgress: ${clientState.isActionInProgress}`);
}
});
}
if (returnToMenuButton) {
returnToMenuButton.addEventListener('click', () => {
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Return to menu button clicked.`);
if (!clientState.isLoggedIn) {
ui.showAuthScreen(); // Если не залогинен, на экран авторизации
return;
}
returnToMenuButton.disabled = true;
clientState.isActionInProgress = false; // Сбрасываем флаг при выходе в меню
clientState.isInGame = false;
disableGameControls(); // Отключаем игровые контролы
ui.showGameSelectionScreen(clientState.loggedInUsername); // Показываем экран выбора игры
});
}
// --- ОБЩИЙ ОБРАБОТЧИК ДЛЯ ЗАПУСКА/ВОССТАНОВЛЕНИЯ ИГРЫ ---
function handleGameDataReceived(data, eventName = "unknown") {
if (!clientState.isLoggedIn) {
console.warn(`[CLIENT GP] handleGameDataReceived (${eventName}) called, but client not logged in. Ignoring.`);
return;
}
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] handleGameDataReceived from event: ${eventName}. GameID: ${data.gameId}, YourPlayerID: ${data.yourPlayerId}, GS.isPlayerTurn: ${data.initialGameState?.isPlayerTurn || data.gameState?.isPlayerTurn}`);
clientState.isActionInProgress = false; // Сброс флага при получении нового полного состояния игры
clientState.currentGameId = data.gameId;
clientState.myPlayerId = data.yourPlayerId; // Роль игрока (player/opponent)
clientState.currentGameState = data.initialGameState || data.gameState;
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;
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
clientState.isInGame = true;
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
clientState.isInGame = false; // Если игра уже завершена, мы не в активной игре
}
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', LOG_TYPE_SYSTEM: 'system' };
}
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
const gameWrapperElement = document.querySelector('.game-wrapper');
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
const isGameWrapperVisible = gameWrapperElement && (gameWrapperElement.style.display === 'flex' || getComputedStyle(gameWrapperElement).display === 'flex');
if (!isGameWrapperVisible) {
ui.showGameScreen(); // Показываем игровой экран, если он не был виден
}
}
initializeAbilityButtons(); // Инициализируем кнопки способностей на основе полученных данных
if (window.gameUI?.uiElements?.log?.list) {
window.gameUI.uiElements.log.list.innerHTML = ''; // Очищаем UI-лог перед добавлением новых
}
if (window.gameUI?.addToLog && data.log) {
data.log.forEach(logEntry => {
window.gameUI.addToLog(logEntry.message, logEntry.type);
});
}
// Запрос на обновление UI и контролов
requestAnimationFrame(() => {
if (window.gameUI?.updateUI) {
window.gameUI.updateUI(); // Обновляем весь UI игры (панели игроков, эффекты и т.д.)
}
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver && window.GAME_CONFIG) {
const config = window.GAME_CONFIG;
const isMyActualTurn = clientState.myPlayerId &&
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
console.log(`[CLIENT ${username} GP] handleGameDataReceived - Determining controls. isMyActualTurn: ${isMyActualTurn}`);
if (isMyActualTurn) {
enableGameControls();
} else {
disableGameControls();
}
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
console.log(`[CLIENT ${username} GP] handleGameDataReceived - Game is over, disabling controls.`);
disableGameControls();
}
});
// Управление gameStatusMessage
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
// gameOver имеет свой обработчик статуса
} else if (eventName === 'gameStarted' || eventName === 'gameState (reconnect)') {
console.log(`[CLIENT ${username} GP] ${eventName} - Clearing game status message for fresh game/state load.`);
ui.setGameStatusMessage("");
} else {
if (clientState.isInGame) {
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (!currentStatusText.toLowerCase().includes("отключился")) {
ui.setGameStatusMessage("");
}
}
}
// Если игра пришла завершенной (даже в gameStarted), вызываем showGameOver
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
// Проверяем, не показан ли уже экран gameOver
const gameOverScreen = document.getElementById('game-over-screen');
const hiddenClass = window.GAME_CONFIG?.CSS_CLASS_HIDDEN || 'hidden';
if (gameOverScreen && gameOverScreen.classList.contains(hiddenClass)) {
let playerWon = data.winnerId === clientState.myPlayerId;
// Дополнительная логика определения победителя, если winnerId не всегда приходит
if (data.winnerId === null && data.reason && data.reason.startsWith('server_error')) {
// Ничья или ошибка сервера, никто не выиграл
} else if (!data.winnerId && clientState.currentGameState.player && clientState.currentGameState.opponent) {
if (clientState.currentGameState.player.currentHp <= 0 && clientState.currentGameState.opponent.currentHp <= 0) playerWon = false; // Ничья или оба проиграли
else if (clientState.currentGameState.player.currentHp > 0 && clientState.currentGameState.opponent.currentHp <= 0) playerWon = (clientState.myPlayerId === clientState.currentGameState.player.id);
else if (clientState.currentGameState.opponent.currentHp > 0 && clientState.currentGameState.player.currentHp <= 0) playerWon = (clientState.myPlayerId === clientState.currentGameState.opponent.id);
}
console.log(`[CLIENT ${username} GP] Game received as 'Over' in ${eventName}. Calling showGameOver. PlayerWon: ${playerWon}`);
if (window.gameUI?.showGameOver) {
window.gameUI.showGameOver(playerWon, data.reason || "Игра завершена", clientState.opponentCharacterKey || data.loserCharacterKey, data);
}
}
if (returnToMenuButton) returnToMenuButton.disabled = false;
}
}
// --- Обработчики событий Socket.IO ---
socket.on('gameStarted', (data) => {
handleGameDataReceived(data, 'gameStarted');
});
socket.on('gameState', (data) => { // Для совместимости со старым событием реконнекта
handleGameDataReceived(data, 'gameState (reconnect)');
});
socket.on('gameStateUpdate', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Event: gameStateUpdate. GS.isPlayerTurn: ${data.gameState?.isPlayerTurn}. ActionInProgress before reset: ${clientState.isActionInProgress}`);
clientState.isActionInProgress = false; // <--- СБРОС ФЛАГА
clientState.currentGameState = data.gameState;
ui.updateGlobalWindowVariablesForUI();
if (window.gameUI?.updateUI) {
requestAnimationFrame(() => {
window.gameUI.updateUI(); // Обновляем панели, эффекты и т.д.
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver && window.GAME_CONFIG) {
const config = window.GAME_CONFIG;
const isMyActualTurn = clientState.myPlayerId &&
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
console.log(`[CLIENT ${username} GP] gameStateUpdate - Determining controls. isMyActualTurn: ${isMyActualTurn}`);
if (isMyActualTurn) {
enableGameControls();
} else {
disableGameControls();
}
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (!currentStatusText.toLowerCase().includes("отключился")) { // Не стираем сообщение о дисконнекте оппонента
ui.setGameStatusMessage(""); // Очищаем общий статус, если игра активна
}
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
console.log(`[CLIENT ${username} GP] gameStateUpdate resulted in GameOver. Disabling controls.`);
disableGameControls(); // Отключаем управление, если игра закончилась этим обновлением
}
});
}
if (window.gameUI?.addToLog && data.log) {
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
}
});
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 username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Event: gameOver. WinnerID: ${data.winnerId}, Reason: ${data.reason}`);
clientState.isActionInProgress = false; // <--- СБРОС ФЛАГА
const playerWon = data.winnerId === clientState.myPlayerId;
clientState.currentGameState = data.finalGameState;
clientState.isInGame = false;
ui.updateGlobalWindowVariablesForUI();
if (window.gameUI?.updateUI) requestAnimationFrame(() => 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.opponentCharacterKey || data.loserCharacterKey;
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
}
if (returnToMenuButton) returnToMenuButton.disabled = false;
if (window.gameUI?.updateTurnTimerDisplay) {
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
}
disableGameControls();
});
socket.on('opponentDisconnected', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Event: opponentDisconnected. PlayerID: ${data.disconnectedPlayerId}`);
const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник';
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
disableGameControls();
}
});
socket.on('playerReconnected', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username} GP] Event: playerReconnected. PlayerID: ${data.reconnectedPlayerId}, Name: ${data.reconnectedPlayerName}`);
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (currentStatusText.toLowerCase().includes("отключился")) {
ui.setGameStatusMessage(""); // Очищаем сообщение об ожидании
}
// После этого должен прийти gameStateUpdate, который правильно установит контролы
// или turnTimerUpdate. Можно не вызывать enable/disable здесь.
}
});
socket.on('turnTimerUpdate', (data) => {
const username = clientState.loggedInUsername || 'N/A';
// console.log(`[CLIENT ${username} GP] Event: turnTimerUpdate RECEIVED. Data:`, JSON.stringify(data), `Current isInGame: ${clientState.isInGame}, GS exists: ${!!clientState.currentGameState}`);
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
if (window.gameUI?.updateTurnTimerDisplay && clientState.currentGameState && clientState.currentGameState.isGameOver === false) { // Если не в игре, но gameState еще не gameOver
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
}
return;
}
if (clientState.currentGameState.isGameOver) {
if (window.gameUI?.updateTurnTimerDisplay) {
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
}
return; // Не обновляем контролы, если игра окончена
}
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
const config = window.GAME_CONFIG;
// data.isPlayerTurn здесь - это isPlayerTurnForTimer от сервера (чей ход с точки зрения таймера)
const isMyTurnAccordingToTimer = clientState.myPlayerId &&
((data.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
(!data.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyTurnAccordingToTimer, clientState.currentGameState.gameMode);
// Управление кнопками на основе информации из ТАЙМЕРА.
// Это может быть полезно, если gameStateUpdate запаздывает.
// Но нужно быть осторожным, чтобы не конфликтовать с gameState.isPlayerTurn.
// Если игра НЕ на паузе (по данным таймера), то можно обновить кнопки.
if (!data.isPaused) { // isPaused от сервера (isTimerLogicPaused || game.isGameEffectivelyPaused())
if (isMyTurnAccordingToTimer && !clientState.currentGameState.isGameOver) {
enableGameControls();
} else if (!isMyTurnAccordingToTimer && !clientState.currentGameState.isGameOver){
disableGameControls();
}
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (!currentStatusText.toLowerCase().includes("отключился") && !clientState.currentGameState.isGameOver) {
ui.setGameStatusMessage("");
}
} else { // Если игра на паузе (по данным таймера)
disableGameControls();
}
}
});
// Начальная деактивация (на случай, если UI не скрыт изначально)
disableGameControls();
}