bc/public/js/gameplay.js

444 lines
26 KiB
JavaScript
Raw Permalink 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;
const attackButton = document.getElementById('button-attack');
const abilitiesGrid = document.getElementById('abilities-grid');
// Инициализируем флаг в clientState, если он еще не существует (лучше делать в main.js)
if (typeof clientState.isActionInProgress === 'undefined') {
clientState.isActionInProgress = false;
}
// --- Вспомогательные функции ---
function enableGameControls(enableAttack = true, enableAbilities = true) {
// console.log(`[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(`[GP] Action in progress, controls remain disabled.`);
if (window.gameUI?.updateUI) requestAnimationFrame(() => window.gameUI.updateUI());
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(`[GP] Controls set. Attack disabled: ${attackButton ? attackButton.disabled : 'N/A'}`);
if (window.gameUI?.updateUI) {
requestAnimationFrame(() => window.gameUI.updateUI()); // Обновляем UI, чтобы 반영 반영反映 изменения в disabled
}
}
function disableGameControls() {
// console.log(`[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; });
}
if (window.gameUI?.updateUI) {
requestAnimationFrame(() => window.gameUI.updateUI()); // Обновляем UI, чтобы 반영 반영反映 изменения в disabled
}
}
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;
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}] 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}] Emitting playerAction (ability: ${abilityId}). Setting isActionInProgress = true.`);
clientState.isActionInProgress = true; // <--- УСТАНОВКА ФЛАГА
disableGameControls(); // <--- БЛОКИРОВКА СРАЗУ
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
} else {
console.warn(`[CLIENT ${username}] Cannot perform ability action. Conditions not met or action in progress. InGame: ${clientState.isInGame}, GameOver: ${clientState.currentGameState?.isGameOver}, ActionInProgress: ${clientState.isActionInProgress}`);
}
}
// --- Обработчики событий DOM ---
if (attackButton) {
attackButton.addEventListener('click', () => {
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username}] Attack button clicked. isActionInProgress: ${clientState.isActionInProgress}`);
if (clientState.isLoggedIn &&
clientState.isInGame &&
clientState.currentGameId &&
clientState.currentGameState &&
!clientState.currentGameState.isGameOver &&
!clientState.isActionInProgress) { // <--- ПРОВЕРКА ФЛАГА
console.log(`[CLIENT ${username}] Emitting playerAction (attack). Setting isActionInProgress = true.`);
clientState.isActionInProgress = true; // <--- УСТАНОВКА ФЛАГА
disableGameControls(); // <--- БЛОКИРОВКА СРАЗУ
socket.emit('playerAction', { actionType: 'attack' });
} else {
console.warn(`[CLIENT ${username}] 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', () => {
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] handleGameDataReceived (${eventName}) called, but client not logged in. Ignoring.`);
return;
}
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username}] 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;
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' }; // Базовый конфиг
}
ui.updateGlobalWindowVariablesForUI();
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 = '';
}
if (window.gameUI?.addToLog && data.log) {
data.log.forEach(logEntry => {
window.gameUI.addToLog(logEntry.message, logEntry.type);
});
}
requestAnimationFrame(() => {
if (window.gameUI?.updateUI) {
window.gameUI.updateUI();
}
if (clientState.isInGame && clientState.currentGameState && !clientState.currentGameState.isGameOver && window.GAME_CONFIG) {
const config = window.GAME_CONFIG;
const isMyActualTurn = clientState.myPlayerId &&
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
console.log(`[CLIENT ${username}] handleGameDataReceived - Determining controls. isMyActualTurn: ${isMyActualTurn}`);
if (isMyActualTurn) {
enableGameControls();
} else {
disableGameControls();
}
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
console.log(`[CLIENT ${username}] handleGameDataReceived - Game is over, disabling controls.`);
disableGameControls();
}
});
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
// Обработка gameOver уже есть в своем обработчике
} else if (eventName === 'gameStarted' || eventName === 'gameState (reconnect)') {
console.log(`[CLIENT ${username}] ${eventName} - Clearing game status message because it's a fresh game/state load.`);
ui.setGameStatusMessage("");
} else {
if (clientState.isInGame) {
// Если это просто gameStateUpdate, и игра активна, убедимся, что нет сообщения об ожидании
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (!currentStatusText.toLowerCase().includes("отключился")) { // Не стираем сообщение об отключении оппонента
ui.setGameStatusMessage("");
}
}
}
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
if (window.gameUI?.showGameOver && !document.getElementById('game-over-screen').classList.contains(window.GAME_CONFIG?.CSS_CLASS_HIDDEN || 'hidden')) {
// Экран уже показан
} else if (window.gameUI?.showGameOver) {
let playerWon = false;
if (data.winnerId) {
playerWon = data.winnerId === clientState.myPlayerId;
} else if (clientState.currentGameState.player && clientState.currentGameState.opponent) {
// Дополнительная логика определения победителя, если winnerId нет (маловероятно при корректной работе сервера)
if (clientState.currentGameState.player.currentHp > 0 && clientState.currentGameState.opponent.currentHp <=0) {
playerWon = clientState.myPlayerId === clientState.currentGameState.player.id;
} else if (clientState.currentGameState.opponent.currentHp > 0 && clientState.currentGameState.player.currentHp <=0) {
playerWon = clientState.myPlayerId === clientState.currentGameState.opponent.id;
}
}
window.gameUI.showGameOver(playerWon, data.reason || "Игра завершена", clientState.opponentCharacterKey || data.loserCharacterKey, { finalGameState: clientState.currentGameState, ...data });
}
if (returnToMenuButton) returnToMenuButton.disabled = false;
}
}
// --- Обработчики событий Socket.IO ---
socket.on('gameStarted', (data) => {
handleGameDataReceived(data, 'gameStarted');
});
socket.on('gameState', (data) => {
handleGameDataReceived(data, 'gameState (reconnect)');
});
socket.on('gameStateUpdate', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
const username = clientState.loggedInUsername || 'N/A';
console.log(`[CLIENT ${username}] Event: gameStateUpdate. GS.isPlayerTurn: ${data.gameState?.isPlayerTurn}`);
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}] 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}] gameStateUpdate - Game is over, 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;
// const username = clientState.loggedInUsername || 'N/A';
// console.log(`[CLIENT ${username}] Event: logUpdate. Logs:`, data.log);
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}] 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}] 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}] Event: playerReconnected. PlayerID: ${data.reconnectedPlayerId}, Name: ${data.reconnectedPlayerName}`);
// const name = data.reconnectedPlayerName || clientState.opponentBaseStatsServer?.name || 'Противник';
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
// Сообщение о переподключении оппонента обычно приходит через 'logUpdate'
// Но если нужно немедленно убрать статус "Ожидание...", можно сделать здесь:
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (currentStatusText.toLowerCase().includes("отключился")) {
ui.setGameStatusMessage(""); // Очищаем сообщение об ожидании
}
// Логика enable/disableGameControls будет вызвана следующим gameStateUpdate или turnTimerUpdate
}
});
socket.on('turnTimerUpdate', (data) => {
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
if (window.gameUI?.updateTurnTimerDisplay && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
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);
}
// disableGameControls() уже должен быть вызван в gameOver
return;
}
// const username = clientState.loggedInUsername || 'N/A';
// console.log(`[CLIENT ${username}] Event: turnTimerUpdate. Remaining: ${data.remainingTime}, isPlayerTurnForTimer: ${data.isPlayerTurn}, isPaused: ${data.isPaused}`);
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
const config = window.GAME_CONFIG;
const isMyTurnForTimer = clientState.myPlayerId && clientState.currentGameState &&
((data.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) || // Серверное data.isPlayerTurn здесь авторитетно для таймера
(!data.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyTurnForTimer, clientState.currentGameState.gameMode);
// Если игра НЕ на паузе (серверной или клиентской из-за дисконнекта оппонента)
if (!data.isPaused) {
// Управление кнопками должно быть на основе isPlayerTurn из gameState, а не из turnTimerUpdate
// gameStateUpdate обработает это. Здесь только если нужно немедленно реагировать на isPlayerTurn из таймера,
// но это может привести к конфликтам с gameState.isPlayerTurn.
// Лучше положиться на gameStateUpdate.
// Однако, если ТАЙМЕР НЕ ПРИОСТАНОВЛЕН и это МОЙ ХОД по таймеру, то кнопки должны быть активны.
// Это может быть полезно, если gameStateUpdate запаздывает.
if (isMyTurnForTimer && !clientState.currentGameState.isGameOver) { // Дополнительная проверка на GameOver
enableGameControls();
} else if (!isMyTurnForTimer && !clientState.currentGameState.isGameOver){ // Иначе, если не мой ход
disableGameControls();
}
const statusMsgElement = document.getElementById('game-status-message');
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
if (!currentStatusText.toLowerCase().includes("отключился") && !clientState.currentGameState.isGameOver) {
// console.log(`[CLIENT ${username}] turnTimerUpdate - Clearing game status message as timer is active and not paused.`);
ui.setGameStatusMessage("");
}
} else { // Если игра на паузе (по данным таймера)
// console.log(`[CLIENT ${username}] turnTimerUpdate - Game is paused, disabling controls.`);
disableGameControls(); // Отключаем управление, если таймер говорит, что игра на паузе
}
}
});
// Начальная деактивация (на всякий случай, хотя showAuthScreen/showGameSelectionScreen должны это делать)
disableGameControls();
}