bc/server/game/instance/PlayerConnectionHandler.js

501 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// /server/game/instance/PlayerConnectionHandler.js
const GAME_CONFIG = require('../../core/config');
const dataUtils = require('../../data/dataUtils');
class PlayerConnectionHandler {
constructor(gameInstance) {
this.gameInstance = gameInstance; // Ссылка на основной GameInstance
this.io = gameInstance.io;
this.gameId = gameInstance.id;
this.mode = gameInstance.mode;
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}] Инициализирован.`);
}
addPlayer(socket, chosenCharacterKey = 'elena', 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} уже связан с ролью игрока ${existingPlayerByIdentifier.id} (сокет ${existingPlayerByIdentifier.socket?.id}). Обрабатывается как возможное переподключение.`);
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
console.warn(`[PCH ${this.gameId}] Игрок ${identifier} пытается (пере)присоединиться к уже завершенной игре. Отправка gameError.`);
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 && 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]) { // Эта проверка уже покрыта playerCount >= 1 выше
// socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
// return false;
// }
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
} else { // pvp
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
} else if (!this.playerSockets[GAME_CONFIG.OPPONENT_ID]) {
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
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 { // Оба слота заняты, но 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];
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,
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).`);
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey);
else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.gameInstance.setOpponentCharacterKey(actualCharacterKey);
if (!this.gameInstance.ownerIdentifier && (this.mode === 'ai' || (this.mode === 'pvp' && assignedPlayerId === GAME_CONFIG.PLAYER_ID))) {
this.gameInstance.setOwnerIdentifier(identifier);
}
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Socket: ${socket.id}) добавлен как ${assignedPlayerId} с персонажем ${this.players[socket.id].name}. Активных игроков: ${this.playerCount}. Владелец: ${this.gameInstance.ownerIdentifier}`);
return true;
}
removePlayer(socketId, reason = "unknown_reason_for_removal") {
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id;
const playerIdentifier = playerInfo.identifier;
console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`);
if (playerInfo.socket) {
try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave: ${e.message}`); }
}
if (!playerInfo.isTemporarilyDisconnected) {
this.playerCount--;
}
delete this.players[socketId];
if (this.playerSockets[playerRole]?.id === socketId) {
delete this.playerSockets[playerRole];
}
this.clearReconnectTimer(playerRole);
console.log(`[PCH ${this.gameId}] Игрок ${playerIdentifier} удален. Активных игроков сейчас: ${this.playerCount}.`);
this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason);
} else {
console.warn(`[PCH ${this.gameId}] removePlayer вызван для неизвестного socketId: ${socketId}`);
}
}
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}] Запись игрока или сокет не найдены для ${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}] Игра уже завершена, не обрабатываем потенциальный выход для ${identifier}.`);
return;
}
if (playerEntry.isTemporarilyDisconnected) {
console.log(`[PCH ${this.gameId}] Игрок ${identifier} уже помечен как временно отключенный.`);
return;
}
playerEntry.isTemporarilyDisconnected = true;
this.playerCount--;
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`);
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];
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole);
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
otherSocket.emit('opponentDisconnected', {
disconnectedPlayerId: playerIdRole,
disconnectedCharacterName: disconnectedName,
});
}
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}] Таймер хода приостановлен из-за отключения. Состояние:`, JSON.stringify(this.pausedTurnState));
} else {
this.pausedTurnState = null;
}
this.clearReconnectTimer(playerIdRole);
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
const reconnectStartTime = Date.now();
const updateInterval = setInterval(() => {
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
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(() => {
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) {
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
}
}, reconnectDuration);
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
}
handlePlayerReconnected(playerIdRole, newSocket) {
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: 'Игра уже завершена.' });
return false;
}
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) {
const oldSocket = playerEntry.socket;
// Обновляем сокет в 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; // Обновляем авторитетный сокет для роли
// Всегда заново присоединяем сокет к комнате
console.log(`[PCH ${this.gameId}] Forcing newSocket ${newSocket.id} (identifier: ${identifier}) to join room ${this.gameId} during reconnect.`);
newSocket.join(this.gameId);
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}`);
}
// Обновление имени
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}`);
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 { // 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}] Очищен таймер переподключения для роли ${playerIdRole}.`);
}
}
clearAllReconnectTimers() {
console.log(`[PCH ${this.gameId}] Очистка ВСЕХ таймеров переподключения.`);
for (const roleId in this.reconnectTimers) {
this.clearReconnectTimer(roleId);
}
}
isGameEffectivelyPaused() {
if (this.mode === 'pvp') {
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;
}
}
} else if (this.mode === 'ai') {
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния.
}
return false;
}
getAllPlayersInfo() {
return { ...this.players };
}
getPlayerSockets() {
return { ...this.playerSockets };
}
}
module.exports = PlayerConnectionHandler;