Обработка ситуаций рекконекта.
This commit is contained in:
parent
ab03488c95
commit
59ac3520f1
@ -1,4 +1,4 @@
|
||||
// /public/js/gameplay.js (Откаченная версия, совместимая с последним GameInstance.js)
|
||||
// /public/js/gameplay.js
|
||||
|
||||
export function initGameplay(dependencies) {
|
||||
const { socket, clientState, ui } = dependencies;
|
||||
@ -7,21 +7,49 @@ export function initGameplay(dependencies) {
|
||||
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());
|
||||
requestAnimationFrame(() => window.gameUI.updateUI()); // Обновляем UI, чтобы 반영 반영反映 изменения в disabled
|
||||
}
|
||||
}
|
||||
|
||||
function disableGameControls() {
|
||||
enableGameControls(false, false);
|
||||
// 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() {
|
||||
@ -63,31 +91,45 @@ export function initGameplay(dependencies) {
|
||||
|
||||
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.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 });
|
||||
disableGameControls();
|
||||
} else {
|
||||
console.warn("Cannot perform ability action, invalid state");
|
||||
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.currentGameState.isGameOver &&
|
||||
!clientState.isActionInProgress) { // <--- ПРОВЕРКА ФЛАГА
|
||||
|
||||
console.log(`[CLIENT ${username}] Emitting playerAction (attack). Setting isActionInProgress = true.`);
|
||||
clientState.isActionInProgress = true; // <--- УСТАНОВКА ФЛАГА
|
||||
disableGameControls(); // <--- БЛОКИРОВКА СРАЗУ
|
||||
socket.emit('playerAction', { actionType: 'attack' });
|
||||
disableGameControls();
|
||||
} else {
|
||||
console.warn("Cannot perform attack action, invalid state.");
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -98,7 +140,8 @@ export function initGameplay(dependencies) {
|
||||
ui.showAuthScreen();
|
||||
return;
|
||||
}
|
||||
returnToMenuButton.disabled = true;
|
||||
returnToMenuButton.disabled = true; // Блокируем сразу, чтобы избежать двойных кликов
|
||||
clientState.isActionInProgress = false; // Сбрасываем на всякий случай, если покидаем игру
|
||||
clientState.isInGame = false;
|
||||
disableGameControls();
|
||||
ui.showGameSelectionScreen(clientState.loggedInUsername);
|
||||
@ -108,11 +151,14 @@ export function initGameplay(dependencies) {
|
||||
|
||||
// --- ОБЩИЙ ОБРАБОТЧИК ДЛЯ ЗАПУСКА/ВОССТАНОВЛЕНИЯ ИГРЫ ---
|
||||
function handleGameDataReceived(data, eventName = "unknown") {
|
||||
if (!clientState.isLoggedIn) return;
|
||||
const username = clientState.loggedInUsername || 'N/A'; // Для логов
|
||||
console.log(`[CLIENT ${username}] ${eventName} received.`);
|
||||
// if (data.log) console.log(`[CLIENT ${username}] ${eventName} log content:`, JSON.parse(JSON.stringify(data.log)));
|
||||
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;
|
||||
@ -133,7 +179,7 @@ export function initGameplay(dependencies) {
|
||||
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' };
|
||||
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; // Базовый конфиг
|
||||
}
|
||||
ui.updateGlobalWindowVariablesForUI();
|
||||
|
||||
@ -148,13 +194,10 @@ export function initGameplay(dependencies) {
|
||||
initializeAbilityButtons();
|
||||
|
||||
if (window.gameUI?.uiElements?.log?.list) {
|
||||
// console.log(`[CLIENT ${username}] Log BEFORE clear in ${eventName}:`, window.gameUI.uiElements.log.list.innerHTML.substring(0,100));
|
||||
window.gameUI.uiElements.log.list.innerHTML = ''; // Очищаем UI-лог перед добавлением новых
|
||||
// console.log(`[CLIENT ${username}] Log AFTER clear in ${eventName}:`, window.gameUI.uiElements.log.list.innerHTML);
|
||||
window.gameUI.uiElements.log.list.innerHTML = '';
|
||||
}
|
||||
if (window.gameUI?.addToLog && data.log) {
|
||||
data.log.forEach(logEntry => {
|
||||
// console.log(`[CLIENT ${username}] Adding to UI log from ${eventName}: "${logEntry.message}"`);
|
||||
window.gameUI.addToLog(logEntry.message, logEntry.type);
|
||||
});
|
||||
}
|
||||
@ -168,45 +211,50 @@ export function initGameplay(dependencies) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// Управление gameStatusMessage
|
||||
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||
// gameOver имеет свой обработчик статуса (внутри socket.on('gameOver',...))
|
||||
// Обработка 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 {
|
||||
// Для gameStateUpdate и других событий, не являющихся полной перезагрузкой,
|
||||
// gameStatusMessage будет управляться в их обработчиках или через turnTimerUpdate.
|
||||
// Если игра продолжается и не gameOver, общее сообщение "Ожидание" должно сниматься.
|
||||
if (clientState.isInGame) {
|
||||
ui.setGameStatusMessage("");
|
||||
// Если это просто gameStateUpdate, и игра активна, убедимся, что нет сообщения об ожидании
|
||||
const statusMsgElement = document.getElementById('game-status-message');
|
||||
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
|
||||
if (!currentStatusText.toLowerCase().includes("отключился")) { // Не стираем сообщение об отключении оппонента
|
||||
ui.setGameStatusMessage("");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если игра пришла завершенной, то showGameOver должен быть вызван.
|
||||
if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||
if (window.gameUI?.showGameOver && !document.getElementById('game-over-screen').classList.contains('hidden')) {
|
||||
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, { finalGameState: clientState.currentGameState, ...data });
|
||||
window.gameUI.showGameOver(playerWon, data.reason || "Игра завершена", clientState.opponentCharacterKey || data.loserCharacterKey, { finalGameState: clientState.currentGameState, ...data });
|
||||
}
|
||||
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||
}
|
||||
@ -218,15 +266,16 @@ export function initGameplay(dependencies) {
|
||||
handleGameDataReceived(data, 'gameStarted');
|
||||
});
|
||||
|
||||
socket.on('gameState', (data) => { // Это событие было добавлено для поддержки reconnect из старого GameInstance
|
||||
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.`);
|
||||
console.log(`[CLIENT ${username}] Event: gameStateUpdate. GS.isPlayerTurn: ${data.gameState?.isPlayerTurn}`);
|
||||
|
||||
clientState.isActionInProgress = false; // <--- СБРОС ФЛАГА
|
||||
clientState.currentGameState = data.gameState;
|
||||
ui.updateGlobalWindowVariablesForUI();
|
||||
|
||||
@ -239,17 +288,22 @@ export function initGameplay(dependencies) {
|
||||
((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();
|
||||
}
|
||||
|
||||
console.log(`[CLIENT ${username}] gameStateUpdate - Clearing game status message as game is active.`);
|
||||
ui.setGameStatusMessage(""); // Очищаем статус, если игра активна
|
||||
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) {
|
||||
disableGameControls(); // Отключаем управление, если игра закончилась этим обновлением
|
||||
console.log(`[CLIENT ${username}] gameStateUpdate - Game is over, disabling controls.`);
|
||||
disableGameControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -261,7 +315,7 @@ export function initGameplay(dependencies) {
|
||||
|
||||
socket.on('logUpdate', (data) => {
|
||||
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
|
||||
const username = clientState.loggedInUsername || 'N/A';
|
||||
// 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));
|
||||
@ -270,106 +324,121 @@ export function initGameplay(dependencies) {
|
||||
|
||||
socket.on('gameOver', (data) => {
|
||||
if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
|
||||
// Если нет ID игры, но залогинен, возможно, стоит запросить состояние
|
||||
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
|
||||
else if (!clientState.isLoggedIn) ui.showAuthScreen(); // Если не залогинен, показать экран входа
|
||||
else if (!clientState.isLoggedIn) ui.showAuthScreen();
|
||||
return;
|
||||
}
|
||||
const username = clientState.loggedInUsername || 'N/A';
|
||||
console.log(`[CLIENT ${username}] Event: gameOver.`);
|
||||
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; // Игра точно закончена
|
||||
clientState.currentGameState = data.finalGameState;
|
||||
clientState.isInGame = false;
|
||||
|
||||
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
|
||||
ui.updateGlobalWindowVariablesForUI();
|
||||
|
||||
if (window.gameUI?.updateUI) requestAnimationFrame(() => window.gameUI.updateUI()); // Обновляем UI один раз
|
||||
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.opponentBaseStatsServer?.characterKey; // Используем сохраненные данные оппонента
|
||||
const oppKey = clientState.opponentCharacterKey || data.loserCharacterKey;
|
||||
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
|
||||
}
|
||||
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||
// `ui.setGameStatusMessage` будет установлено специфичным сообщением о результате игры
|
||||
// ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
||||
|
||||
if (window.gameUI?.updateTurnTimerDisplay) {
|
||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode); // Сбрасываем таймер
|
||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
|
||||
}
|
||||
disableGameControls(); // Отключаем управление игрой
|
||||
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.`);
|
||||
console.log(`[CLIENT ${username}] Event: opponentDisconnected. PlayerID: ${data.disconnectedPlayerId}`);
|
||||
const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник';
|
||||
|
||||
// Сообщение об отключении оппонента должно приходить через 'logUpdate' от сервера
|
||||
// if (window.gameUI?.addToLog) {
|
||||
// window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
|
||||
// }
|
||||
|
||||
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true); // Показываем сообщение ожидания
|
||||
disableGameControls(); // Отключаем управление на время ожидания
|
||||
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) => {
|
||||
// Проверяем, в игре ли мы и есть ли gameState, прежде чем обновлять таймер
|
||||
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
|
||||
// Если не в игре, но gameState есть (например, игра завершена, но экран еще не обновился),
|
||||
// то таймер нужно сбросить/скрыть.
|
||||
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(); // Убедимся, что управление отключено
|
||||
// disableGameControls() уже должен быть вызван в gameOver
|
||||
return;
|
||||
}
|
||||
// const username = clientState.loggedInUsername || 'N/A';
|
||||
// console.log(`[CLIENT ${username}] Event: turnTimerUpdate. Remaining: ${data.remainingTime}, isPlayerTurnForTimer: ${data.isPlayerTurn}, isPaused: ${data.isPaused}`);
|
||||
|
||||
const username = clientState.loggedInUsername || 'N/A';
|
||||
// console.log(`[CLIENT ${username}] Event: turnTimerUpdate.`);
|
||||
|
||||
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
|
||||
const config = window.GAME_CONFIG;
|
||||
const isMyActualTurn = clientState.myPlayerId && clientState.currentGameState &&
|
||||
((clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.PLAYER_ID) ||
|
||||
(!clientState.currentGameState.isPlayerTurn && clientState.myPlayerId === config.OPPONENT_ID));
|
||||
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, isMyActualTurn, clientState.currentGameState.gameMode);
|
||||
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyTurnForTimer, clientState.currentGameState.gameMode);
|
||||
|
||||
// Включаем/отключаем управление в зависимости от хода
|
||||
if (isMyActualTurn) {
|
||||
enableGameControls();
|
||||
} else {
|
||||
disableGameControls();
|
||||
}
|
||||
// Если игра НЕ на паузе (серверной или клиентской из-за дисконнекта оппонента)
|
||||
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();
|
||||
}
|
||||
|
||||
// Если таймер активен и игра не закончена, общее сообщение "Ожидание" должно быть снято
|
||||
// (если оно не специфично для дисконнекта оппонента)
|
||||
if (!clientState.currentGameState.isGameOver) {
|
||||
// Проверяем, не показывается ли уже сообщение о дисконнекте оппонента
|
||||
const statusMsgElement = document.getElementById('game-status-message');
|
||||
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
|
||||
if (!currentStatusText.toLowerCase().includes("отключился")) {
|
||||
console.log(`[CLIENT ${username}] turnTimerUpdate - Clearing game status message as timer is active.`);
|
||||
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();
|
||||
}
|
@ -9,12 +9,10 @@ import { initGameplay } from './gameplay.js';
|
||||
function parseJwtPayloadForValidation(token) {
|
||||
try {
|
||||
if (typeof token !== 'string') {
|
||||
// console.warn("[Main.js parseJwtPayloadForValidation] Token is not a string:", token);
|
||||
return null;
|
||||
}
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
// console.warn("[Main.js parseJwtPayloadForValidation] Token does not have 3 parts:", token);
|
||||
return null;
|
||||
}
|
||||
const base64Url = parts[1];
|
||||
@ -31,22 +29,18 @@ function parseJwtPayloadForValidation(token) {
|
||||
|
||||
function isTokenValid(token) {
|
||||
if (!token) {
|
||||
// console.log("[Main.js isTokenValid] No token provided.");
|
||||
return false;
|
||||
}
|
||||
const decodedToken = parseJwtPayloadForValidation(token);
|
||||
if (!decodedToken || typeof decodedToken.exp !== 'number') {
|
||||
// console.warn("[Main.js isTokenValid] Token invalid or no 'exp' field. Clearing token from storage.");
|
||||
localStorage.removeItem('jwtToken'); // Удаляем невалидный токен
|
||||
localStorage.removeItem('jwtToken');
|
||||
return false;
|
||||
}
|
||||
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
|
||||
if (decodedToken.exp < currentTimeInSeconds) {
|
||||
// console.warn("[Main.js isTokenValid] Token expired. Clearing token from storage.");
|
||||
localStorage.removeItem('jwtToken'); // Удаляем истекший токен
|
||||
localStorage.removeItem('jwtToken');
|
||||
return false;
|
||||
}
|
||||
// console.log("[Main.js isTokenValid] Token is valid.");
|
||||
return true;
|
||||
}
|
||||
// --- КОНЕЦ ВСПОМОГАТЕЛЬНЫХ ФУНКЦИЙ ДЛЯ JWT ---
|
||||
@ -65,31 +59,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
isInGame: false,
|
||||
currentGameId: null,
|
||||
currentGameState: null,
|
||||
myPlayerId: null,
|
||||
myPlayerId: null, // Роль в текущей игре (player/opponent)
|
||||
myCharacterKey: null,
|
||||
opponentCharacterKey: null,
|
||||
playerBaseStatsServer: null,
|
||||
opponentBaseStatsServer: null,
|
||||
playerAbilitiesServer: null,
|
||||
opponentAbilitiesServer: null,
|
||||
isActionInProgress: false, // <--- ВАЖНО: Флаг для предотвращения двойных действий
|
||||
};
|
||||
|
||||
// Проверяем валидность initialToken перед установкой clientState
|
||||
if (initialToken && isTokenValid(initialToken)) { // Используем нашу новую функцию
|
||||
const decodedToken = parseJwtPayloadForValidation(initialToken); // Повторно парсим, т.к. isTokenValid не возвращает payload
|
||||
if (initialToken && isTokenValid(initialToken)) {
|
||||
const decodedToken = parseJwtPayloadForValidation(initialToken);
|
||||
if (decodedToken && decodedToken.userId && decodedToken.username) {
|
||||
console.log("[Main.js] Token found and confirmed valid, pre-populating clientState:", decodedToken);
|
||||
clientState.isLoggedIn = true;
|
||||
clientState.myUserId = decodedToken.userId;
|
||||
clientState.myUserId = decodedToken.userId; // Это ID пользователя из БД
|
||||
clientState.loggedInUsername = decodedToken.username;
|
||||
} else {
|
||||
// Этого не должно случиться, если isTokenValid прошла, но на всякий случай
|
||||
console.warn("[Main.js] Token deemed valid by isTokenValid, but payload incomplete. Clearing.");
|
||||
localStorage.removeItem('jwtToken');
|
||||
}
|
||||
} else if (initialToken) { // Токен был, но isTokenValid его отверг (и удалил)
|
||||
} else if (initialToken) {
|
||||
console.warn("[Main.js] Initial token was present but invalid/expired. It has been cleared.");
|
||||
// clientState остается по умолчанию (isLoggedIn: false)
|
||||
} else {
|
||||
console.log("[Main.js] No initial token found in localStorage.");
|
||||
}
|
||||
@ -98,9 +90,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
console.log('[Main.js] Initializing Socket.IO client...');
|
||||
const socket = io({
|
||||
path:base_path + "/socket.io",
|
||||
autoConnect: false,
|
||||
auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null, если был очищен)
|
||||
path:base_path + "/socket.io", // base_path определяется в HTML
|
||||
autoConnect: false, // Подключаемся вручную после инициализации всего
|
||||
auth: { token: localStorage.getItem('jwtToken') }
|
||||
});
|
||||
console.log('[Main.js] Socket.IO client initialized.');
|
||||
|
||||
@ -110,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const registerForm = document.getElementById('register-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');
|
||||
@ -121,18 +113,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
|
||||
const gameIdInput = document.getElementById('game-id-input');
|
||||
const availableGamesDiv = document.getElementById('available-games-list');
|
||||
const gameStatusMessage = document.getElementById('game-status-message');
|
||||
const gameStatusMessage = document.getElementById('game-status-message'); // Сообщение на экране выбора игры
|
||||
const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]');
|
||||
const gameWrapper = document.querySelector('.game-wrapper');
|
||||
const returnToMenuButton = document.getElementById('return-to-menu-button');
|
||||
const returnToMenuButton = document.getElementById('return-to-menu-button'); // Кнопка в gameOver модальном окне
|
||||
const turnTimerContainer = document.getElementById('turn-timer-container');
|
||||
const turnTimerSpan = document.getElementById('turn-timer');
|
||||
|
||||
console.log('[Main.js DOM Check] authSection:', !!authSection);
|
||||
console.log('[Main.js DOM Check] loginForm:', !!loginForm);
|
||||
console.log('[Main.js DOM Check] registerForm:', !!registerForm);
|
||||
console.log('[Main.js DOM Check] logoutButton:', !!logoutButton);
|
||||
|
||||
// --- Функции обновления UI и состояния ---
|
||||
function updateGlobalWindowVariablesForUI() {
|
||||
window.gameState = clientState.currentGameState;
|
||||
@ -142,7 +129,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
playerAbilities: clientState.playerAbilitiesServer,
|
||||
opponentAbilities: clientState.opponentAbilitiesServer
|
||||
};
|
||||
window.myPlayerId = clientState.myPlayerId;
|
||||
window.myPlayerId = clientState.myPlayerId; // Роль игрока (player/opponent)
|
||||
}
|
||||
|
||||
function resetGameVariables() {
|
||||
@ -156,6 +143,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
clientState.opponentBaseStatsServer = null;
|
||||
clientState.playerAbilitiesServer = null;
|
||||
clientState.opponentAbilitiesServer = null;
|
||||
clientState.isActionInProgress = false; // <--- Сброс флага
|
||||
updateGlobalWindowVariablesForUI();
|
||||
console.log("[Main.js resetGameVariables] Game variables reset. State AFTER:", JSON.parse(JSON.stringify(clientState)));
|
||||
}
|
||||
@ -180,20 +168,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function showAuthScreen() {
|
||||
console.log("[Main.js showAuthScreen] Showing Auth Screen. Resetting game state if not already done.");
|
||||
console.log("[Main.js showAuthScreen] Showing Auth Screen. Resetting game state.");
|
||||
if(authSection) authSection.style.display = 'block';
|
||||
if(userInfoDiv) userInfoDiv.style.display = 'none';
|
||||
if(gameSetupDiv) gameSetupDiv.style.display = 'none';
|
||||
if(gameWrapper) gameWrapper.style.display = 'none';
|
||||
explicitlyHideGameOverModal();
|
||||
if(statusContainer) statusContainer.style.display = 'block';
|
||||
if(statusContainer) statusContainer.style.display = 'block'; // Показываем общий контейнер для сообщений
|
||||
clientState.isInGame = false;
|
||||
resetGameVariables();
|
||||
resetGameVariables(); // Включает сброс isActionInProgress
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
if(registerForm && registerForm.querySelector('button')) registerForm.querySelector('button').disabled = false;
|
||||
if(loginForm && loginForm.querySelector('button')) loginForm.querySelector('button').disabled = false;
|
||||
if(logoutButton) logoutButton.disabled = true;
|
||||
if(logoutButton) logoutButton.disabled = true; // Кнопка выхода неактивна на экране логина
|
||||
}
|
||||
|
||||
function showGameSelectionScreen(username) {
|
||||
@ -213,34 +201,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.emit('requestPvPGameList');
|
||||
} else {
|
||||
console.warn("[Main.js showGameSelectionScreen] Socket not connected, cannot request PvP game list yet.");
|
||||
// Можно попробовать подключить сокет, если он не подключен
|
||||
// socket.connect(); // Или дождаться авто-реконнекта
|
||||
}
|
||||
|
||||
if (availableGamesDiv) availableGamesDiv.innerHTML = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>';
|
||||
if (gameIdInput) gameIdInput.value = '';
|
||||
const elenaRadio = document.getElementById('char-elena');
|
||||
if (elenaRadio) elenaRadio.checked = true;
|
||||
if (elenaRadio) elenaRadio.checked = true; // Персонаж по умолчанию
|
||||
clientState.isInGame = false;
|
||||
clientState.isActionInProgress = false; // <--- Сброс флага при переходе в меню
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
enableSetupButtons();
|
||||
enableSetupButtons(); // Включаем кнопки настройки игры
|
||||
if (window.gameUI?.uiElements?.gameOver?.returnToMenuButton) {
|
||||
window.gameUI.uiElements.gameOver.returnToMenuButton.disabled = false;
|
||||
window.gameUI.uiElements.gameOver.returnToMenuButton.disabled = false; // Убедимся, что кнопка в модалке gameOver активна
|
||||
}
|
||||
}
|
||||
|
||||
function showGameScreen() {
|
||||
console.log("[Main.js showGameScreen] Showing Game Screen.");
|
||||
if(authSection) authSection.style.display = 'none';
|
||||
if(userInfoDiv) userInfoDiv.style.display = 'block';
|
||||
if(userInfoDiv) userInfoDiv.style.display = 'block'; // userInfo (имя, выход) остается видимым
|
||||
if(logoutButton) logoutButton.disabled = false;
|
||||
if(gameSetupDiv) gameSetupDiv.style.display = 'none';
|
||||
if(gameWrapper) gameWrapper.style.display = 'flex';
|
||||
setGameStatusMessage("");
|
||||
if(statusContainer) statusContainer.style.display = 'none';
|
||||
if(gameWrapper) gameWrapper.style.display = 'flex'; // Используем flex для game-wrapper
|
||||
setGameStatusMessage(""); // Очищаем сообщение статуса игры при входе на экран игры
|
||||
if(statusContainer) statusContainer.style.display = 'none'; // Скрываем общий статус-контейнер на игровом экране
|
||||
clientState.isInGame = true;
|
||||
// clientState.isActionInProgress остается false до первого действия игрока
|
||||
updateGlobalWindowVariablesForUI();
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'block';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Таймер обновится по событию
|
||||
}
|
||||
|
||||
function setAuthMessage(message, isError = false) {
|
||||
@ -250,7 +242,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
authMessage.className = isError ? 'error' : 'success';
|
||||
authMessage.style.display = message ? 'block' : 'none';
|
||||
}
|
||||
if (message && gameStatusMessage && gameStatusMessage.style.display !== 'none') gameStatusMessage.style.display = 'none';
|
||||
// Если показываем authMessage, скрываем gameStatusMessage
|
||||
if (message && gameStatusMessage && gameStatusMessage.style.display !== 'none') {
|
||||
gameStatusMessage.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setGameStatusMessage(message, isError = false) {
|
||||
@ -258,10 +253,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (gameStatusMessage) {
|
||||
gameStatusMessage.textContent = message;
|
||||
gameStatusMessage.style.display = message ? 'block' : 'none';
|
||||
gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
|
||||
gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)'; // или другой цвет для обычных сообщений
|
||||
// Управляем видимостью общего контейнера статуса
|
||||
if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
|
||||
}
|
||||
if (message && authMessage && authMessage.style.display !== 'none') authMessage.style.display = 'none';
|
||||
// Если показываем gameStatusMessage, скрываем authMessage
|
||||
if (message && authMessage && authMessage.style.display !== 'none') {
|
||||
authMessage.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function disableSetupButtons() {
|
||||
@ -276,29 +275,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if(createPvPGameButton) createPvPGameButton.disabled = false;
|
||||
if(joinPvPGameButton) joinPvPGameButton.disabled = false;
|
||||
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
|
||||
// Кнопки в списке доступных игр управляются в updateAvailableGamesList
|
||||
// Кнопки в списке доступных игр управляются в updateAvailableGamesList в gameSetup.js
|
||||
}
|
||||
|
||||
// --- НОВАЯ ФУНКЦИЯ ДЛЯ ПЕРЕНАПРАВЛЕНИЯ НА ЛОГИН ---
|
||||
function redirectToLogin(message) {
|
||||
console.log(`[Main.js redirectToLogin] Redirecting to login. Message: "${message}"`);
|
||||
clientState.isLoggedIn = false;
|
||||
clientState.loggedInUsername = '';
|
||||
clientState.myUserId = null;
|
||||
clientState.isInGame = false; // Важно сбросить, если он пытался войти в игру
|
||||
clientState.isInGame = false;
|
||||
localStorage.removeItem('jwtToken');
|
||||
resetGameVariables(); // Сбрасываем все игровые переменные
|
||||
resetGameVariables(); // Сбрасываем все игровые переменные, включая isActionInProgress
|
||||
|
||||
if (socket.auth) socket.auth.token = null;
|
||||
if (socket.auth) socket.auth.token = null; // Обновляем auth объект сокета
|
||||
if (socket.connected) {
|
||||
console.log("[Main.js redirectToLogin] Socket connected, disconnecting before showing auth screen.");
|
||||
socket.disconnect(); // Отключаем текущий сокет, чтобы он не пытался переподключиться с невалидными данными
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
showAuthScreen();
|
||||
setAuthMessage(message || "Для продолжения необходимо войти или обновить сессию.", true);
|
||||
}
|
||||
// --- КОНЕЦ НОВОЙ ФУНКЦИИ ---
|
||||
|
||||
// --- Сборка зависимостей для модулей ---
|
||||
console.log('[Main.js] Preparing dependencies for modules...');
|
||||
@ -315,16 +312,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateGlobalWindowVariablesForUI,
|
||||
disableSetupButtons,
|
||||
enableSetupButtons,
|
||||
redirectToLogin, // <-- ДОБАВЛЕНО
|
||||
redirectToLogin,
|
||||
elements: {
|
||||
loginForm, registerForm, logoutButton,
|
||||
createAIGameButton, createPvPGameButton, joinPvPGameButton,
|
||||
findRandomPvPGameButton, gameIdInput, availableGamesDiv,
|
||||
pvpCharacterRadios, returnToMenuButton,
|
||||
// Не передаем сюда все элементы из ui.js, так как ui.js сам их менеджит.
|
||||
// Если какой-то модуль должен напрямую менять что-то из ui.js.uiElements,
|
||||
// то можно передать ui.js.uiElements целиком или конкретные элементы.
|
||||
}
|
||||
},
|
||||
utils: { // <-- ДОБАВЛЕН ОБЪЕКТ UTILS
|
||||
isTokenValid // <-- ДОБАВЛЕНО
|
||||
utils: {
|
||||
isTokenValid,
|
||||
parseJwtPayloadForValidation // На всякий случай, если понадобится где-то еще
|
||||
}
|
||||
};
|
||||
|
||||
@ -338,28 +339,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- Обработчики событий Socket.IO ---
|
||||
socket.on('connect', () => {
|
||||
const currentToken = localStorage.getItem('jwtToken'); // Получаем актуальный токен
|
||||
socket.auth.token = currentToken; // Убедимся, что сокет использует актуальный токен для этого соединения
|
||||
// (хотя handshake.auth устанавливается при io(), это для ясности)
|
||||
const currentToken = localStorage.getItem('jwtToken');
|
||||
if (socket.auth) socket.auth.token = currentToken; // Убедимся, что auth объект сокета обновлен
|
||||
else socket.auth = { token: currentToken }; // Если auth объекта не было
|
||||
|
||||
console.log('[Main.js Socket.IO] Event: connect. Socket ID:', socket.id, 'Auth token associated with this connection attempt:', !!currentToken);
|
||||
|
||||
if (clientState.isLoggedIn && clientState.myUserId && isTokenValid(currentToken)) { // Дополнительная проверка токена
|
||||
if (clientState.isLoggedIn && clientState.myUserId && isTokenValid(currentToken)) {
|
||||
console.log(`[Main.js Socket.IO] Client state indicates logged in as ${clientState.loggedInUsername} (ID: ${clientState.myUserId}) and token is valid. Requesting game state.`);
|
||||
if (!clientState.isInGame && (authSection.style.display === 'block' || gameSetupDiv.style.display === 'block')) {
|
||||
// Если мы на экране выбора игры, показываем сообщение о восстановлении
|
||||
if (!clientState.isInGame && (gameSetupDiv.style.display === 'block' || authSection.style.display === 'block')) {
|
||||
setGameStatusMessage("Восстановление игровой сессии...");
|
||||
}
|
||||
socket.emit('requestGameState');
|
||||
} else {
|
||||
// Если clientState говорит, что залогинен, но токен невалиден, или если не залогинен
|
||||
if (clientState.isLoggedIn && !isTokenValid(currentToken)) {
|
||||
console.warn('[Main.js Socket.IO connect] Client state says logged in, but token is invalid/expired. Redirecting to login.');
|
||||
redirectToLogin("Ваша сессия истекла. Пожалуйста, войдите снова.");
|
||||
} else {
|
||||
console.log('[Main.js Socket.IO connect] Client state indicates NOT logged in or no valid token. Showing auth screen if not already visible.');
|
||||
if (authSection.style.display !== 'block') {
|
||||
showAuthScreen();
|
||||
showAuthScreen(); // Показываем экран логина, если еще не на нем
|
||||
}
|
||||
setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь.");
|
||||
setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь."); // Сообщение по умолчанию для экрана логина
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -378,12 +380,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let currentScreenMessageFunc = setAuthMessage;
|
||||
if (clientState.isLoggedIn && clientState.isInGame) {
|
||||
currentScreenMessageFunc = setGameStatusMessage;
|
||||
} else if (clientState.isLoggedIn) {
|
||||
} else if (clientState.isLoggedIn) { // Если залогинен, но не в игре (на экране выбора)
|
||||
currentScreenMessageFunc = setGameStatusMessage;
|
||||
}
|
||||
currentScreenMessageFunc(`Ошибка подключения: ${err.message}. Попытка переподключения...`, true);
|
||||
if (authSection.style.display !== 'block' && !clientState.isLoggedIn) {
|
||||
// Если не залогинены и не на экране логина, показать экран логина
|
||||
// Если не залогинены и не на экране авторизации, показываем его
|
||||
if (!clientState.isLoggedIn && authSection.style.display !== 'block') {
|
||||
showAuthScreen();
|
||||
}
|
||||
}
|
||||
@ -392,30 +394,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.warn('[Main.js Socket.IO] Event: disconnect. Reason:', reason);
|
||||
let messageFunc = setAuthMessage;
|
||||
if (clientState.isInGame) { // Если были в игре, сообщение на игровом статусе
|
||||
let messageFunc = setAuthMessage; // По умолчанию сообщение для экрана авторизации
|
||||
if (clientState.isInGame) {
|
||||
messageFunc = setGameStatusMessage;
|
||||
} else if (clientState.isLoggedIn && gameSetupDiv.style.display === 'block') { // Если были на экране выбора игры
|
||||
} else if (clientState.isLoggedIn && gameSetupDiv.style.display === 'block') {
|
||||
messageFunc = setGameStatusMessage;
|
||||
}
|
||||
|
||||
if (reason === 'io server disconnect') { // Сервер принудительно отключил
|
||||
if (reason === 'io server disconnect') {
|
||||
messageFunc("Соединение разорвано сервером. Пожалуйста, попробуйте войти снова.", true);
|
||||
// Можно сразу перенаправить на логин, если это означает проблему с сессией
|
||||
redirectToLogin("Соединение разорвано сервером. Пожалуйста, войдите снова.");
|
||||
} else if (reason !== 'io client disconnect') { // Если это не преднамеренный дисконнект клиента (например, при logout)
|
||||
} else if (reason === 'io client disconnect') {
|
||||
// Это преднамеренный дисконнект (например, при logout или смене токена).
|
||||
// Сообщение уже должно быть установлено функцией, вызвавшей дисконнект.
|
||||
// Ничего не делаем здесь, чтобы не перезаписать его.
|
||||
console.log('[Main.js Socket.IO] Disconnect was intentional (io client disconnect). No additional message needed.');
|
||||
} else { // Другие причины (например, проблемы с сетью)
|
||||
messageFunc(`Потеряно соединение: ${reason}. Попытка переподключения...`, true);
|
||||
}
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
|
||||
// Не вызываем redirectToLogin здесь автоматически при каждом дисконнекте,
|
||||
// так как Socket.IO будет пытаться переподключиться.
|
||||
// redirectToLogin будет вызван из connect_error или connect, если токен окажется невалидным.
|
||||
clientState.isActionInProgress = false; // На всякий случай сбрасываем флаг при дисконнекте
|
||||
});
|
||||
|
||||
socket.on('gameError', (data) => {
|
||||
console.error('[Main.js Socket.IO] Event: gameError. Message:', data.message, 'Data:', JSON.stringify(data));
|
||||
clientState.isActionInProgress = false; // Сбрасываем флаг при ошибке сервера
|
||||
|
||||
// Проверка на специфичные ошибки, требующие перелогина
|
||||
if (data.message && (data.message.toLowerCase().includes("сессия истекла") || data.message.toLowerCase().includes("необходимо войти"))) {
|
||||
redirectToLogin(data.message);
|
||||
return;
|
||||
@ -423,9 +427,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (clientState.isInGame && window.gameUI?.addToLog) {
|
||||
window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system');
|
||||
// Если ошибка произошла в игре, но игра не закончилась, кнопки могут остаться заблокированными.
|
||||
// Возможно, стоит проверить, чей ход, и разблокировать, если ход игрока и игра не окончена.
|
||||
// Но это зависит от типа ошибки. Сейчас просто логируем.
|
||||
} else if (clientState.isLoggedIn) {
|
||||
setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
|
||||
enableSetupButtons(); // Разблокируем кнопки, если произошла ошибка на экране выбора игры
|
||||
enableSetupButtons(); // Разблокируем кнопки, если ошибка на экране выбора игры
|
||||
} else {
|
||||
setAuthMessage(`❌ Ошибка: ${data.message}`, true);
|
||||
if(registerForm && registerForm.querySelector('button')) registerForm.querySelector('button').disabled = false;
|
||||
@ -436,17 +443,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.on('gameNotFound', (data) => {
|
||||
console.log('[Main.js Socket.IO] Event: gameNotFound. Message:', data?.message, 'Data:', JSON.stringify(data));
|
||||
clientState.isInGame = false;
|
||||
resetGameVariables();
|
||||
resetGameVariables(); // Включает сброс isActionInProgress
|
||||
explicitlyHideGameOverModal();
|
||||
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
|
||||
if (turnTimerSpan) turnTimerSpan.textContent = '--';
|
||||
|
||||
if (clientState.isLoggedIn && isTokenValid(localStorage.getItem('jwtToken'))) { // Проверяем, что токен еще валиден
|
||||
if (gameSetupDiv.style.display !== 'block') {
|
||||
if (clientState.isLoggedIn && isTokenValid(localStorage.getItem('jwtToken'))) {
|
||||
if (gameSetupDiv.style.display !== 'block') { // Если мы не на экране выбора игры, показываем его
|
||||
showGameSelectionScreen(clientState.loggedInUsername);
|
||||
}
|
||||
setGameStatusMessage(data?.message || "Активная игровая сессия не найдена. Выберите новую игру.");
|
||||
} else { // Если не залогинен или токен истек
|
||||
} else {
|
||||
redirectToLogin(data?.message || "Пожалуйста, войдите для продолжения.");
|
||||
}
|
||||
});
|
||||
@ -457,11 +464,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if(gameSetupDiv) gameSetupDiv.style.display = 'none';
|
||||
if(gameWrapper) gameWrapper.style.display = 'none';
|
||||
if(userInfoDiv) userInfoDiv.style.display = 'none';
|
||||
if(statusContainer) statusContainer.style.display = 'block';
|
||||
if(statusContainer) statusContainer.style.display = 'block'; // Показываем общий контейнер для сообщений
|
||||
|
||||
if (clientState.isLoggedIn) { // isLoggedIn уже учитывает валидность токена при начальной загрузке
|
||||
if (clientState.isLoggedIn) {
|
||||
console.log('[Main.js] Client is considered logged in. Will attempt session recovery on socket connect.');
|
||||
setGameStatusMessage("Подключение и восстановление сессии...");
|
||||
// Не показываем экран выбора игры сразу, дожидаемся 'connect' и 'requestGameState'
|
||||
setAuthMessage("Подключение и восстановление сессии..."); // Используем authMessage для начального сообщения
|
||||
} else {
|
||||
console.log('[Main.js] Client is NOT considered logged in. Showing auth screen.');
|
||||
showAuthScreen();
|
||||
@ -469,6 +477,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
console.log('[Main.js] Attempting to connect socket...');
|
||||
socket.connect();
|
||||
socket.connect(); // Подключаемся здесь, после всей инициализации
|
||||
console.log('[Main.js] socket.connect() called.');
|
||||
});
|
@ -10,7 +10,7 @@ class GameManager {
|
||||
this.games = {}; // { gameId: GameInstance }
|
||||
this.userIdentifierToGameId = {}; // { userId: gameId }
|
||||
this.pendingPvPGames = []; // Массив gameId ожидающих PvP игр
|
||||
console.log("[GameManager] Initialized.");
|
||||
console.log("[GameManager] Инициализирован.");
|
||||
}
|
||||
|
||||
_cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') {
|
||||
@ -24,89 +24,71 @@ class GameManager {
|
||||
this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих
|
||||
(!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена
|
||||
) {
|
||||
console.log(`[GameManager._cleanupPreviousPendingGameForUser] User ${identifier} has an existing pending PvP game ${oldPendingGameId}. Removing it. Reason: ${reasonSuffix}`);
|
||||
console.log(`[GameManager._cleanupPreviousPendingGameForUser] Пользователь ${identifier} имеет существующую ожидающую PvP игру ${oldPendingGameId}. Удаление. Причина: ${reasonSuffix}`);
|
||||
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_game_${reasonSuffix}`);
|
||||
// _cleanupGame должен удалить запись из userIdentifierToGameId
|
||||
return true; // Успешно очистили
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // Нечего было очищать или условия не совпали
|
||||
return false;
|
||||
}
|
||||
|
||||
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
|
||||
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
|
||||
console.log(`[GameManager.createGame] Пользователь: ${identifier} (Socket: ${socket.id}), Режим: ${mode}, Персонаж: ${chosenCharacterKey || 'По умолчанию'}`);
|
||||
|
||||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||||
|
||||
// 1. Проверить, не находится ли пользователь уже в какой-либо АКТИВНОЙ игре.
|
||||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
||||
const existingGame = this.games[existingGameIdForUser];
|
||||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
||||
console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up before creating new.`);
|
||||
console.warn(`[GameManager.createGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка перед созданием новой.`);
|
||||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`);
|
||||
// После _cleanupGame, existingGameIdForUser в userIdentifierToGameId[identifier] должен быть удален
|
||||
} else {
|
||||
// Пользователь в активной игре.
|
||||
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, и он пытается создать НОВУЮ (любую), то ее нужно будет удалить ниже.
|
||||
// Если это ДРУГАЯ активная игра (не его ожидающая PvP), то отказать.
|
||||
const isHisOwnPendingPvp = existingGame.mode === 'pvp' &&
|
||||
existingGame.ownerIdentifier === identifier &&
|
||||
existingGame.playerCount === 1 &&
|
||||
this.pendingPvPGames.includes(existingGameIdForUser);
|
||||
|
||||
if (!isHisOwnPendingPvp) {
|
||||
// Он в другой активной игре (AI, или PvP с оппонентом, или PvP другого игрока)
|
||||
console.warn(`[GameManager.createGame] User ${identifier} is already in an active game ${existingGameIdForUser} (mode: ${existingGame.mode}, owner: ${existingGame.ownerIdentifier}). Cannot create new.`);
|
||||
console.warn(`[GameManager.createGame] Пользователь ${identifier} уже в активной игре ${existingGameIdForUser} (режим: ${existingGame.mode}, владелец: ${existingGame.ownerIdentifier}). Невозможно создать новую.`);
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||||
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
// Если это его ожидающая PvP, то _cleanupPreviousPendingGameForUser ниже ее удалит.
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру.
|
||||
// Это важно сделать ДО создания новой игры, чтобы освободить userIdentifierToGameId.
|
||||
const cleanedUp = this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
|
||||
if (cleanedUp) {
|
||||
console.log(`[GameManager.createGame] Successfully cleaned up previous pending PvP game for ${identifier}.`);
|
||||
} else {
|
||||
console.log(`[GameManager.createGame] No previous pending PvP game found or needed cleanup for ${identifier}.`);
|
||||
}
|
||||
console.log(`[GameManager.createGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
||||
this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
|
||||
console.log(`[GameManager.createGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||||
|
||||
|
||||
// 3. Окончательная проверка: если ПОСЛЕ очистки пользователь все еще привязан к какой-то активной игре
|
||||
// (Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он был в другой игре, что было бы ошибкой логики выше)
|
||||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
||||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
||||
console.error(`[GameManager.createGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying creation.`);
|
||||
console.error(`[GameManager.createGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Создание отклонено.`);
|
||||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию.' });
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
const gameId = uuidv4();
|
||||
console.log(`[GameManager.createGame] New GameID: ${gameId}`);
|
||||
console.log(`[GameManager.createGame] Новый GameID: ${gameId}`);
|
||||
const game = new GameInstance(gameId, this.io, mode, this);
|
||||
this.games[gameId] = game;
|
||||
|
||||
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
||||
|
||||
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameId; // Связываем пользователя с НОВОЙ игрой
|
||||
this.userIdentifierToGameId[identifier] = gameId;
|
||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
const assignedPlayerId = playerInfo?.id;
|
||||
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
||||
|
||||
if (!assignedPlayerId || !actualCharacterKey) {
|
||||
console.error(`[GameManager.createGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`);
|
||||
console.error(`[GameManager.createGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier} в игре ${gameId}. Очистка.`);
|
||||
this._cleanupGame(gameId, 'player_info_missing_after_add_on_create');
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||||
console.log(`[GameManager.createGame] Игрок ${identifier} добавлен в игру ${gameId} как ${assignedPlayerId}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||||
socket.emit('gameCreated', {
|
||||
gameId: gameId,
|
||||
mode: mode,
|
||||
@ -116,33 +98,32 @@ class GameManager {
|
||||
|
||||
if (mode === 'ai') {
|
||||
if (game.initializeGame()) {
|
||||
console.log(`[GameManager.createGame] AI game ${gameId} initialized by GameManager, starting...`);
|
||||
console.log(`[GameManager.createGame] AI игра ${gameId} инициализирована GameManager, запуск...`);
|
||||
game.startGame();
|
||||
} else {
|
||||
console.error(`[GameManager.createGame] AI game ${gameId} init failed in GameManager. Cleaning up.`);
|
||||
console.error(`[GameManager.createGame] Инициализация AI игры ${gameId} не удалась в GameManager. Очистка.`);
|
||||
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
||||
}
|
||||
} else if (mode === 'pvp') {
|
||||
if (game.initializeGame()) { // Для PvP инициализируем даже с одним игроком
|
||||
if (game.initializeGame()) {
|
||||
if (!this.pendingPvPGames.includes(gameId)) {
|
||||
this.pendingPvPGames.push(gameId);
|
||||
}
|
||||
socket.emit('waitingForOpponent');
|
||||
this.broadcastAvailablePvPGames();
|
||||
} else {
|
||||
console.error(`[GameManager.createGame] PvP game ${gameId} (single player) init failed. Cleaning up.`);
|
||||
console.error(`[GameManager.createGame] Инициализация PvP игры ${gameId} (один игрок) не удалась. Очистка.`);
|
||||
this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`);
|
||||
console.error(`[GameManager.createGame] game.addPlayer не удалось для ${identifier} в ${gameId}. Очистка.`);
|
||||
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create');
|
||||
// game.addPlayer должен был сам отправить ошибку клиенту
|
||||
}
|
||||
}
|
||||
|
||||
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
|
||||
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
|
||||
console.log(`[GameManager.joinGame] Пользователь: ${identifier} (Socket: ${socket.id}) пытается присоединиться к ${gameIdToJoin} с персонажем ${chosenCharacterKey || 'По умолчанию'}`);
|
||||
const gameToJoin = this.games[gameIdToJoin];
|
||||
|
||||
if (!gameToJoin) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||||
@ -153,22 +134,20 @@ class GameManager {
|
||||
if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
||||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||||
}
|
||||
// Запрещаем владельцу "присоединяться" к своей ожидающей игре как новый игрок, если он не был временно отключен.
|
||||
// Если он хочет вернуться, он должен использовать requestGameState.
|
||||
if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
||||
console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} where they are owner and not disconnected. Treating as reconnect request.`);
|
||||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} пытается присоединиться к своей игре ${gameIdToJoin}, где он владелец и не отключен. Обработка как запрос на переподключение.`);
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Очистка завершенной игры пользователя, если такая есть
|
||||
const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier];
|
||||
if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) {
|
||||
console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameIdUserIsIn} while trying to join ${gameIdToJoin}. Cleaning old one.`);
|
||||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} был в завершенной игре ${currentActiveGameIdUserIsIn} при попытке присоединиться к ${gameIdToJoin}. Очистка старой.`);
|
||||
this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`);
|
||||
}
|
||||
|
||||
// 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться),
|
||||
// и это НЕ его собственная ожидающая PvP игра, то отказать.
|
||||
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить.
|
||||
const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||||
if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) {
|
||||
const usersCurrentGame = this.games[stillExistingGameIdForUser];
|
||||
@ -178,30 +157,29 @@ class GameManager {
|
||||
this.pendingPvPGames.includes(stillExistingGameIdForUser);
|
||||
|
||||
if (isHisOwnPendingPvp) {
|
||||
console.log(`[GameManager.joinGame] User ${identifier} is owner of pending game ${stillExistingGameIdForUser}, but wants to join ${gameIdToJoin}. Cleaning up old game.`);
|
||||
console.log(`[GameManager.joinGame] Пользователь ${identifier} является владельцем ожидающей игры ${stillExistingGameIdForUser}, но хочет присоединиться к ${gameIdToJoin}. Очистка старой игры.`);
|
||||
this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
|
||||
} else {
|
||||
// Пользователь в другой активной игре (не своей ожидающей)
|
||||
console.warn(`[GameManager.joinGame] User ${identifier} is in another active game ${stillExistingGameIdForUser}. Cannot join ${gameIdToJoin}.`);
|
||||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} находится в другой активной игре ${stillExistingGameIdForUser}. Невозможно присоединиться к ${gameIdToJoin}.`);
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
|
||||
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`[GameManager.joinGame] After potential cleanup before join, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
||||
console.log(`[GameManager.joinGame] После возможной очистки перед присоединением, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||||
|
||||
const charKeyForJoin = chosenCharacterKey || 'elena';
|
||||
if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameIdToJoin; // Связываем пользователя с игрой, к которой он присоединился
|
||||
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||||
const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
||||
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
|
||||
console.error(`[GameManager.joinGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier}, присоединяющегося к ${gameIdToJoin}.`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
||||
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
|
||||
return;
|
||||
}
|
||||
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||||
console.log(`[GameManager.joinGame] Игрок ${identifier} добавлен/переподключен к ${gameIdToJoin} как ${joinedPlayerInfo.id}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||||
socket.emit('gameCreated', {
|
||||
gameId: gameIdToJoin,
|
||||
mode: gameToJoin.mode,
|
||||
@ -210,7 +188,8 @@ class GameManager {
|
||||
});
|
||||
|
||||
if (gameToJoin.playerCount === 2) {
|
||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
|
||||
console.log(`[GameManager.joinGame] Игра ${gameIdToJoin} теперь заполнена. Инициализация и запуск.`);
|
||||
// Важно! Инициализация может обновить ключи персонажей, если они были одинаковыми.
|
||||
if (gameToJoin.initializeGame()) {
|
||||
gameToJoin.startGame();
|
||||
} else {
|
||||
@ -221,60 +200,57 @@ class GameManager {
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else {
|
||||
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`);
|
||||
// GameInstance должен был отправить причину
|
||||
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer вернул false для пользователя ${identifier} в игре ${gameIdToJoin}.`);
|
||||
}
|
||||
}
|
||||
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||||
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||||
console.log(`[GameManager.findRandomPvPGame] Пользователь: ${identifier} (Socket: ${socket.id}), Персонаж для создания: ${chosenCharacterKeyForCreation}`);
|
||||
|
||||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
||||
const existingGame = this.games[existingGameIdForUser];
|
||||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
||||
console.warn(`[GameManager.findRandomPvPGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up.`);
|
||||
console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка.`);
|
||||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`);
|
||||
} else {
|
||||
console.warn(`[GameManager.findRandomPvPGame] User ${identifier} is already in an active/pending game ${existingGameIdForUser}. Cannot find random.`);
|
||||
console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} уже в активной/ожидающей игре ${existingGameIdForUser}. Невозможно найти случайную.`);
|
||||
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
|
||||
this.handleRequestGameState(socket, identifier); return;
|
||||
}
|
||||
}
|
||||
|
||||
// Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую.
|
||||
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
|
||||
console.log(`[GameManager.findRandomPvPGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
||||
console.log(`[GameManager.findRandomPvPGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||||
|
||||
// Если после очистки пользователь все еще привязан к какой-то *другой* активной игре
|
||||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
||||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
||||
console.error(`[GameManager.findRandomPvPGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying find random.`);
|
||||
console.error(`[GameManager.findRandomPvPGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Поиск случайной игры отклонен.`);
|
||||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию для поиска.' });
|
||||
this.handleRequestGameState(socket, identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
let gameIdToJoin = null;
|
||||
for (const id of [...this.pendingPvPGames]) { // Итерируем копию
|
||||
for (const id of [...this.pendingPvPGames]) {
|
||||
const pendingGame = this.games[id];
|
||||
if (pendingGame && pendingGame.mode === 'pvp' &&
|
||||
pendingGame.playerCount === 1 &&
|
||||
pendingGame.ownerIdentifier !== identifier &&
|
||||
(!pendingGame.gameState || !pendingGame.gameState.isGameOver)) {
|
||||
gameIdToJoin = id; break;
|
||||
} else if (!pendingGame || (pendingGame.gameState && pendingGame.gameState.isGameOver)) {
|
||||
console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`);
|
||||
} else if (!pendingGame || (pendingGame?.gameState && pendingGame.gameState.isGameOver)) {
|
||||
console.warn(`[GameManager.findRandomPvPGame] Найдена устаревшая/завершенная ожидающая игра ${id}. Очистка.`);
|
||||
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
|
||||
}
|
||||
}
|
||||
|
||||
if (gameIdToJoin) {
|
||||
console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`);
|
||||
console.log(`[GameManager.findRandomPvPGame] Найдена ожидающая игра ${gameIdToJoin} для ${identifier}. Присоединение...`);
|
||||
const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)];
|
||||
this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey);
|
||||
} else {
|
||||
console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`);
|
||||
console.log(`[GameManager.findRandomPvPGame] Подходящая ожидающая игра не найдена. Создание новой PvP игры для ${identifier}.`);
|
||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||||
}
|
||||
}
|
||||
@ -286,17 +262,17 @@ class GameManager {
|
||||
if (game.gameState?.isGameOver) {
|
||||
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
|
||||
if (playerSocket) {
|
||||
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`);
|
||||
console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, но игра завершена. Запрос состояния.`);
|
||||
this.handleRequestGameState(playerSocket, identifier);
|
||||
} else {
|
||||
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, game over, but no socket found for user.`);
|
||||
console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, игра завершена, но сокет для пользователя не найден.`);
|
||||
this._cleanupGame(gameId, `action_on_over_no_socket_gm_${identifier}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
game.processPlayerAction(identifier, actionData);
|
||||
} else {
|
||||
console.warn(`[GameManager.handlePlayerAction] No game found for user ${identifier} (mapped to game ${gameId}). Clearing map entry.`);
|
||||
console.warn(`[GameManager.handlePlayerAction] Игра для пользователя ${identifier} не найдена (сопоставлена с игрой ${gameId}). Очистка записи в карте.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при совершении действия.' });
|
||||
@ -305,53 +281,52 @@ class GameManager {
|
||||
|
||||
handlePlayerSurrender(identifier) {
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`);
|
||||
console.log(`[GameManager.handlePlayerSurrender] Пользователь: ${identifier} сдался. GameID из карты: ${gameId}`);
|
||||
const game = this.games[gameId];
|
||||
if (game) {
|
||||
if (game.gameState?.isGameOver) {
|
||||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
|
||||
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это.
|
||||
console.warn(`[GameManager.handlePlayerSurrender] Пользователь ${identifier} в игре ${gameId} сдается, но игра УЖЕ ЗАВЕРШЕНА.`);
|
||||
return;
|
||||
}
|
||||
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
|
||||
else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); }
|
||||
else { console.error(`[GameManager.handlePlayerSurrender] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); }
|
||||
} else {
|
||||
console.warn(`[GameManager.handlePlayerSurrender] No game found for user ${identifier}. Clearing map entry.`);
|
||||
console.warn(`[GameManager.handlePlayerSurrender] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`);
|
||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
|
||||
handleLeaveAiGame(identifier) {
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager.handleLeaveAiGame] User: ${identifier} leaving AI game. GameID from map: ${gameId}`);
|
||||
console.log(`[GameManager.handleLeaveAiGame] Пользователь: ${identifier} покидает AI игру. GameID из карты: ${gameId}`);
|
||||
const game = this.games[gameId];
|
||||
if (game) {
|
||||
if (game.gameState?.isGameOver) {
|
||||
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} game ${gameId} leaving, but game ALREADY OVER.`);
|
||||
console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} в игре ${gameId} выходит, но игра УЖЕ ЗАВЕРШЕНА.`);
|
||||
return;
|
||||
}
|
||||
if (game.mode === 'ai') {
|
||||
if (typeof game.playerExplicitlyLeftAiGame === 'function') {
|
||||
game.playerExplicitlyLeftAiGame(identifier);
|
||||
} else {
|
||||
console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`);
|
||||
console.error(`[GameManager.handleLeaveAiGame] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerExplicitlyLeftAiGame! Прямая очистка.`);
|
||||
this._cleanupGame(gameId, "leave_ai_missing_method_gm");
|
||||
}
|
||||
} else {
|
||||
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
|
||||
socket.emit('gameError', { message: 'Вы не в AI игре.' }); // Сообщить клиенту об ошибке
|
||||
console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} отправил leaveAiGame, но игра ${gameId} не в режиме AI (${game.mode}).`);
|
||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||
if(clientSocket) clientSocket.emit('gameError', { message: 'Вы не в AI игре.' });
|
||||
}
|
||||
} else {
|
||||
console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`);
|
||||
console.warn(`[GameManager.handleLeaveAiGame] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`);
|
||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||
// Сообщить клиенту, что игра не найдена
|
||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||
if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' });
|
||||
}
|
||||
}
|
||||
|
||||
_findClientSocketByIdentifier(identifier) {
|
||||
for (const s of this.io.sockets.sockets.values()) { // Использование .values() для итератора
|
||||
for (const s of this.io.sockets.sockets.values()) {
|
||||
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
||||
}
|
||||
return null;
|
||||
@ -359,103 +334,99 @@ class GameManager {
|
||||
|
||||
handleDisconnect(socketId, identifier) {
|
||||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
|
||||
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, Пользователь: ${identifier}, GameID из карты: ${gameIdFromMap}`);
|
||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||
|
||||
if (game) {
|
||||
if (game.gameState?.isGameOver) {
|
||||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER. Game will be cleaned up by its own logic or next relevant action.`);
|
||||
console.log(`[GameManager.handleDisconnect] Игра ${gameIdFromMap} для пользователя ${identifier} (сокет ${socketId}) УЖЕ ЗАВЕРШЕНА. Игра будет очищена своей собственной логикой или следующим релевантным действием.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
|
||||
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
||||
if (playerInfoInGame) { // Игрок существует в этой игре
|
||||
console.log(`[GameManager.handleDisconnect] Отключающийся сокет ${socketId} для пользователя ${identifier} (Роль: ${playerInfoInGame.id}) в игре ${gameIdFromMap}. Уведомление GameInstance.`);
|
||||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||||
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey);
|
||||
// Передаем фактический socketId, который отключился. PCH определит, устарел ли он.
|
||||
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey, socketId);
|
||||
} else {
|
||||
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} missing handlePlayerPotentiallyLeft!`);
|
||||
console.error(`[GameManager.handleDisconnect] КРИТИЧЕСКИ: GameInstance ${gameIdFromMap} отсутствует handlePlayerPotentiallyLeft!`);
|
||||
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
|
||||
}
|
||||
} else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) {
|
||||
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}. No action taken by GM.`);
|
||||
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
|
||||
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while ALREADY temp disconnected. Reconnect timer in GameInstance handles final cleanup.`);
|
||||
} else if (!playerInfoInGame) {
|
||||
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. This might indicate a stale userIdentifierToGameId entry. Clearing map for this user.`);
|
||||
} else {
|
||||
console.warn(`[GameManager.handleDisconnect] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но не найден в game.players. Это может указывать на устаревшую запись userIdentifierToGameId. Очистка карты для этого пользователя.`);
|
||||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.userIdentifierToGameId[identifier]) {
|
||||
console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
|
||||
console.warn(`[GameManager.handleDisconnect] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cleanupGame(gameId, reason = 'unknown') {
|
||||
console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`);
|
||||
console.log(`[GameManager._cleanupGame] Попытка очистки для GameID: ${gameId}, Причина: ${reason}`);
|
||||
const game = this.games[gameId];
|
||||
|
||||
if (!game) {
|
||||
console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`);
|
||||
console.warn(`[GameManager._cleanupGame] Экземпляр игры для ${gameId} не найден в this.games. Очистка связанных записей.`);
|
||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||
if (pendingIdx > -1) {
|
||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
|
||||
console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`);
|
||||
}
|
||||
// Важно: итерируем по ключам, так как удаление может изменить объект
|
||||
Object.keys(this.userIdentifierToGameId).forEach(idKey => {
|
||||
if (this.userIdentifierToGameId[idKey] === gameId) {
|
||||
delete this.userIdentifierToGameId[idKey];
|
||||
console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`);
|
||||
console.log(`[GameManager._cleanupGame] Удалено сопоставление для пользователя ${idKey} с игрой ${gameId}.`);
|
||||
}
|
||||
});
|
||||
this.broadcastAvailablePvPGames();
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`);
|
||||
console.log(`[GameManager._cleanupGame] Очистка игры ${game.id}. Владелец: ${game.ownerIdentifier}. Причина: ${reason}. Игроков в игре: ${game.playerCount}`);
|
||||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||||
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
|
||||
|
||||
if (game.gameState && !game.gameState.isGameOver) {
|
||||
console.log(`[GameManager._cleanupGame] Marking game ${game.id} as game over because it's being cleaned up while active.`);
|
||||
console.log(`[GameManager._cleanupGame] Пометка игры ${game.id} как завершенной, так как она очищается во время активности.`);
|
||||
game.gameState.isGameOver = true;
|
||||
// Можно рассмотреть отправку gameOver, если игра прерывается извне
|
||||
// game.io.to(game.id).emit('gameOver', { reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() });
|
||||
// game.io.to(game.id).emit('gameOver', { winnerId: null, reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() });
|
||||
}
|
||||
|
||||
Object.values(game.players).forEach(pInfo => {
|
||||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||||
delete this.userIdentifierToGameId[pInfo.identifier];
|
||||
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`);
|
||||
console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для игрока ${pInfo.identifier}.`);
|
||||
}
|
||||
});
|
||||
// Дополнительная проверка для владельца, если он не был в списке игроков (маловероятно, но для полноты)
|
||||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
|
||||
if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) {
|
||||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||||
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for owner ${game.ownerIdentifier} (was not in players list).`);
|
||||
console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для владельца ${game.ownerIdentifier} (не был в списке игроков).`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||
if (pendingIdx > -1) {
|
||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
|
||||
console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`);
|
||||
}
|
||||
|
||||
delete this.games[gameId];
|
||||
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||||
console.log(`[GameManager._cleanupGame] Экземпляр игры ${gameId} удален. Осталось игр: ${Object.keys(this.games).length}. Ожидающих: ${this.pendingPvPGames.length}. Размер карты пользователей: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||||
this.broadcastAvailablePvPGames();
|
||||
return true;
|
||||
}
|
||||
|
||||
getAvailablePvPGamesListForClient() {
|
||||
// Итерируем копию массива pendingPvPGames, так как _cleanupGame может его изменять
|
||||
return [...this.pendingPvPGames]
|
||||
.map(gameId => {
|
||||
const game = this.games[gameId];
|
||||
@ -463,22 +434,22 @@ class GameManager {
|
||||
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||
let p1Username = 'Игрок';
|
||||
let p1CharName = 'Неизвестный';
|
||||
const ownerId = game.ownerIdentifier; // Это должен быть identifier создателя
|
||||
const ownerId = game.ownerIdentifier;
|
||||
|
||||
if (p1Entry && p1Entry.socket?.userData) {
|
||||
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
||||
if (p1Entry) { // Используем данные из p1Entry, если он есть (более надежно)
|
||||
p1Username = p1Entry.socket?.userData?.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
||||
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
|
||||
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
|
||||
} else if (ownerId){
|
||||
} else if (ownerId){ // Резервный вариант, если p1Entry почему-то нет
|
||||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||||
const ownerCharKey = game.playerCharacterKey; // Это ключ персонажа для роли PLAYER_ID в этой игре
|
||||
const ownerCharKey = game.playerCharacterKey;
|
||||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||||
}
|
||||
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
||||
} else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) {
|
||||
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is in pendingPvPGames but is not a valid pending game (players: ${game.playerCount}, over: ${game.gameState?.isGameOver}). Removing.`);
|
||||
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Игра ${gameId} находится в pendingPvPGames, но не является допустимой ожидающей игрой (игроков: ${game.playerCount}, завершена: ${game.gameState?.isGameOver}). Удаление.`);
|
||||
this._cleanupGame(gameId, 'invalid_pending_game_in_list');
|
||||
}
|
||||
return null;
|
||||
@ -493,7 +464,7 @@ class GameManager {
|
||||
|
||||
handleRequestGameState(socket, identifier) {
|
||||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
|
||||
console.log(`[GameManager.handleRequestGameState] Пользователь: ${identifier} (Socket: ${socket.id}) запрашивает состояние. GameID из карты: ${gameIdFromMap}`);
|
||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||
|
||||
if (game) {
|
||||
@ -502,49 +473,84 @@ class GameManager {
|
||||
if (playerInfoInGame) {
|
||||
if (game.gameState?.isGameOver) {
|
||||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
||||
// _cleanupGame будет вызвана, когда игра фактически завершается.
|
||||
// Здесь не удаляем из userIdentifierToGameId, если игра еще есть в this.games.
|
||||
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это, если игра еще в this.games
|
||||
return;
|
||||
}
|
||||
if (typeof game.handlePlayerReconnected === 'function') {
|
||||
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
||||
if (!reconnected) {
|
||||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`);
|
||||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected для ${identifier} в ${game.id} вернул false.`);
|
||||
// GameInstance должен был отправить ошибку.
|
||||
}
|
||||
} else {
|
||||
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
|
||||
console.error(`[GameManager.handleRequestGameState] КРИТИЧЕСКИ: GameInstance ${game.id} отсутствует handlePlayerReconnected!`);
|
||||
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request');
|
||||
}
|
||||
} else {
|
||||
console.warn(`[GameManager.handleRequestGameState] User ${identifier} mapped to game ${gameIdFromMap}, but NOT FOUND in game.players. Cleaning map & sending gameNotFound.`);
|
||||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_but_mapped_on_request');
|
||||
// Игрок сопоставлен с игрой, но НЕ НАЙДЕН в game.players. Это может произойти, если PCH еще не добавил игрока (например, F5 на экране создания игры).
|
||||
// Попытаемся добавить игрока в игру, если это PvP и есть место, или если это его же игра в режиме AI.
|
||||
console.warn(`[GameManager.handleRequestGameState] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но НЕ НАЙДЕН в game.players. Попытка добавить/переподключить.`);
|
||||
if (game.mode === 'pvp') {
|
||||
// Пытаемся присоединить, предполагая, что он мог быть удален или это F5 перед полным присоединением
|
||||
const chosenCharKey = socket.handshake.query.chosenCharacterKey || 'elena'; // Получаем ключ из запроса или дефолтный
|
||||
if (game.addPlayer(socket, chosenCharKey, identifier)) {
|
||||
// Успешно добавили или переподключили через addPlayer -> handlePlayerReconnected
|
||||
const newPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
socket.emit('gameCreated', { // Отправляем событие, как при обычном присоединении
|
||||
gameId: game.id,
|
||||
mode: game.mode,
|
||||
yourPlayerId: newPlayerInfo.id,
|
||||
chosenCharacterKey: newPlayerInfo.chosenCharacterKey
|
||||
});
|
||||
if (game.playerCount === 2) { // Если игра стала полной
|
||||
if(game.initializeGame()) game.startGame(); else this._cleanupGame(game.id, 'init_fail_pvp_readd_gm');
|
||||
const idx = this.pendingPvPGames.indexOf(game.id);
|
||||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else {
|
||||
// Не удалось добавить/переподключить через addPlayer
|
||||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_readd_failed_in_gi_on_request');
|
||||
}
|
||||
|
||||
} else if (game.mode === 'ai' && game.ownerIdentifier === identifier) {
|
||||
// Для AI игры, если это владелец, пытаемся через handlePlayerReconnected
|
||||
if (typeof game.handlePlayerReconnected === 'function') {
|
||||
// Предполагаем, что роль PLAYER_ID, так как это AI игра и он владелец
|
||||
const reconnected = game.handlePlayerReconnected(GAME_CONFIG.PLAYER_ID, socket);
|
||||
if (!reconnected) {
|
||||
this._handleGameRecoveryError(socket, game.id, identifier, 'ai_owner_reconnect_failed_on_request');
|
||||
}
|
||||
} else {
|
||||
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_ai_owner_on_request');
|
||||
}
|
||||
} else {
|
||||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_unhandled_case_on_request');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
||||
if (this.userIdentifierToGameId[identifier]) {
|
||||
console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
|
||||
console.warn(`[GameManager.handleRequestGameState] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`);
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||||
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
||||
console.error(`[GameManager._handleGameRecoveryError] Ошибка восстановления игры (ID: ${gameId || 'N/A'}) для пользователя ${identifier}. Причина: ${reasonCode}.`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' });
|
||||
|
||||
if (gameId && this.games[gameId]) {
|
||||
this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`);
|
||||
} else if (this.userIdentifierToGameId[identifier]) {
|
||||
const problematicGameIdForUser = this.userIdentifierToGameId[identifier];
|
||||
// Если игра была удалена, но пользователь к ней привязан, просто чистим карту
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager._handleGameRecoveryError] Cleaned stale userIdentifierToGameId[${identifier}] pointing to ${problematicGameIdForUser}.`);
|
||||
console.log(`[GameManager._handleGameRecoveryError] Очищено устаревшее userIdentifierToGameId[${identifier}], указывающее на ${problematicGameIdForUser}.`);
|
||||
}
|
||||
// Убедимся, что после всех очисток пользователь точно не привязан
|
||||
if (this.userIdentifierToGameId[identifier]) {
|
||||
if (this.userIdentifierToGameId[identifier]) { // Финальная проверка
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
console.warn(`[GameManager._handleGameRecoveryError] Force cleaned userIdentifierToGameId[${identifier}] as a final measure.`);
|
||||
console.warn(`[GameManager._handleGameRecoveryError] Принудительно очищено userIdentifierToGameId[${identifier}] в качестве финальной меры.`);
|
||||
}
|
||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ class GameInstance {
|
||||
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
||||
() => this.handleTurnTimeout(),
|
||||
(remainingTime, isPlayerTurnForTimer, isPaused) => {
|
||||
// Логируем отправку обновления таймера
|
||||
// console.log(`[GI TURN_TIMER_CB ${this.id}] Sending update. Remaining: ${remainingTime}, isPlayerT: ${isPlayerTurnForTimer}, isPaused (raw): ${isPaused}, effectivelyPaused: ${this.isGameEffectivelyPaused()}`);
|
||||
this.io.to(this.id).emit('turnTimerUpdate', {
|
||||
remainingTime,
|
||||
isPlayerTurn: isPlayerTurnForTimer,
|
||||
@ -38,28 +40,23 @@ class GameInstance {
|
||||
);
|
||||
|
||||
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
||||
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: GameManager reference invalid.`);
|
||||
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: Ссылка на GameManager недействительна.`);
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}. PlayerConnectionHandler also initialized.`);
|
||||
console.log(`[GameInstance ${this.id}] Создан. Режим: ${mode}. PlayerConnectionHandler также инициализирован.`);
|
||||
}
|
||||
|
||||
// --- Геттеры для GameManager и внутреннего использования ---
|
||||
get playerCount() {
|
||||
return this.playerConnectionHandler.playerCount;
|
||||
}
|
||||
|
||||
// Этот геттер может быть полезен, если GameManager или другая часть GameInstance
|
||||
// захочет получить доступ ко всем данным игроков, не зная о PCH.
|
||||
get players() {
|
||||
return this.playerConnectionHandler.getAllPlayersInfo();
|
||||
}
|
||||
|
||||
// --- Сеттеры для PCH ---
|
||||
setPlayerCharacterKey(key) { this.playerCharacterKey = key; }
|
||||
setOpponentCharacterKey(key) { this.opponentCharacterKey = key; }
|
||||
setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; }
|
||||
|
||||
// --- Методы, делегирующие PCH ---
|
||||
addPlayer(socket, chosenCharacterKey, identifier) {
|
||||
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
|
||||
}
|
||||
@ -68,11 +65,12 @@ class GameInstance {
|
||||
this.playerConnectionHandler.removePlayer(socketId, reason);
|
||||
}
|
||||
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||
this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey);
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
|
||||
this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId);
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||
console.log(`[GameInstance ${this.id}] Делегирование handlePlayerReconnected в PCH для роли ${playerIdRole}, сокет ${newSocket.id}`);
|
||||
return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket);
|
||||
}
|
||||
|
||||
@ -85,19 +83,17 @@ class GameInstance {
|
||||
}
|
||||
|
||||
handlePlayerPermanentlyLeft(playerRole, characterKey, reason) {
|
||||
console.log(`[GameInstance ${this.id}] Player permanently left. Role: ${playerRole}, Reason: ${reason}`);
|
||||
console.log(`[GameInstance ${this.id}] Игрок окончательно покинул игру. Роль: ${playerRole}, Персонаж: ${characterKey}, Причина: ${reason}`);
|
||||
if (this.gameState && !this.gameState.isGameOver) {
|
||||
// Используем геттер playerCount
|
||||
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
|
||||
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
|
||||
} else if (this.mode === 'pvp') {
|
||||
if (this.playerCount < 2) {
|
||||
// Используем геттер players для поиска оставшегося
|
||||
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
|
||||
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
|
||||
}
|
||||
}
|
||||
} else if (!this.gameState && Object.keys(this.players).length === 0) { // Используем геттер players
|
||||
} else if (!this.gameState && Object.keys(this.players).length === 0) {
|
||||
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch");
|
||||
}
|
||||
}
|
||||
@ -105,7 +101,7 @@ class GameInstance {
|
||||
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
|
||||
if (!characterState || !characterState.characterKey) return;
|
||||
if (!opponentCharacterKey) return;
|
||||
if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`); return; }
|
||||
if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt недоступен!`); return; }
|
||||
if (!this.gameState) return;
|
||||
|
||||
let context = {};
|
||||
@ -146,24 +142,27 @@ class GameInstance {
|
||||
}
|
||||
|
||||
initializeGame() {
|
||||
// Используем геттеры
|
||||
console.log(`[GameInstance ${this.id}] Initializing game state. Mode: ${this.mode}. Active players: ${this.playerCount}. Total entries: ${Object.keys(this.players).length}`);
|
||||
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков (PCH): ${this.playerCount}. Всего записей в PCH.players: ${Object.keys(this.players).length}. PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`);
|
||||
|
||||
const p1ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||
const p2ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||
|
||||
// Устанавливаем ключи персонажей, если они еще не установлены, на основе активных игроков в PCH
|
||||
// Это важно, если initializeGame вызывается до того, как PCH успел обновить ключи в GI через сеттеры
|
||||
if (p1ActiveEntry && !this.playerCharacterKey) this.playerCharacterKey = p1ActiveEntry.chosenCharacterKey;
|
||||
if (p2ActiveEntry && !this.opponentCharacterKey && this.mode === 'pvp') this.opponentCharacterKey = p2ActiveEntry.chosenCharacterKey;
|
||||
|
||||
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_gi_v4', 'AI game init: Human player not found or not active.'); return false; }
|
||||
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'AI game init: Player character key not set.'); return false;}
|
||||
if (!p1ActiveEntry) { this._handleCriticalError('init_ai_no_active_player_gi', 'Инициализация AI игры: Игрок-человек не найден или не активен.'); return false; }
|
||||
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'Инициализация AI игры: Ключ персонажа игрока не установлен.'); return false;}
|
||||
this.opponentCharacterKey = 'balard';
|
||||
} else {
|
||||
// Используем геттер playerCount
|
||||
if (this.playerCount === 1 && p1Entry && !this.playerCharacterKey) {
|
||||
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP init (1 player): Player char key missing.'); return false;
|
||||
} else { // pvp
|
||||
if (this.playerCount === 1 && p1ActiveEntry && !this.playerCharacterKey) {
|
||||
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP инициализация (1 игрок): Ключ персонажа игрока отсутствует.'); return false;
|
||||
}
|
||||
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
|
||||
console.error(`[GameInstance ${this.id}] PvP init error: activePlayerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_gi_v4', `PvP init: activePlayerCount is 2, but a charKey is missing.`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_gi', `Инициализация PvP: playerCount=2, но ключ персонажа отсутствует. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -171,35 +170,44 @@ class GameInstance {
|
||||
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
|
||||
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
|
||||
|
||||
const isPlayerSlotFilledAndActive = !!(playerData && p1Entry);
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry));
|
||||
const isPlayerSlotFilledAndActive = !!(playerData && p1ActiveEntry);
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2ActiveEntry));
|
||||
|
||||
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
|
||||
this._handleCriticalError('init_ai_data_fail_gs_gi_v4', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
|
||||
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !opponentData) ) {
|
||||
this._handleCriticalError('init_ai_data_fail_gs_gi', 'Инициализация AI игры: Не удалось загрузить данные игрока или AI для gameState.'); return false;
|
||||
}
|
||||
|
||||
this.logBuffer = [];
|
||||
|
||||
// Имена берутся из playerData/opponentData, если они есть. PCH обновит их при реконнекте, если они изменились.
|
||||
const playerName = playerData?.baseStats?.name || (p1ActiveEntry?.name || 'Ожидание Игрока 1...');
|
||||
let opponentName;
|
||||
if (this.mode === 'ai') {
|
||||
opponentName = opponentData?.baseStats?.name || 'Противник AI';
|
||||
} else {
|
||||
opponentName = opponentData?.baseStats?.name || (p2ActiveEntry?.name || 'Ожидание Игрока 2...');
|
||||
}
|
||||
|
||||
|
||||
this.gameState = {
|
||||
player: isPlayerSlotFilledAndActive ?
|
||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities) :
|
||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание Игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
|
||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities, playerName) : // Передаем имя
|
||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: playerName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], playerName),
|
||||
opponent: isOpponentSlotFilledAndActive ?
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: (this.mode === 'pvp' ? 'Ожидание Игрока 2...' : 'Противник AI'), maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities, opponentName) : // Передаем имя
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: opponentName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], opponentName),
|
||||
isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true,
|
||||
isGameOver: false,
|
||||
turnNumber: 1,
|
||||
gameMode: this.mode
|
||||
};
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name}. Opponent: ${this.gameState.opponent.name}. Ready for start if both active: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
|
||||
return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) : isPlayerSlotFilledAndActive;
|
||||
console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Игрок: ${this.gameState.player.name} (${this.gameState.player.characterKey}). Оппонент: ${this.gameState.opponent.name} (${this.gameState.opponent.characterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}. Готово к старту: AI=${isPlayerSlotFilledAndActive && !!opponentData}, PvP1=${isPlayerSlotFilledAndActive}, PvP2=${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
|
||||
return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && !!opponentData) : isPlayerSlotFilledAndActive;
|
||||
}
|
||||
|
||||
_createFighterState(roleId, baseStats, abilities) {
|
||||
_createFighterState(roleId, baseStats, abilities, explicitName = null) {
|
||||
const fighterState = {
|
||||
id: roleId, characterKey: baseStats.characterKey, name: baseStats.name,
|
||||
id: roleId, characterKey: baseStats.characterKey, name: explicitName || baseStats.name, // Используем explicitName если передано
|
||||
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
|
||||
currentResource: baseStats.maxResource, maxResource: baseStats.maxResource,
|
||||
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
||||
@ -218,40 +226,46 @@ class GameInstance {
|
||||
}
|
||||
|
||||
startGame() {
|
||||
console.log(`[GameInstance ${this.id}] Попытка запуска игры. Paused: ${this.isGameEffectivelyPaused()}`);
|
||||
if (this.isGameEffectivelyPaused()) {
|
||||
console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`);
|
||||
console.log(`[GameInstance ${this.id}] Запуск игры отложен: игра на паузе.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
|
||||
console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init.`);
|
||||
console.warn(`[GameInstance ${this.id}] startGame: gameState или ключи персонажей не полностью инициализированы. Попытка повторной инициализации.`);
|
||||
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_gi_v5', 'Re-initialization before start failed or keys still missing in gameState.');
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_gi', 'Повторная инициализация перед стартом не удалась или ключи все еще отсутствуют в gameState.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Opponent in GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey})`);
|
||||
console.log(`[GameInstance ${this.id}] Запуск игры. Игрок в GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Оппонент в GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}`);
|
||||
|
||||
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
|
||||
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
||||
|
||||
if (!pData || !oData) {
|
||||
this._handleCriticalError('start_char_data_fail_sg_gi_v6', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
|
||||
this._handleCriticalError('start_char_data_fail_sg_gi', `Не удалось загрузить данные персонажей при старте игры. PData: ${!!pData}, OData: ${!!oData}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем имена в gameState на основе данных персонажей перед отправкой клиентам
|
||||
// Это гарантирует, что имена из dataUtils (самые "правильные") попадут в первое gameStarted
|
||||
if (this.gameState.player && pData?.baseStats?.name) this.gameState.player.name = pData.baseStats.name;
|
||||
if (this.gameState.opponent && oData?.baseStats?.name) this.gameState.opponent.name = oData.baseStats.name;
|
||||
|
||||
|
||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
|
||||
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
|
||||
} else {
|
||||
console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready.`);
|
||||
console.warn(`[GameInstance ${this.id}] Не удалось произнести стартовые насмешки во время startGame, gameState акторы/ключи не полностью готовы.`);
|
||||
}
|
||||
|
||||
const initialLog = this.consumeLogBuffer();
|
||||
|
||||
// Используем геттер this.players
|
||||
Object.values(this.players).forEach(playerInfo => {
|
||||
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
|
||||
const dataForThisClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
|
||||
@ -259,8 +273,12 @@ class GameInstance {
|
||||
{ playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities };
|
||||
|
||||
playerInfo.socket.emit('gameStarted', {
|
||||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||||
...dataForThisClient, log: [...initialLog], clientConfig: { ...GAME_CONFIG }
|
||||
gameId: this.id,
|
||||
yourPlayerId: playerInfo.id,
|
||||
initialGameState: this.gameState,
|
||||
...dataForThisClient,
|
||||
log: [...initialLog],
|
||||
clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -270,18 +288,23 @@ class GameInstance {
|
||||
this.broadcastLogUpdate();
|
||||
|
||||
const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||
console.log(`[GameInstance ${this.id}] Запуск таймера в startGame. isPlayerTurn: ${this.gameState.isPlayerTurn}, isFirstTurnAi: ${isFirstTurnAi}`);
|
||||
this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi);
|
||||
|
||||
if (isFirstTurnAi) {
|
||||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
setTimeout(() => {
|
||||
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) {
|
||||
this.processAiTurn();
|
||||
}
|
||||
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
}
|
||||
}
|
||||
|
||||
processPlayerAction(identifier, actionData) {
|
||||
// Используем геттер this.players
|
||||
console.log(`[GameInstance ${this.id}] processPlayerAction от ${identifier}. Действие: ${actionData.actionType}. Текущий GS.isPlayerTurn: ${this.gameState?.isPlayerTurn}. Paused: ${this.isGameEffectivelyPaused()}`);
|
||||
const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
if (!actingPlayerInfo || !actingPlayerInfo.socket) {
|
||||
console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return;
|
||||
console.error(`[GameInstance ${this.id}] Действие от неизвестного или безсокетного идентификатора ${identifier}.`); return;
|
||||
}
|
||||
|
||||
if (this.isGameEffectivelyPaused()) {
|
||||
@ -293,8 +316,14 @@ class GameInstance {
|
||||
const actingPlayerRole = actingPlayerInfo.id;
|
||||
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
|
||||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
|
||||
if (!isCorrectTurn) { actingPlayerInfo.socket.emit('gameError', { message: "Не ваш ход." }); return; }
|
||||
|
||||
if (!isCorrectTurn) {
|
||||
console.warn(`[GameInstance ${this.id}] Неверный ход! Игрок ${identifier} (роль ${actingPlayerRole}) пытался действовать. GS.isPlayerTurn: ${this.gameState.isPlayerTurn}`);
|
||||
actingPlayerInfo.socket.emit('gameError', { message: "Не ваш ход." });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Ход корректен. Очистка таймера.`);
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
|
||||
const attackerState = this.gameState[actingPlayerRole];
|
||||
@ -302,11 +331,11 @@ class GameInstance {
|
||||
const defenderState = this.gameState[defenderRole];
|
||||
|
||||
if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) {
|
||||
this._handleCriticalError('action_actor_state_invalid_gi_v4', `Attacker or Defender state/key invalid.`); return;
|
||||
this._handleCriticalError('action_actor_state_invalid_gi', `Состояние/ключ Атакующего или Защитника недействительны.`); return;
|
||||
}
|
||||
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
|
||||
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
|
||||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi_v4', 'Ошибка данных персонажа при действии.'); return; }
|
||||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi', 'Ошибка данных персонажа при действии.'); return; }
|
||||
|
||||
let actionIsValidAndPerformed = false;
|
||||
|
||||
@ -341,20 +370,22 @@ class GameInstance {
|
||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||
} else {
|
||||
const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||
console.log(`[GameInstance ${this.id}] Действие не выполнено, перезапуск таймера. isPlayerTurn: ${this.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`);
|
||||
this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer);
|
||||
}
|
||||
}
|
||||
|
||||
switchTurn() {
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Switch turn deferred: game paused.`); return; }
|
||||
console.log(`[GameInstance ${this.id}] Попытка смены хода. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}`);
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Смена хода отложена: игра на паузе.`); return; }
|
||||
if (!this.gameState || this.gameState.isGameOver) { return; }
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
|
||||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||||
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Ending turn actor state or key invalid.`); return; }
|
||||
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Состояние или ключ актора, завершающего ход, недействительны.`); return; }
|
||||
const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey);
|
||||
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Char data missing.`); return; }
|
||||
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Отсутствуют данные персонажа.`); return; }
|
||||
|
||||
gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils);
|
||||
gameLogic.updateBlockingStatus(endingTurnActorState);
|
||||
@ -369,32 +400,42 @@ class GameInstance {
|
||||
|
||||
const currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const currentTurnActorState = this.gameState[currentTurnActorRole];
|
||||
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Current turn actor state or name invalid.`); return; }
|
||||
|
||||
// Используем геттер this.players
|
||||
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
|
||||
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Состояние или имя текущего актора недействительны.`); return; }
|
||||
|
||||
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||
this.broadcastGameStateUpdate();
|
||||
|
||||
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
|
||||
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
|
||||
console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player ${currentTurnPlayerEntry.identifier} disconnected. Timer not started by switchTurn.`);
|
||||
console.log(`[GameInstance ${this.id}] Ход перешел к ${currentTurnActorRole}, но игрок ${currentTurnPlayerEntry.identifier} отключен. Таймер не запущен switchTurn.`);
|
||||
} else {
|
||||
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||
console.log(`[GameInstance ${this.id}] Запуск таймера в switchTurn. isPlayerTurn: ${this.gameState.isPlayerTurn}, isNextTurnAi: ${isNextTurnAi}`);
|
||||
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
|
||||
if (isNextTurnAi) setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
if (isNextTurnAi) {
|
||||
setTimeout(() => {
|
||||
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) {
|
||||
this.processAiTurn();
|
||||
}
|
||||
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processAiTurn() { // Остается без изменений, так как использует this.gameState
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] AI turn deferred: game paused.`); return; }
|
||||
processAiTurn() {
|
||||
console.log(`[GameInstance ${this.id}] processAiTurn. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}, IsPlayerTurn: ${this.gameState?.isPlayerTurn}`);
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Ход AI отложен: игра на паузе.`); return; }
|
||||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
|
||||
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) { console.error(`[GameInstance ${this.id}] AI is not Balard!`); this.switchTurn(); return; }
|
||||
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) {
|
||||
console.error(`[GameInstance ${this.id}] AI не Балард! Персонаж AI: ${this.gameState.opponent?.characterKey}. Принудительная смена хода.`);
|
||||
this.switchTurn();
|
||||
return;
|
||||
}
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
|
||||
const aiState = this.gameState.opponent;
|
||||
const playerState = this.gameState.player;
|
||||
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Player state invalid for AI turn.'); return; }
|
||||
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Состояние игрока недействительно для хода AI.'); return; }
|
||||
|
||||
const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this));
|
||||
let actionIsValidAndPerformedForAI = false;
|
||||
@ -411,17 +452,21 @@ class GameInstance {
|
||||
actionIsValidAndPerformedForAI = true;
|
||||
} else if (aiDecision.actionType === 'pass') {
|
||||
if (aiDecision.logMessage && this.addToLog) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
|
||||
else if (this.addToLog) this.addToLog(`${aiState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||
else if(this.addToLog) this.addToLog(`${aiState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||
actionIsValidAndPerformedForAI = true;
|
||||
}
|
||||
|
||||
if (this.checkGameOver()) return;
|
||||
this.broadcastLogUpdate();
|
||||
if (actionIsValidAndPerformedForAI) setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||
else { console.error(`[GameInstance ${this.id}] AI failed action. Forcing switch.`); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); }
|
||||
if (actionIsValidAndPerformedForAI) {
|
||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||
} else {
|
||||
console.error(`[GameInstance ${this.id}] AI не смог выполнить действие. Принудительная смена хода.`);
|
||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||
}
|
||||
}
|
||||
|
||||
checkGameOver() { // Остается без изменений, так как использует this.gameState
|
||||
checkGameOver() {
|
||||
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
|
||||
|
||||
if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||
@ -429,8 +474,12 @@ class GameInstance {
|
||||
const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey);
|
||||
if (pData && oData) {
|
||||
const nearDefeatThreshold = GAME_CONFIG.OPPONENT_NEAR_DEFEAT_THRESHOLD_PERCENT || 0.2;
|
||||
if (opponent.currentHp > 0 && (opponent.currentHp / oData.baseStats.maxHp) <= nearDefeatThreshold) this._sayTaunt(player, opponent.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
if (player.currentHp > 0 && (player.currentHp / pData.baseStats.maxHp) <= nearDefeatThreshold) this._sayTaunt(opponent, player.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
if (opponent.currentHp > 0 && (opponent.currentHp / oData.baseStats.maxHp) <= nearDefeatThreshold) {
|
||||
this._sayTaunt(player, opponent.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
if (player.currentHp > 0 && (player.currentHp / pData.baseStats.maxHp) <= nearDefeatThreshold) {
|
||||
this._sayTaunt(opponent, player.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,9 +495,8 @@ class GameInstance {
|
||||
if (winnerState?.characterKey && loserState?.characterKey) {
|
||||
this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
if (loserState?.characterKey) { /* ... сюжетные логи ... */ }
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Game over. Winner: ${gameOverResult.winnerRole || 'None'}. Reason: ${gameOverResult.reason}.`);
|
||||
console.log(`[GameInstance ${this.id}] Игра окончена. Победитель: ${gameOverResult.winnerRole || 'Нет'}. Причина: ${gameOverResult.reason}.`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: gameOverResult.winnerRole,
|
||||
reason: gameOverResult.reason,
|
||||
@ -472,7 +520,6 @@ class GameInstance {
|
||||
let winnerActuallyExists = false;
|
||||
|
||||
if (actualWinnerRole) {
|
||||
// Используем геттер this.players
|
||||
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
|
||||
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
@ -483,7 +530,6 @@ class GameInstance {
|
||||
|
||||
if (!winnerActuallyExists) {
|
||||
actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
|
||||
// Используем геттер this.players
|
||||
const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
|
||||
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
@ -493,11 +539,10 @@ class GameInstance {
|
||||
}
|
||||
|
||||
const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null;
|
||||
|
||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole);
|
||||
|
||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
console.log(`[GameInstance ${this.id}] Game ended by disconnect: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`);
|
||||
console.log(`[GameInstance ${this.id}] Игра завершена из-за отключения: ${reason}. Победитель: ${result.winnerRole || 'Нет'}.`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: result.winnerRole,
|
||||
reason: result.reason,
|
||||
@ -509,28 +554,28 @@ class GameInstance {
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${result.reason}`);
|
||||
} else if (this.gameState?.isGameOver) {
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`);
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: игра уже была завершена.`);
|
||||
this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup_gi`);
|
||||
} else {
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`);
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: нет gameState.`);
|
||||
this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`);
|
||||
}
|
||||
}
|
||||
|
||||
playerExplicitlyLeftAiGame(identifier) {
|
||||
if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) {
|
||||
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over.`);
|
||||
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame вызван, но не режим AI или игра завершена.`);
|
||||
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`);
|
||||
return;
|
||||
}
|
||||
// Используем геттер this.players
|
||||
|
||||
const playerEntry = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) {
|
||||
console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Identifier ${identifier} is not the human player or not found.`);
|
||||
console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Идентификатор ${identifier} не является игроком-человеком или не найден.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Player ${identifier} explicitly left AI game.`);
|
||||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} явно покинул AI игру.`);
|
||||
if (this.gameState) {
|
||||
this.gameState.isGameOver = true;
|
||||
this.addToLog(`Игрок покинул битву с ${this.gameState.opponent?.name || 'AI'}.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
@ -548,44 +593,42 @@ class GameInstance {
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: playerEntry.chosenCharacterKey
|
||||
});
|
||||
|
||||
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi');
|
||||
}
|
||||
|
||||
playerDidSurrender(surrenderingPlayerIdentifier) {
|
||||
console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`);
|
||||
console.log(`[GameInstance ${this.id}] playerDidSurrender вызван для идентификатора: ${surrenderingPlayerIdentifier}`);
|
||||
|
||||
if (!this.gameState || this.gameState.isGameOver) {
|
||||
if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished_gi`); }
|
||||
console.warn(`[GameInstance ${this.id}] Surrender attempt on inactive/finished game by ${surrenderingPlayerIdentifier}.`);
|
||||
console.warn(`[GameInstance ${this.id}] Попытка сдачи в неактивной/завершенной игре от ${surrenderingPlayerIdentifier}.`);
|
||||
return;
|
||||
}
|
||||
// Используем геттер this.players
|
||||
|
||||
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||||
if (!surrenderedPlayerEntry) {
|
||||
console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found.`);
|
||||
console.error(`[GameInstance ${this.id}] Сдающийся игрок ${surrenderingPlayerIdentifier} не найден.`);
|
||||
return;
|
||||
}
|
||||
const surrenderingPlayerRole = surrenderedPlayerEntry.id;
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) {
|
||||
console.log(`[GameInstance ${this.id}] Player ${surrenderingPlayerIdentifier} "surrendered" (left) AI game.`);
|
||||
console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} "сдался" (покинул) AI игру.`);
|
||||
this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier);
|
||||
} else {
|
||||
console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}).`);
|
||||
console.warn(`[GameInstance ${this.id}] Сдача в AI режиме от не-игрока (роль: ${surrenderingPlayerRole}). Игнорируется.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode !== 'pvp') {
|
||||
console.warn(`[GameInstance ${this.id}] Surrender called in non-PvP, non-AI mode: ${this.mode}. Ignoring.`);
|
||||
console.warn(`[GameInstance ${this.id}] Сдача вызвана в не-PvP, не-AI режиме: ${this.mode}. Игнорируется.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const surrenderedPlayerName = this.gameState[surrenderingPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey;
|
||||
const surrenderedPlayerCharKey = this.gameState[surrenderingPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey;
|
||||
|
||||
const winnerRole = surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
const winnerName = this.gameState[winnerRole]?.name || `Оппонент`;
|
||||
const winnerCharKey = this.gameState[winnerRole]?.characterKey;
|
||||
@ -595,7 +638,7 @@ class GameInstance {
|
||||
this.clearAllReconnectTimers();
|
||||
|
||||
this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
console.log(`[GameInstance ${this.id}] Player ${surrenderedPlayerName} (Role: ${surrenderingPlayerRole}) surrendered. Winner: ${winnerName} (Role: ${winnerRole}).`);
|
||||
console.log(`[GameInstance ${this.id}] Игрок ${surrenderedPlayerName} (Роль: ${surrenderingPlayerRole}) сдался. Победитель: ${winnerName} (Роль: ${winnerRole}).`);
|
||||
|
||||
if (winnerCharKey && surrenderedPlayerCharKey && this.gameState[winnerRole]) {
|
||||
this._sayTaunt(this.gameState[winnerRole], surrenderedPlayerCharKey, 'onBattleState', 'opponentNearDefeat');
|
||||
@ -611,7 +654,7 @@ class GameInstance {
|
||||
|
||||
handleTurnTimeout() {
|
||||
if (!this.gameState || this.gameState.isGameOver) return;
|
||||
console.log(`[GameInstance ${this.id}] Turn timeout occurred.`);
|
||||
console.log(`[GameInstance ${this.id}] Произошел таймаут хода.`);
|
||||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
|
||||
const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
@ -620,7 +663,6 @@ class GameInstance {
|
||||
if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
} else {
|
||||
// Используем геттер this.players
|
||||
const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected);
|
||||
winnerActuallyExists = !!winnerEntry;
|
||||
}
|
||||
@ -634,7 +676,7 @@ class GameInstance {
|
||||
if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) {
|
||||
this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Turn timed out for ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Winner: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`);
|
||||
console.log(`[GameInstance ${this.id}] Ход истек для ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Победитель: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: result.winnerRole,
|
||||
reason: result.reason,
|
||||
@ -646,9 +688,11 @@ class GameInstance {
|
||||
}
|
||||
|
||||
_handleCriticalError(reasonCode, logMessage) {
|
||||
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`);
|
||||
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`);
|
||||
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
||||
else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
||||
else if (!this.gameState) {
|
||||
this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
||||
}
|
||||
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
this.clearAllReconnectTimers();
|
||||
@ -667,6 +711,8 @@ class GameInstance {
|
||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||||
if (!message) return;
|
||||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||||
// Раскомментируйте для немедленной отправки логов, если нужно (но обычно лучше батчинг)
|
||||
// this.broadcastLogUpdate();
|
||||
}
|
||||
|
||||
consumeLogBuffer() {
|
||||
@ -676,8 +722,15 @@ class GameInstance {
|
||||
}
|
||||
|
||||
broadcastGameStateUpdate() {
|
||||
if (this.isGameEffectivelyPaused()) { return; }
|
||||
if (!this.gameState) return;
|
||||
if (this.isGameEffectivelyPaused()) {
|
||||
console.log(`[GameInstance ${this.id}] broadcastGameStateUpdate отложено: игра на паузе.`);
|
||||
return;
|
||||
}
|
||||
if (!this.gameState) {
|
||||
console.warn(`[GameInstance ${this.id}] broadcastGameStateUpdate: gameState отсутствует.`);
|
||||
return;
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Отправка gameStateUpdate. IsPlayerTurn: ${this.gameState.isPlayerTurn}`);
|
||||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
||||
}
|
||||
|
||||
@ -687,7 +740,7 @@ class GameInstance {
|
||||
if (systemLogs.length > 0) {
|
||||
this.io.to(this.id).emit('logUpdate', { log: systemLogs });
|
||||
}
|
||||
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); // Оставляем несистемные
|
||||
return;
|
||||
}
|
||||
if (this.logBuffer.length > 0) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// /server/game/instance/PlayerConnectionHandler.js
|
||||
const GAME_CONFIG = require('../../core/config');
|
||||
const dataUtils = require('../../data/dataUtils'); // Потребуется для получения данных персонажа при реконнекте
|
||||
const dataUtils = require('../../data/dataUtils');
|
||||
|
||||
class PlayerConnectionHandler {
|
||||
constructor(gameInstance) {
|
||||
@ -9,47 +9,51 @@ class PlayerConnectionHandler {
|
||||
this.gameId = gameInstance.id;
|
||||
this.mode = gameInstance.mode;
|
||||
|
||||
this.players = {}; // { socket.id: { id, socket, chosenCharacterKey, identifier, isTemporarilyDisconnected } }
|
||||
this.playerSockets = {}; // { playerIdRole: socket }
|
||||
this.players = {}; // { socket.id: { id, socket, chosenCharacterKey, identifier, isTemporarilyDisconnected, name (optional from gameState) } }
|
||||
this.playerSockets = {}; // { playerIdRole: socket } // Авторитетный сокет для роли
|
||||
this.playerCount = 0;
|
||||
|
||||
this.reconnectTimers = {}; // { playerIdRole: { timerId, updateIntervalId, startTimeMs, durationMs } }
|
||||
this.pausedTurnState = null; // { remainingTime, forPlayerRoleIsPlayer, isAiCurrentlyMoving }
|
||||
console.log(`[PCH for Game ${this.gameId}] Initialized.`);
|
||||
console.log(`[PCH for Game ${this.gameId}] Инициализирован.`);
|
||||
}
|
||||
|
||||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||||
console.log(`[PCH ${this.gameId}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
|
||||
console.log(`[PCH ${this.gameId}] Попытка addPlayer. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
|
||||
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (existingPlayerByIdentifier) {
|
||||
console.warn(`[PCH ${this.gameId}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`);
|
||||
console.warn(`[PCH ${this.gameId}] Идентификатор ${identifier} уже связан с ролью игрока ${existingPlayerByIdentifier.id} (сокет ${existingPlayerByIdentifier.socket?.id}). Обрабатывается как возможное переподключение.`);
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
console.warn(`[PCH ${this.gameId}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
|
||||
console.warn(`[PCH ${this.gameId}] Игрок ${identifier} пытается (пере)присоединиться к уже завершенной игре. Отправка gameError.`);
|
||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||
this.gameInstance.gameManager._cleanupGame(this.gameId, `rejoin_attempt_to_finished_game_pch_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||
}
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||||
return false;
|
||||
// Если игрок уже есть, и это не временное отключение, и сокет другой - это F5 или новая вкладка.
|
||||
// GameManager должен был направить на handleRequestGameState, который вызовет handlePlayerReconnected.
|
||||
// Прямой addPlayer в этом случае - редкий сценарий, но handlePlayerReconnected его обработает.
|
||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||
}
|
||||
|
||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
|
||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2 && this.mode === 'pvp') { // В AI режиме только 1 человек
|
||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||||
return false;
|
||||
}
|
||||
if (this.mode === 'ai' && this.playerCount >=1) {
|
||||
socket.emit('gameError', { message: 'К AI игре может присоединиться только один игрок.'});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let assignedPlayerId;
|
||||
let actualCharacterKey = chosenCharacterKey || 'elena';
|
||||
const charData = dataUtils.getCharacterData(actualCharacterKey);
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
return false;
|
||||
}
|
||||
// if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Эта проверка уже покрыта playerCount >= 1 выше
|
||||
// socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
// return false;
|
||||
// }
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
} else { // pvp
|
||||
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
@ -60,35 +64,39 @@ class PlayerConnectionHandler {
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
|
||||
if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
||||
else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
||||
// Добавьте другие пары, если нужно, или более общую логику выбора другого персонажа
|
||||
else actualCharacterKey = dataUtils.getAllCharacterKeys().find(k => k !== firstPlayerInfo.chosenCharacterKey) || 'elena';
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||
} else { // Оба слота заняты, но playerCount мог быть < 2 если кто-то в процессе дисконнекта
|
||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре (возможно, все заняты или в процессе переподключения).' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если для этой роли уже был игрок (например, старый сокет), удаляем его
|
||||
// Если для этой роли УЖЕ был игрок (например, старый сокет при F5 до того, как сработал disconnect),
|
||||
// то handlePlayerReconnected должен был бы это обработать. Этот блок здесь - подстраховка,
|
||||
// если addPlayer вызван напрямую в таком редком случае.
|
||||
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id);
|
||||
if (oldPlayerSocketIdForRole) {
|
||||
const oldPlayerInfo = this.players[oldPlayerSocketIdForRole];
|
||||
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); } catch(e){} } // Убедимся, что старый сокет покинул комнату
|
||||
console.warn(`[PCH ${this.gameId}] addPlayer: Найден старый сокет ${oldPlayerInfo.socket?.id} для роли ${assignedPlayerId}. Удаляем его запись.`);
|
||||
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); oldPlayerInfo.socket.disconnect(true); } catch(e){} }
|
||||
delete this.players[oldPlayerSocketIdForRole];
|
||||
}
|
||||
|
||||
|
||||
this.players[socket.id] = {
|
||||
id: assignedPlayerId,
|
||||
socket: socket,
|
||||
chosenCharacterKey: actualCharacterKey,
|
||||
identifier: identifier,
|
||||
isTemporarilyDisconnected: false
|
||||
isTemporarilyDisconnected: false,
|
||||
name: charData?.baseStats?.name || actualCharacterKey
|
||||
};
|
||||
this.playerSockets[assignedPlayerId] = socket;
|
||||
this.playerCount++;
|
||||
socket.join(this.gameId);
|
||||
console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`);
|
||||
|
||||
|
||||
// Сообщаем GameInstance об установленных ключах и владельце
|
||||
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey);
|
||||
else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.gameInstance.setOpponentCharacterKey(actualCharacterKey);
|
||||
|
||||
@ -96,8 +104,7 @@ class PlayerConnectionHandler {
|
||||
this.gameInstance.setOwnerIdentifier(identifier);
|
||||
}
|
||||
|
||||
const charData = dataUtils.getCharacterData(actualCharacterKey); // Используем dataUtils напрямую
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${this.gameInstance.ownerIdentifier}`);
|
||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Socket: ${socket.id}) добавлен как ${assignedPlayerId} с персонажем ${this.players[socket.id].name}. Активных игроков: ${this.playerCount}. Владелец: ${this.gameInstance.ownerIdentifier}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -106,61 +113,72 @@ class PlayerConnectionHandler {
|
||||
if (playerInfo) {
|
||||
const playerRole = playerInfo.id;
|
||||
const playerIdentifier = playerInfo.identifier;
|
||||
console.log(`[PCH ${this.gameId}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`);
|
||||
console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`);
|
||||
|
||||
if (playerInfo.socket) {
|
||||
try { playerInfo.socket.leave(this.gameId); } catch (e) { /* ignore */ }
|
||||
try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave: ${e.message}`); }
|
||||
}
|
||||
|
||||
if (!playerInfo.isTemporarilyDisconnected) { // Уменьшаем счетчик только если это был активный игрок, а не временное отключение
|
||||
if (!playerInfo.isTemporarilyDisconnected) {
|
||||
this.playerCount--;
|
||||
}
|
||||
|
||||
delete this.players[socketId];
|
||||
if (this.playerSockets[playerRole]?.id === socketId) { // Если это был текущий сокет для роли
|
||||
if (this.playerSockets[playerRole]?.id === socketId) {
|
||||
delete this.playerSockets[playerRole];
|
||||
}
|
||||
this.clearReconnectTimer(playerRole); // Очищаем таймер переподключения для этой роли
|
||||
this.clearReconnectTimer(playerRole);
|
||||
|
||||
console.log(`[PCH ${this.gameId}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`);
|
||||
|
||||
// Сигнализируем GameInstance, чтобы он решил, нужно ли завершать игру
|
||||
console.log(`[PCH ${this.gameId}] Игрок ${playerIdentifier} удален. Активных игроков сейчас: ${this.playerCount}.`);
|
||||
this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason);
|
||||
|
||||
} else {
|
||||
console.warn(`[PCH ${this.gameId}] removePlayer called for unknown socketId: ${socketId}`);
|
||||
console.warn(`[PCH ${this.gameId}] removePlayer вызван для неизвестного socketId: ${socketId}`);
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||
console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`);
|
||||
// Находим запись игрока по роли и идентификатору, так как сокет мог уже измениться или быть удален
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
|
||||
console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft для роли ${playerIdRole}, id ${identifier}, char ${characterKey}, disconnectedSocketId ${disconnectedSocketId}`);
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
if (!playerEntry || !playerEntry.socket) {
|
||||
console.warn(`[PCH ${this.gameId}] No player entry or socket found for ${identifier} (role ${playerIdRole}) during potential left.`);
|
||||
console.warn(`[PCH ${this.gameId}] Запись игрока или сокет не найдены для ${identifier} (роль ${playerIdRole}) во время потенциального выхода. disconnectedSocketId: ${disconnectedSocketId}`);
|
||||
// Если записи нет, возможно, игрок уже удален или это был очень старый сокет.
|
||||
// Проверим, есть ли запись по disconnectedSocketId, и если да, удалим ее.
|
||||
if (this.players[disconnectedSocketId]) {
|
||||
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId}, удаляем ее.`);
|
||||
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_entry');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerEntry.socket.id !== disconnectedSocketId) {
|
||||
console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игрок, вероятно, уже переподключился или сессия обновлена. Игнорируем дальнейшую логику "потенциального выхода" для этого устаревшего сокета.`);
|
||||
if (this.players[disconnectedSocketId]) {
|
||||
delete this.players[disconnectedSocketId]; // Удаляем только эту запись, не вызываем полный removePlayer
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
console.log(`[PCH ${this.gameId}] Game already over, not handling potential left for ${identifier}.`);
|
||||
console.log(`[PCH ${this.gameId}] Игра уже завершена, не обрабатываем потенциальный выход для ${identifier}.`);
|
||||
return;
|
||||
}
|
||||
if (playerEntry.isTemporarilyDisconnected) {
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} already marked as temp disconnected.`);
|
||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} уже помечен как временно отключенный.`);
|
||||
return;
|
||||
}
|
||||
|
||||
playerEntry.isTemporarilyDisconnected = true;
|
||||
this.playerCount--; // Уменьшаем счетчик активных игроков
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`);
|
||||
this.playerCount--;
|
||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`);
|
||||
|
||||
const disconnectedName = this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
|
||||
const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
|
||||
this.gameInstance.addToLog(`🔌 Игрок ${disconnectedName} отключился. Ожидание переподключения...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.gameInstance.broadcastLogUpdate();
|
||||
|
||||
// Уведомляем другого игрока, если он есть и подключен
|
||||
const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
const otherSocket = this.playerSockets[otherPlayerRole]; // Берем сокет из нашего this.playerSockets
|
||||
const otherSocket = this.playerSockets[otherPlayerRole];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole);
|
||||
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
@ -170,35 +188,37 @@ class PlayerConnectionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
// Приостанавливаем таймер хода, если он активен
|
||||
if (this.gameInstance.turnTimer.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isAiCurrentlyMakingMove) ) {
|
||||
if (this.gameInstance.turnTimer && (this.gameInstance.turnTimer.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isConfiguredForAiMove))) {
|
||||
this.pausedTurnState = this.gameInstance.turnTimer.pause();
|
||||
console.log(`[PCH ${this.gameId}] Turn timer paused due to disconnect. State:`, JSON.stringify(this.pausedTurnState));
|
||||
console.log(`[PCH ${this.gameId}] Таймер хода приостановлен из-за отключения. Состояние:`, JSON.stringify(this.pausedTurnState));
|
||||
} else {
|
||||
this.pausedTurnState = null; // Явно сбрасываем, если таймер не был активен
|
||||
this.pausedTurnState = null;
|
||||
}
|
||||
|
||||
this.clearReconnectTimer(playerIdRole); // Очищаем старый таймер, если был
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
|
||||
const reconnectStartTime = Date.now();
|
||||
|
||||
// Таймер для обновления UI клиента
|
||||
const updateInterval = setInterval(() => {
|
||||
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
|
||||
if (remaining <= 0) { // Если основной таймаут уже сработал или время вышло
|
||||
if (remaining <= 0 || !this.reconnectTimers[playerIdRole] || this.reconnectTimers[playerIdRole]?.timerId === null) { // Добавлена проверка на существование таймера
|
||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
if (this.reconnectTimers[playerIdRole]) this.reconnectTimers[playerIdRole].updateIntervalId = null; // Помечаем, что интервал очищен
|
||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
|
||||
return;
|
||||
}
|
||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: Math.ceil(remaining) });
|
||||
}, 1000);
|
||||
|
||||
// Основной таймер на окончательное удаление
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.clearReconnectTimer(playerIdRole); // Очищаем таймеры (включая updateInterval)
|
||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) { // Очищаем интервал, если он еще существует
|
||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
this.reconnectTimers[playerIdRole].updateIntervalId = null;
|
||||
}
|
||||
this.reconnectTimers[playerIdRole].timerId = null; // Помечаем, что основной таймаут сработал или очищен
|
||||
|
||||
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
|
||||
// Передаем socket.id из записи, а не старый socketId, который мог быть от предыдущего сокета
|
||||
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
|
||||
}
|
||||
}, reconnectDuration);
|
||||
@ -206,158 +226,247 @@ class PlayerConnectionHandler {
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||
const identifier = newSocket.userData?.userId; // Получаем идентификатор из нового сокета
|
||||
console.log(`[PCH ${this.gameId}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
|
||||
const identifier = newSocket.userData?.userId;
|
||||
console.log(`[PCH RECONNECT_ATTEMPT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${identifier}, NewSocket: ${newSocket.id}`);
|
||||
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
newSocket.emit('gameError', { message: 'Игра уже завершена.' });
|
||||
this.gameInstance.gameManager._cleanupGame(this.gameId, `reconnect_to_finished_game_pch_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Находим запись игрока по роли и идентификатору
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
let playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
console.log(`[PCH RECONNECT_ATTEMPT] Found playerEntry:`, playerEntry ? {id: playerEntry.id, identifier: playerEntry.identifier, oldSocketId: playerEntry.socket?.id, isTempDisc: playerEntry.isTemporarilyDisconnected} : null);
|
||||
|
||||
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сигнал, что таймер остановлен
|
||||
if (playerEntry) {
|
||||
const oldSocket = playerEntry.socket;
|
||||
|
||||
// Удаляем старую запись по socket.id, если сокет действительно новый
|
||||
const oldSocketId = playerEntry.socket.id;
|
||||
if (this.players[oldSocketId] && oldSocketId !== newSocket.id) {
|
||||
delete this.players[oldSocketId];
|
||||
// Обновляем сокет в playerEntry и в this.players / this.playerSockets, если сокет новый
|
||||
if (oldSocket && oldSocket.id !== newSocket.id) {
|
||||
console.log(`[PCH ${this.gameId}] New socket ${newSocket.id} for player ${identifier}. Old socket: ${oldSocket.id}. Updating records.`);
|
||||
if (this.players[oldSocket.id]) delete this.players[oldSocket.id]; // Удаляем старую запись по старому socket.id
|
||||
if (oldSocket.connected) { // Пытаемся корректно закрыть старый сокет
|
||||
console.log(`[PCH ${this.gameId}] Disconnecting old stale socket ${oldSocket.id}.`);
|
||||
oldSocket.disconnect(true);
|
||||
}
|
||||
}
|
||||
playerEntry.socket = newSocket; // Обновляем сокет в существующей playerEntry
|
||||
this.players[newSocket.id] = playerEntry; // Убеждаемся, что по новому ID есть актуальная запись
|
||||
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
|
||||
// Если вдруг playerEntry был взят по старому socket.id, и этот ID теперь должен быть удален
|
||||
delete this.players[oldSocket.id];
|
||||
}
|
||||
this.playerSockets[playerIdRole] = newSocket; // Обновляем авторитетный сокет для роли
|
||||
|
||||
// Обновляем запись игрока
|
||||
playerEntry.socket = newSocket;
|
||||
playerEntry.isTemporarilyDisconnected = false;
|
||||
this.players[newSocket.id] = playerEntry; // Добавляем/обновляем запись с новым socket.id
|
||||
this.playerSockets[playerIdRole] = newSocket; // Обновляем активный сокет для роли
|
||||
this.playerCount++; // Восстанавливаем счетчик активных игроков
|
||||
|
||||
// Всегда заново присоединяем сокет к комнате
|
||||
console.log(`[PCH ${this.gameId}] Forcing newSocket ${newSocket.id} (identifier: ${identifier}) to join room ${this.gameId} during reconnect.`);
|
||||
newSocket.join(this.gameId);
|
||||
const reconnectedName = this.gameInstance.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey;
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`);
|
||||
this.gameInstance.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
// Получаем ключ персонажа оппонента из gameState ИЛИ из предварительно сохраненных ключей в GameInstance
|
||||
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
||||
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
|
||||
// Если gameState нет (маловероятно при реконнекте в активную игру, но возможно если это был первый игрок PvP)
|
||||
// GameInstance должен сам решить, нужно ли ему initializeGame()
|
||||
if (!this.gameInstance.gameState) {
|
||||
// Пытаемся инициализировать игру, если она не была инициализирована
|
||||
// Это важно, если первый игрок в PvP отключался до подключения второго
|
||||
if (!this.gameInstance.initializeGame()) {
|
||||
this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch', 'PCH: GS null after re-init on reconnect.');
|
||||
return false;
|
||||
}
|
||||
if (playerEntry.isTemporarilyDisconnected) {
|
||||
console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`);
|
||||
this.clearReconnectTimer(playerIdRole); // Очищаем таймер реконнекта
|
||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сообщаем UI, что таймер остановлен
|
||||
|
||||
playerEntry.isTemporarilyDisconnected = false;
|
||||
this.playerCount++; // Восстанавливаем счетчик активных игроков
|
||||
} else {
|
||||
// Игрок не был помечен как временно отключенный.
|
||||
// Это может быть F5 или запрос состояния на "том же" (или новом, но старый не отвалился) сокете.
|
||||
// playerCount не меняется, т.к. игрок считался активным.
|
||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Old socket ID: ${oldSocket?.id}`);
|
||||
}
|
||||
|
||||
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.gameId,
|
||||
yourPlayerId: playerIdRole,
|
||||
initialGameState: this.gameInstance.gameState, // Отправляем текущее состояние
|
||||
playerBaseStats: pData?.baseStats, // Данные для этого игрока
|
||||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||
playerAbilities: pData?.abilities,
|
||||
opponentAbilities: oData?.abilities || [],
|
||||
log: this.gameInstance.consumeLogBuffer(),
|
||||
clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
|
||||
});
|
||||
|
||||
// Уведомляем другого игрока
|
||||
const otherSocket = this.playerSockets[oppRoleKey];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey);
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
otherSocket.emit('playerReconnected', {
|
||||
reconnectedPlayerId: playerIdRole,
|
||||
reconnectedPlayerName: reconnectedName
|
||||
});
|
||||
if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи, если есть
|
||||
otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() });
|
||||
}
|
||||
// Обновление имени
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState[playerIdRole]?.name) {
|
||||
playerEntry.name = this.gameInstance.gameState[playerIdRole].name;
|
||||
} else {
|
||||
const charData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
playerEntry.name = charData?.baseStats?.name || playerEntry.chosenCharacterKey;
|
||||
}
|
||||
console.log(`[PCH ${this.gameId}] Имя игрока ${identifier} обновлено/установлено на: ${playerEntry.name}`);
|
||||
|
||||
// Если игра не на "эффективной" паузе и не закончена, возобновляем игру
|
||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
||||
this.gameInstance.broadcastGameStateUpdate(); // Обновляем состояние для всех
|
||||
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
|
||||
this.gameInstance.turnTimer.resume(
|
||||
this.pausedTurnState.remainingTime,
|
||||
this.pausedTurnState.forPlayerRoleIsPlayer,
|
||||
this.pausedTurnState.isAiCurrentlyMoving
|
||||
);
|
||||
this.pausedTurnState = null; // Сбрасываем сохраненное состояние таймера
|
||||
} else {
|
||||
// Если pausedTurnState нет, значит, таймер не был активен или это первый ход
|
||||
// GameInstance.startGame или switchTurn должны запустить таймер корректно
|
||||
// Но если это реконнект в середину игры, где ход уже чей-то, нужно запустить таймер
|
||||
const currentTurnIsForPlayer = this.gameInstance.gameState.isPlayerTurn;
|
||||
const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer;
|
||||
this.gameInstance.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi);
|
||||
this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole);
|
||||
|
||||
if (playerEntry.isTemporarilyDisconnected === false && this.pausedTurnState) { // Если игрок был временно отключен, isTemporarilyDisconnected уже false
|
||||
this.resumeGameLogicAfterReconnect(playerIdRole);
|
||||
} else if (playerEntry.isTemporarilyDisconnected === false && !this.pausedTurnState) {
|
||||
// Игрок не был temp disconnected, и не было сохраненного состояния таймера (значит, он и не останавливался из-за этого игрока)
|
||||
// Просто отправляем текущее состояние таймера, если он активен
|
||||
console.log(`[PCH ${this.gameId}] Player was not temp disconnected, and no pausedTurnState. Forcing timer update if active.`);
|
||||
if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive() && this.gameInstance.turnTimer.onTickCallback) {
|
||||
const tt = this.gameInstance.turnTimer;
|
||||
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
|
||||
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
|
||||
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
|
||||
} else if (this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused() && !this.isGameEffectivelyPaused()) {
|
||||
// Если таймер не активен, не на паузе, и игра не на общей паузе - возможно, его нужно запустить (если сейчас ход этого игрока)
|
||||
const gs = this.gameInstance.gameState;
|
||||
if (gs && !gs.isGameOver) {
|
||||
const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID);
|
||||
const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn;
|
||||
if(isHisTurnNow || isAiTurnNow) {
|
||||
console.log(`[PCH ${this.gameId}] Timer not active, not paused. Game not paused. Attempting to start timer for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`);
|
||||
this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow);
|
||||
if (isAiTurnNow && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) {
|
||||
// Доп. проверка, чтобы AI точно пошел, если это его ход и таймер не стартовал для него как "AI move"
|
||||
setTimeout(() => {
|
||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
||||
this.gameInstance.processAiTurn();
|
||||
}
|
||||
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
|
||||
// Игрок уже был подключен и не был отмечен как isTemporarilyDisconnected
|
||||
// Это может быть попытка открыть игру в новой вкладке или "обновить сессию"
|
||||
if (playerEntry.socket.id !== newSocket.id) {
|
||||
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."});
|
||||
return false; // Не позволяем подключиться с нового сокета, если старый активен
|
||||
}
|
||||
// Если это тот же сокет (например, клиент запросил состояние), просто отправляем ему данные
|
||||
if (!this.gameInstance.gameState) { // На всякий случай, если gameState вдруг нет
|
||||
if (!this.gameInstance.initializeGame()) {
|
||||
this.gameInstance._handleCriticalError('reconnect_same_socket_no_gs_pch','PCH: GS null on same socket reconnect.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
||||
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.gameId,
|
||||
yourPlayerId: playerIdRole,
|
||||
initialGameState: this.gameInstance.gameState,
|
||||
playerBaseStats: pData?.baseStats,
|
||||
opponentBaseStats: oData?.baseStats, // Могут быть неполными, если оппонент еще не подключился
|
||||
playerAbilities: pData?.abilities,
|
||||
opponentAbilities: oData?.abilities,
|
||||
log: this.gameInstance.consumeLogBuffer(),
|
||||
clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
// Запись игрока не найдена или он не был помечен как isTemporarilyDisconnected, но сокет новый.
|
||||
// Это может быть попытка реконнекта к игре, из которой игрок был уже удален (например, по таймауту).
|
||||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена или сессия устарела).' });
|
||||
} else { // playerEntry не найден
|
||||
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена. Это может быть новый игрок или сессия истекла.`);
|
||||
// Если это новый игрок для этой роли, то addPlayer должен был быть вызван GameManager'ом.
|
||||
// Если PCH вызывается напрямую, и игрока нет, это ошибка или устаревший запрос.
|
||||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена). Попробуйте создать игру заново.' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sendFullGameStateOnReconnect(socket, playerEntry, playerIdRole) {
|
||||
console.log(`[PCH SEND_STATE_RECONNECT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${playerEntry.identifier}`);
|
||||
if (!this.gameInstance.gameState) {
|
||||
console.log(`[PCH SEND_STATE_RECONNECT] gameState отсутствует, попытка инициализации...`);
|
||||
if (!this.gameInstance.initializeGame()) { // initializeGame должен установить gameState
|
||||
this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch_helper', 'PCH Helper: GS null после повторной инициализации при переподключении.');
|
||||
return;
|
||||
}
|
||||
console.log(`[PCH SEND_STATE_RECONNECT] gameState инициализирован. Player: ${this.gameInstance.gameState.player.name}, Opponent: ${this.gameInstance.gameState.opponent.name}`);
|
||||
}
|
||||
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
|
||||
// Получаем ключ оппонента из gameState ИЛИ из сохраненных ключей в GameInstance
|
||||
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
||||
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
|
||||
// Обновляем имена в gameState на основе сохраненных в PCH или данных персонажей
|
||||
if (this.gameInstance.gameState) {
|
||||
if (this.gameInstance.gameState[playerIdRole]) {
|
||||
this.gameInstance.gameState[playerIdRole].name = playerEntry.name || pData?.baseStats?.name || 'Игрок';
|
||||
}
|
||||
const opponentPCHEntry = Object.values(this.players).find(p => p.id === oppRoleKey);
|
||||
if (this.gameInstance.gameState[oppRoleKey]) {
|
||||
if (opponentPCHEntry?.name) {
|
||||
this.gameInstance.gameState[oppRoleKey].name = opponentPCHEntry.name;
|
||||
} else if (oData?.baseStats?.name) {
|
||||
this.gameInstance.gameState[oppRoleKey].name = oData.baseStats.name;
|
||||
} else if (this.mode === 'ai' && oppRoleKey === GAME_CONFIG.OPPONENT_ID) {
|
||||
this.gameInstance.gameState[oppRoleKey].name = 'Балард'; // Фоллбэк для AI
|
||||
} else {
|
||||
this.gameInstance.gameState[oppRoleKey].name = 'Оппонент';
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`[PCH SEND_STATE_RECONNECT] Отправка gameStarted. Player GS: ${this.gameInstance.gameState?.player?.name}, Opponent GS: ${this.gameInstance.gameState?.opponent?.name}. IsPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`);
|
||||
|
||||
socket.emit('gameStarted', { // Используем 'gameStarted' для полной синхронизации состояния
|
||||
gameId: this.gameId,
|
||||
yourPlayerId: playerIdRole,
|
||||
initialGameState: this.gameInstance.gameState,
|
||||
playerBaseStats: pData?.baseStats,
|
||||
opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||
playerAbilities: pData?.abilities,
|
||||
opponentAbilities: oData?.abilities || [],
|
||||
log: this.gameInstance.consumeLogBuffer(),
|
||||
clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
}
|
||||
|
||||
resumeGameLogicAfterReconnect(reconnectedPlayerIdRole) {
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === reconnectedPlayerIdRole);
|
||||
const reconnectedName = playerEntry?.name || this.gameInstance.gameState?.[reconnectedPlayerIdRole]?.name || `Игрок (Роль ${reconnectedPlayerIdRole})`;
|
||||
console.log(`[PCH RESUME_LOGIC] gameId: ${this.gameId}, Role: ${reconnectedPlayerIdRole}, Name: ${reconnectedName}, PausedState: ${JSON.stringify(this.pausedTurnState)}, TimerActive: ${this.gameInstance.turnTimer?.isActive()}, GS.isPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`);
|
||||
|
||||
const otherPlayerRole = reconnectedPlayerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
const otherSocket = this.playerSockets[otherPlayerRole];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole);
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
otherSocket.emit('playerReconnected', {
|
||||
reconnectedPlayerId: reconnectedPlayerIdRole,
|
||||
reconnectedPlayerName: reconnectedName
|
||||
});
|
||||
if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи другому игроку
|
||||
otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() });
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем состояние для всех (включая переподключившегося, т.к. его лог мог быть уже потреблен)
|
||||
this.gameInstance.broadcastGameStateUpdate(); // Это отправит gameState и оставшиеся логи
|
||||
|
||||
|
||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
||||
// this.gameInstance.broadcastGameStateUpdate(); // Перенесено выше
|
||||
|
||||
if (Object.keys(this.reconnectTimers).length === 0) { // Только если нет других ожидающих реконнекта
|
||||
const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn;
|
||||
const isCurrentTurnAiForTimer = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
|
||||
let resumedFromPausedState = false;
|
||||
|
||||
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
|
||||
const gsTurnMatchesPausedTurn = (currentTurnIsForPlayerInGS && this.pausedTurnState.forPlayerRoleIsPlayer) ||
|
||||
(!currentTurnIsForPlayerInGS && !this.pausedTurnState.forPlayerRoleIsPlayer);
|
||||
|
||||
if (gsTurnMatchesPausedTurn) {
|
||||
console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`);
|
||||
this.gameInstance.turnTimer.resume(
|
||||
this.pausedTurnState.remainingTime,
|
||||
this.pausedTurnState.forPlayerRoleIsPlayer, // Это isConfiguredForPlayerSlotTurn для таймера
|
||||
this.pausedTurnState.isAiCurrentlyMoving // Это isConfiguredForAiMove для таймера
|
||||
);
|
||||
resumedFromPausedState = true;
|
||||
} else {
|
||||
console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) не совпадает с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState и запускаем таймер заново, если нужно.`);
|
||||
}
|
||||
this.pausedTurnState = null; // Сбрасываем в любом случае
|
||||
}
|
||||
|
||||
if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) {
|
||||
console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался или был неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isCurrentTurnAiForTimer}`);
|
||||
this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isCurrentTurnAiForTimer);
|
||||
if (isCurrentTurnAiForTimer && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) {
|
||||
setTimeout(() => {
|
||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
||||
this.gameInstance.processAiTurn();
|
||||
}
|
||||
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
}
|
||||
} else if (!resumedFromPausedState && this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive()){
|
||||
console.log(`[PCH ${this.gameId}] Таймер уже был активен при попытке перезапуска после реконнекта (pausedTurnState не использовался/неактуален). Ничего не делаем с таймером.`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[PCH ${this.gameId}] Возобновление логики таймера отложено, есть другие активные таймеры реконнекта: ${Object.keys(this.reconnectTimers)}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[PCH ${this.gameId}] Игра на паузе или завершена, логика таймера не возобновляется. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameInstance.gameState?.isGameOver}`);
|
||||
}
|
||||
}
|
||||
|
||||
clearReconnectTimer(playerIdRole) {
|
||||
if (this.reconnectTimers[playerIdRole]) {
|
||||
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
|
||||
this.reconnectTimers[playerIdRole].timerId = null; // Явно обнуляем
|
||||
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
|
||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
this.reconnectTimers[playerIdRole].updateIntervalId = null; // Явно обнуляем
|
||||
}
|
||||
delete this.reconnectTimers[playerIdRole];
|
||||
console.log(`[PCH ${this.gameId}] Cleared reconnect timer for role ${playerIdRole}.`);
|
||||
delete this.reconnectTimers[playerIdRole]; // Удаляем всю запись
|
||||
console.log(`[PCH ${this.gameId}] Очищен таймер переподключения для роли ${playerIdRole}.`);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllReconnectTimers() {
|
||||
console.log(`[PCH ${this.gameId}] Clearing ALL reconnect timers.`);
|
||||
console.log(`[PCH ${this.gameId}] Очистка ВСЕХ таймеров переподключения.`);
|
||||
for (const roleId in this.reconnectTimers) {
|
||||
this.clearReconnectTimer(roleId);
|
||||
}
|
||||
@ -365,30 +474,25 @@ class PlayerConnectionHandler {
|
||||
|
||||
isGameEffectivelyPaused() {
|
||||
if (this.mode === 'pvp') {
|
||||
// Если игроков меньше 2, И есть хотя бы один игрок в this.players (ожидающий или в процессе дисконнекта)
|
||||
if (this.playerCount < 2 && Object.keys(this.players).length > 0) {
|
||||
// Проверяем, есть ли кто-то из них в состоянии временного дисконнекта
|
||||
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||
|
||||
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
|
||||
return true; // Игра на паузе, если один из игроков временно отключен
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (this.mode === 'ai') {
|
||||
// В AI режиме игра на паузе, если единственный человек-игрок временно отключен
|
||||
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния.
|
||||
}
|
||||
return false; // В остальных случаях игра не считается на паузе из-за дисконнектов
|
||||
return false;
|
||||
}
|
||||
|
||||
// Вспомогательный метод для получения информации о всех игроках (может пригодиться GameInstance)
|
||||
getAllPlayersInfo() {
|
||||
return { ...this.players };
|
||||
}
|
||||
|
||||
// Вспомогательный метод для получения сокетов (может пригодиться GameInstance)
|
||||
getPlayerSockets() {
|
||||
return { ...this.playerSockets };
|
||||
}
|
||||
|
@ -1,197 +1,30 @@
|
||||
// /server/game/instance/TurnTimer.js
|
||||
|
||||
class TurnTimer {
|
||||
/**
|
||||
* Конструктор таймера хода.
|
||||
* @param {number} turnDurationMs - Изначальная длительность хода в миллисекундах.
|
||||
* @param {number} updateIntervalMs - Интервал для отправки обновлений времени клиентам (в мс).
|
||||
* @param {function} onTimeoutCallback - Колбэк, вызываемый при истечении времени хода.
|
||||
* @param {function} onTickCallback - Колбэк, вызываемый на каждом тике обновления (передает remainingTime, isPlayerTurnForTimer, isPaused).
|
||||
* @param {string} [gameIdForLogs=''] - (Опционально) ID игры для более понятных логов таймера.
|
||||
*/
|
||||
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') {
|
||||
this.initialTurnDurationMs = turnDurationMs; // Сохраняем начальную полную длительность хода
|
||||
this.currentEffectiveDurationMs = turnDurationMs; // Длительность, с которой стартует текущий отсчет (может быть меньше initial при resume)
|
||||
|
||||
this.initialTurnDurationMs = turnDurationMs;
|
||||
this.updateIntervalMs = updateIntervalMs;
|
||||
this.onTimeoutCallback = onTimeoutCallback;
|
||||
this.onTickCallback = onTickCallback;
|
||||
this.gameId = gameIdForLogs; // Для логов
|
||||
this.onTickCallback = onTickCallback; // (remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic)
|
||||
this.gameId = gameIdForLogs;
|
||||
|
||||
this.timeoutId = null; // ID для setTimeout (обработка общего таймаута хода)
|
||||
this.tickIntervalId = null; // ID для setInterval (периодическое обновление клиента)
|
||||
this.timeoutId = null;
|
||||
this.tickIntervalId = null;
|
||||
|
||||
this.startTimeMs = 0; // Время (Date.now()) начала текущего отсчета таймера
|
||||
this.isRunning = false; // Активен ли таймер в данный момент (идет отсчет)
|
||||
this.segmentStartTimeMs = 0; // Время начала текущего активного сегмента (после start/resume)
|
||||
this.segmentDurationMs = 0; // Длительность, с которой был запущен текущий сегмент
|
||||
|
||||
// Состояние, для которого был запущен/приостановлен таймер
|
||||
this.isForPlayerTurn = false; // true, если таймер отсчитывает ход игрока (слот 'player')
|
||||
this.isAiCurrentlyMoving = false; // true, если это ход AI, и таймер для реального игрока не должен "тикать"
|
||||
this.isCurrentlyRunning = false; // Идет ли активный отсчет (не на паузе, не ход AI)
|
||||
this.isManuallyPausedState = false; // Была ли вызвана pause()
|
||||
|
||||
this.isManuallyPaused = false; // Флаг, что таймер был приостановлен вызовом pause()
|
||||
// console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
||||
// Состояние, для которого таймер был запущен (или должен быть запущен)
|
||||
this.isConfiguredForPlayerSlotTurn = false;
|
||||
this.isConfiguredForAiMove = false;
|
||||
|
||||
console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает или перезапускает таймер хода.
|
||||
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player', false - если ход слота 'opponent'.
|
||||
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI (таймер для реального игрока не тикает).
|
||||
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого оставшегося времени.
|
||||
*/
|
||||
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
||||
this.clear(true); // Очищаем предыдущие таймеры, сохраняя флаг isManuallyPaused если это resume
|
||||
|
||||
this.isForPlayerTurn = isPlayerSlotTurn;
|
||||
this.isAiCurrentlyMakingMove = isAiMakingMove;
|
||||
// При явном старте (не resume) сбрасываем флаг ручной паузы
|
||||
if (customRemainingTimeMs === null) {
|
||||
this.isManuallyPaused = false;
|
||||
}
|
||||
|
||||
|
||||
if (this.isAiCurrentlyMakingMove) {
|
||||
this.isRunning = false; // Для хода AI основной таймер не "бежит" для игрока
|
||||
// console.log(`[TurnTimer ${this.gameId}] Start: AI's turn. Player timer not ticking.`);
|
||||
if (this.onTickCallback) {
|
||||
// Уведомляем один раз, что таймер неактивен (ход AI), передаем isPaused = false (т.к. это не ручная пауза)
|
||||
// Время может быть полным или оставшимся, если AI "думает"
|
||||
this.onTickCallback(this.initialTurnDurationMs, this.isForPlayerTurn, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Устанавливаем длительность для текущего запуска
|
||||
this.currentEffectiveDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0)
|
||||
? customRemainingTimeMs
|
||||
: this.initialTurnDurationMs;
|
||||
|
||||
this.startTimeMs = Date.now();
|
||||
this.isRunning = true;
|
||||
// console.log(`[TurnTimer ${this.gameId}] Started. Effective Duration: ${this.currentEffectiveDurationMs}ms. For ${this.isForPlayerTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${this.isAiCurrentlyMakingMove}`);
|
||||
|
||||
// Основной таймер на истечение времени хода
|
||||
this.timeoutId = setTimeout(() => {
|
||||
// console.log(`[TurnTimer ${this.gameId}] Timeout occurred! Was running: ${this.isRunning}`);
|
||||
if (this.isRunning) { // Доп. проверка, что таймер все еще должен был работать
|
||||
this.isRunning = false;
|
||||
if (this.onTimeoutCallback) {
|
||||
this.onTimeoutCallback();
|
||||
}
|
||||
this.clear(); // Очищаем и интервал обновления после таймаута
|
||||
}
|
||||
}, this.currentEffectiveDurationMs);
|
||||
|
||||
// Интервал для отправки обновлений клиентам
|
||||
this.tickIntervalId = setInterval(() => {
|
||||
if (!this.isRunning) {
|
||||
// Если таймер был остановлен (например, ход сделан, игра окончена, или pause вызван),
|
||||
// но интервал еще не очищен - очищаем.
|
||||
this.clear(this.isManuallyPaused); // Сохраняем флаг, если это была ручная пауза
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedTime = Date.now() - this.startTimeMs;
|
||||
const remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||||
|
||||
if (this.onTickCallback) {
|
||||
// isManuallyPaused здесь всегда false, т.к. если бы была пауза, isRunning был бы false
|
||||
this.onTickCallback(remainingTime, this.isForPlayerTurn, false);
|
||||
}
|
||||
|
||||
if (remainingTime <= 0 && this.isRunning) {
|
||||
// Время вышло по интервалу (на всякий случай, setTimeout должен сработать)
|
||||
// Не вызываем onTimeoutCallback здесь напрямую, чтобы избежать двойного вызова.
|
||||
this.clear(this.isManuallyPaused); // Очищаем интервал, setTimeout сработает для onTimeoutCallback
|
||||
}
|
||||
}, this.updateIntervalMs);
|
||||
|
||||
// Отправляем начальное значение немедленно
|
||||
if (this.onTickCallback) {
|
||||
this.onTickCallback(this.currentEffectiveDurationMs, this.isForPlayerTurn, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Приостанавливает таймер и возвращает его текущее состояние.
|
||||
* @returns {{remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean}}
|
||||
* - remainingTime: Оставшееся время в мс.
|
||||
* - forPlayerRoleIsPlayer: true, если таймер был для хода игрока (слот 'player').
|
||||
* - isAiCurrentlyMoving: true, если это был ход AI.
|
||||
*/
|
||||
pause() {
|
||||
// console.log(`[TurnTimer ${this.gameId}] Pause called. isRunning: ${this.isRunning}, isAiCurrentlyMoving: ${this.isAiCurrentlyMoving}`);
|
||||
|
||||
let remainingTime = 0;
|
||||
const wasForPlayerTurn = this.isForPlayerTurn;
|
||||
const wasAiMoving = this.isAiCurrentlyMoving;
|
||||
|
||||
if (this.isAiCurrentlyMakingMove) {
|
||||
// Если это был ход AI, таймер для игрока не тикал, считаем, что у него полное время.
|
||||
// Однако, если AI "думал" и мы хотим сохранить это, логика должна быть сложнее.
|
||||
// Для простоты, если AI ход, то время "не шло" для игрока.
|
||||
remainingTime = this.initialTurnDurationMs;
|
||||
// console.log(`[TurnTimer ${this.gameId}] Paused during AI move. Effective remaining time for player turn: ${remainingTime}ms.`);
|
||||
} else if (this.isRunning) {
|
||||
const elapsedTime = Date.now() - this.startTimeMs;
|
||||
remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||||
// console.log(`[TurnTimer ${this.gameId}] Paused while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTime}ms.`);
|
||||
} else {
|
||||
// Если таймер не был запущен (например, уже истек или был очищен),
|
||||
// или был уже на паузе, возвращаем 0 или последнее известное значение.
|
||||
// Если isManuallyPaused уже true, то просто возвращаем то, что было.
|
||||
remainingTime = this.isManuallyPaused ? this.currentEffectiveDurationMs : 0; // currentEffectiveDurationMs тут может быть уже оставшимся временем
|
||||
// console.log(`[TurnTimer ${this.gameId}] Pause called, but timer not actively running or already paused. Returning current/zero remaining time: ${remainingTime}ms.`);
|
||||
}
|
||||
|
||||
this.isManuallyPaused = true; // Устанавливаем флаг ручной паузы
|
||||
this.clear(true); // Очищаем внутренние таймеры, сохраняя флаг isManuallyPaused
|
||||
this.isRunning = false; // Явно указываем, что отсчет остановлен
|
||||
|
||||
// Уведомляем клиента, что таймер на паузе
|
||||
if (this.onTickCallback) {
|
||||
// console.log(`[TurnTimer ${this.gameId}] Notifying client of pause. Remaining: ${remainingTime}, ForPlayer: ${wasForPlayerTurn}`);
|
||||
this.onTickCallback(remainingTime, wasForPlayerTurn, true); // isPaused = true
|
||||
}
|
||||
|
||||
return { remainingTime, forPlayerRoleIsPlayer: wasForPlayerTurn, isAiCurrentlyMoving: wasAiMoving };
|
||||
}
|
||||
|
||||
/**
|
||||
* Возобновляет таймер с указанного оставшегося времени и для указанного состояния.
|
||||
* @param {number} remainingTimeMs - Оставшееся время в миллисекундах для возобновления.
|
||||
* @param {boolean} forPlayerSlotTurn - Для чьего хода (слот 'player' = true) возобновляется таймер.
|
||||
* @param {boolean} isAiMakingMove - Был ли это ход AI, когда таймер приостановили (и возобновляем ли ход AI).
|
||||
*/
|
||||
resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) {
|
||||
if (!this.isManuallyPaused) {
|
||||
// console.warn(`[TurnTimer ${this.gameId}] Resume called, but timer was not manually paused. Starting normally or doing nothing.`);
|
||||
// Если не был на ручной паузе, то либо запускаем заново (если не был ход AI), либо ничего не делаем
|
||||
// if (!isAiMakingMove) this.start(forPlayerSlotTurn, false, remainingTimeMs > 0 ? remainingTimeMs : null);
|
||||
// Безопаснее просто выйти, если не был на ручной паузе, GameInstance должен управлять этим.
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingTimeMs <= 0) {
|
||||
// console.log(`[TurnTimer ${this.gameId}] Resume called with 0 or less time. Triggering timeout.`);
|
||||
this.isManuallyPaused = false; // Сбрасываем флаг
|
||||
if (this.onTimeoutCallback) {
|
||||
this.onTimeoutCallback(); // Немедленный таймаут
|
||||
}
|
||||
return;
|
||||
}
|
||||
// console.log(`[TurnTimer ${this.gameId}] Resuming. Remaining: ${remainingTimeMs}ms. For ${forPlayerSlotTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${isAiMakingMove}`);
|
||||
|
||||
this.isManuallyPaused = false; // Сбрасываем флаг ручной паузы перед стартом
|
||||
// Запускаем таймер с сохраненным состоянием и оставшимся временем
|
||||
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает (останавливает) все активные таймеры (setTimeout и setInterval).
|
||||
* @param {boolean} [preserveManuallyPausedFlag=false] - Если true, не сбрасывает флаг isManuallyPaused.
|
||||
* Используется внутренне при вызове clear из pause().
|
||||
*/
|
||||
clear(preserveManuallyPausedFlag = false) {
|
||||
_clearInternalTimers() {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
@ -200,40 +33,205 @@ class TurnTimer {
|
||||
clearInterval(this.tickIntervalId);
|
||||
this.tickIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const wasPreviouslyRunning = this.isRunning; // Запоминаем, работал ли он до clear
|
||||
this.isRunning = false;
|
||||
// this.startTimeMs = 0; // Не сбрасываем startTime, чтобы pause мог корректно вычислить remainingTime
|
||||
/**
|
||||
* Запускает или перезапускает таймер хода.
|
||||
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player'.
|
||||
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI.
|
||||
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого времени.
|
||||
*/
|
||||
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
||||
console.log(`[TurnTimer ${this.gameId}] Attempting START. ForPlayer: ${isPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CustomTime: ${customRemainingTimeMs}, ManualPause: ${this.isManuallyPausedState}`);
|
||||
this._clearInternalTimers(); // Всегда очищаем старые таймеры перед новым запуском
|
||||
|
||||
if (!preserveManuallyPausedFlag) {
|
||||
this.isManuallyPaused = false;
|
||||
this.isConfiguredForPlayerSlotTurn = isPlayerSlotTurn;
|
||||
this.isConfiguredForAiMove = isAiMakingMove;
|
||||
|
||||
// Если это не resume (т.е. customRemainingTimeMs не передан явно как результат pause),
|
||||
// то сбрасываем флаг ручной паузы.
|
||||
if (customRemainingTimeMs === null) {
|
||||
this.isManuallyPausedState = false;
|
||||
}
|
||||
|
||||
// Если таймер был очищен не через pause(), он был активен (и это не был ход AI, который и так не тикает)
|
||||
// то опционально можно уведомить клиента, что таймер больше не тикает (например, ход сделан)
|
||||
// Это может быть полезно, чтобы клиент сбросил свой отображаемый таймер на '--'
|
||||
// if (wasPreviouslyRunning && !this.isAiCurrentlyMakingMove && !this.isManuallyPaused && this.onTickCallback) {
|
||||
// // console.log(`[TurnTimer ${this.gameId}] Cleared while running (not AI, not manual pause). Notifying client.`);
|
||||
// this.onTickCallback(null, this.isForPlayerTurn, this.isManuallyPaused); // remainingTime = null
|
||||
if (this.isConfiguredForAiMove) {
|
||||
this.isCurrentlyRunning = false; // Для хода AI основной таймер не "бежит" для игрока
|
||||
console.log(`[TurnTimer ${this.gameId}] START: AI's turn. Player timer not actively ticking.`);
|
||||
if (this.onTickCallback) {
|
||||
// Отправляем состояние "ход AI", таймер не тикает для игрока, не на ручной паузе
|
||||
this.onTickCallback(this.initialTurnDurationMs, this.isConfiguredForPlayerSlotTurn, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Если это не ход AI, то таймер должен работать для игрока (или оппонента-человека)
|
||||
this.segmentDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0)
|
||||
? customRemainingTimeMs
|
||||
: this.initialTurnDurationMs;
|
||||
|
||||
this.segmentStartTimeMs = Date.now();
|
||||
this.isCurrentlyRunning = true; // Таймер теперь активен
|
||||
// this.isManuallyPausedState остается как есть, если это был resume, или false, если это новый start
|
||||
|
||||
console.log(`[TurnTimer ${this.gameId}] STARTED. Effective Duration: ${this.segmentDurationMs}ms. ForPlayer: ${this.isConfiguredForPlayerSlotTurn}. IsRunning: ${this.isCurrentlyRunning}. ManualPause: ${this.isManuallyPausedState}`);
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT occurred. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
||||
// Проверяем, что таймер все еще должен был работать и не был на паузе
|
||||
if (this.isCurrentlyRunning && !this.isManuallyPausedState) {
|
||||
this._clearInternalTimers(); // Очищаем все, включая интервал
|
||||
this.isCurrentlyRunning = false;
|
||||
if (this.onTimeoutCallback) {
|
||||
this.onTimeoutCallback();
|
||||
}
|
||||
} else {
|
||||
console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT ignored (not running or manually paused).`);
|
||||
}
|
||||
}, this.segmentDurationMs);
|
||||
|
||||
this.tickIntervalId = setInterval(() => {
|
||||
// Таймер должен обновлять UI только если он isCurrentlyRunning и НЕ isManuallyPausedState
|
||||
// isManuallyPausedState проверяется в onTickCallback, который должен передать "isPaused" клиенту
|
||||
if (!this.isCurrentlyRunning) { // Если таймер был остановлен (clear/timeout)
|
||||
this._clearInternalTimers(); // Убедимся, что этот интервал тоже остановлен
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedTime = Date.now() - this.segmentStartTimeMs;
|
||||
const remainingTime = Math.max(0, this.segmentDurationMs - elapsedTime);
|
||||
|
||||
if (this.onTickCallback) {
|
||||
// Передаем isManuallyPausedState как состояние "паузы" для клиента
|
||||
this.onTickCallback(remainingTime, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
||||
}
|
||||
|
||||
// Не очищаем интервал здесь при remainingTime <= 0, пусть setTimeout это сделает.
|
||||
// Отправка 0 - это нормально.
|
||||
}, this.updateIntervalMs);
|
||||
|
||||
// Немедленная первая отправка состояния таймера
|
||||
if (this.onTickCallback) {
|
||||
console.log(`[TurnTimer ${this.gameId}] Initial tick after START. Remaining: ${this.segmentDurationMs}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}, ManualPause: ${this.isManuallyPausedState}`);
|
||||
this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
console.log(`[TurnTimer ${this.gameId}] Attempting PAUSE. IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}, ManualPause: ${this.isManuallyPausedState}`);
|
||||
|
||||
if (this.isManuallyPausedState) { // Уже на ручной паузе
|
||||
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but already manually paused. Returning previous pause state.`);
|
||||
// Нужно вернуть актуальное оставшееся время, которое было на момент установки паузы.
|
||||
// segmentDurationMs при паузе сохраняет это значение.
|
||||
if (this.onTickCallback) { // Уведомляем клиента еще раз, что на паузе
|
||||
this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, true);
|
||||
}
|
||||
return {
|
||||
remainingTime: this.segmentDurationMs, // Это время, которое осталось на момент паузы
|
||||
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
|
||||
isAiCurrentlyMoving: this.isConfiguredForAiMove // Важно сохранить, чей ход это был
|
||||
};
|
||||
}
|
||||
|
||||
let remainingTimeToSave;
|
||||
|
||||
if (this.isConfiguredForAiMove) {
|
||||
// Если ход AI, таймер для игрока не тикал, у него полное время
|
||||
remainingTimeToSave = this.initialTurnDurationMs;
|
||||
console.log(`[TurnTimer ${this.gameId}] PAUSED during AI move. Effective remaining: ${remainingTimeToSave}ms for player turn.`);
|
||||
} else if (this.isCurrentlyRunning) {
|
||||
// Таймер активно работал для игрока/оппонента-человека
|
||||
const elapsedTime = Date.now() - this.segmentStartTimeMs;
|
||||
remainingTimeToSave = Math.max(0, this.segmentDurationMs - elapsedTime);
|
||||
console.log(`[TurnTimer ${this.gameId}] PAUSED while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTimeToSave}ms from segment duration ${this.segmentDurationMs}ms.`);
|
||||
} else {
|
||||
// Таймер не был активен (например, уже истек, был очищен, или это был start() для AI)
|
||||
// В этом случае, если не ход AI, то время 0
|
||||
remainingTimeToSave = 0;
|
||||
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but timer not actively running (and not AI move). Remaining set to 0.`);
|
||||
}
|
||||
|
||||
this._clearInternalTimers();
|
||||
this.isCurrentlyRunning = false;
|
||||
this.isManuallyPausedState = true;
|
||||
this.segmentDurationMs = remainingTimeToSave; // Сохраняем оставшееся время для resume
|
||||
|
||||
if (this.onTickCallback) {
|
||||
console.log(`[TurnTimer ${this.gameId}] Notifying client of PAUSE. Remaining: ${remainingTimeToSave}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}`);
|
||||
this.onTickCallback(remainingTimeToSave, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true
|
||||
}
|
||||
|
||||
return {
|
||||
remainingTime: remainingTimeToSave,
|
||||
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn, // Чей ход это был
|
||||
isAiCurrentlyMoving: this.isConfiguredForAiMove // Был ли это ход AI
|
||||
};
|
||||
}
|
||||
|
||||
resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) {
|
||||
console.log(`[TurnTimer ${this.gameId}] Attempting RESUME. SavedRemaining: ${remainingTimeMs}, ForPlayer: ${forPlayerSlotTurn}, IsAI: ${isAiMakingMove}, ManualPauseBefore: ${this.isManuallyPausedState}`);
|
||||
|
||||
if (!this.isManuallyPausedState) {
|
||||
console.warn(`[TurnTimer ${this.gameId}] RESUME called, but timer was not manually paused. Current state - IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}. Ignoring resume, let PCH handle start if needed.`);
|
||||
// Если не был на ручной паузе, возможно, игра уже продолжается или была очищена.
|
||||
// Не вызываем start() отсюда, чтобы избежать неожиданного поведения.
|
||||
// PCH должен решить, нужен ли новый start().
|
||||
// Однако, если текущий ход совпадает, и таймер просто неактивен, можно запустить.
|
||||
// Но лучше, чтобы PCH всегда вызывал start() с нуля, если resume не применим.
|
||||
// Просто отправим текущее состояние, если onTickCallback есть.
|
||||
if (this.onTickCallback) {
|
||||
const currentElapsedTime = this.isCurrentlyRunning ? (Date.now() - this.segmentStartTimeMs) : 0;
|
||||
const currentRemaining = this.isCurrentlyRunning ? Math.max(0, this.segmentDurationMs - currentElapsedTime) : this.segmentDurationMs;
|
||||
this.onTickCallback(currentRemaining, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingTimeMs <= 0 && !isAiMakingMove) { // Если не ход AI и время вышло
|
||||
console.log(`[TurnTimer ${this.gameId}] RESUME called with 0 or less time (and not AI move). Triggering timeout.`);
|
||||
this.isManuallyPausedState = false; // Сбрасываем флаг
|
||||
this._clearInternalTimers(); // Убедимся, что все остановлено
|
||||
this.isCurrentlyRunning = false;
|
||||
if (this.onTimeoutCallback) {
|
||||
this.onTimeoutCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем флаг ручной паузы и запускаем таймер с сохраненным состоянием
|
||||
this.isManuallyPausedState = false;
|
||||
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs); // `start` теперь правильно обработает customRemainingTimeMs
|
||||
}
|
||||
|
||||
clear() {
|
||||
console.log(`[TurnTimer ${this.gameId}] CLEAR called. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
||||
this._clearInternalTimers();
|
||||
this.isCurrentlyRunning = false;
|
||||
// При полном clear сбрасываем и ручную паузу, т.к. таймер полностью останавливается.
|
||||
// `pause` использует этот метод, но затем сам выставляет isManuallyPausedState = true.
|
||||
this.isManuallyPausedState = false;
|
||||
this.segmentDurationMs = 0; // Сбрасываем сохраненную длительность
|
||||
this.segmentStartTimeMs = 0;
|
||||
|
||||
// Опционально: уведомить клиента, что таймер остановлен (например, null или 0)
|
||||
// if (this.onTickCallback) {
|
||||
// this.onTickCallback(null, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true (т.к. он остановлен)
|
||||
// }
|
||||
// console.log(`[TurnTimer ${this.gameId}] Cleared. Was running: ${wasPreviouslyRunning}. PreservePaused: ${preserveManuallyPausedFlag}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, активен ли таймер в данный момент (идет ли отсчет).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isActive() {
|
||||
return this.isRunning;
|
||||
// Таймер активен, если он isCurrentlyRunning и не на ручной паузе
|
||||
return this.isCurrentlyRunning && !this.isManuallyPausedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, был ли таймер приостановлен вручную вызовом pause().
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPaused() {
|
||||
return this.isManuallyPaused;
|
||||
isPaused() { // Возвращает, находится ли таймер в состоянии ручной паузы
|
||||
return this.isManuallyPausedState;
|
||||
}
|
||||
|
||||
// Этот геттер больше не нужен в таком виде, т.к. isConfiguredForAiMove хранит это состояние
|
||||
// get isAiCurrentlyMakingMove() {
|
||||
// return this.isConfiguredForAiMove && !this.isCurrentlyRunning;
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = TurnTimer;
|
Loading…
x
Reference in New Issue
Block a user