bc/server/game/instance/PlayerConnectionHandler.js

521 lines
36 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;
}
// Делегируем handlePlayerReconnected, который разберется, новый ли это сокет или тот же.
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
}
// Проверка на максимальное количество игроков
if (this.mode === 'pvp' && this.playerCount >= 2) {
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' });
return false;
}
if (this.mode === 'ai' && this.playerCount >= 1) {
socket.emit('gameError', { message: 'К AI игре может присоединиться только один игрок.'});
return false;
}
let assignedPlayerId;
let actualCharacterKey = chosenCharacterKey || 'elena';
const charDataForName = dataUtils.getCharacterData(actualCharacterKey); // Для имени
if (this.mode === 'ai') {
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) {
const allKeys = dataUtils.getAllCharacterKeys ? dataUtils.getAllCharacterKeys() : ['elena', 'almagest', 'balard'];
const otherKey = allKeys.find(k => k !== firstPlayerInfo.chosenCharacterKey && k !== 'balard'); // Не даем Баларда второму игроку по умолчанию
actualCharacterKey = otherKey || (actualCharacterKey === 'elena' ? 'almagest' : 'elena'); // Фоллбэк
}
} else {
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
return false;
}
}
// Удаление старой записи, если сокет для этой роли уже существует, но с другим ID
// (на случай очень быстрой смены сокета до срабатывания disconnect)
const oldPlayerSocketEntry = Object.entries(this.players).find(([sid, pInfo]) => pInfo.id === assignedPlayerId);
if (oldPlayerSocketEntry) {
const [oldSocketId, oldPlayerInfo] = oldPlayerSocketEntry;
if (oldPlayerInfo.socket && oldPlayerInfo.socket.id !== socket.id) {
console.warn(`[PCH ${this.gameId}] addPlayer: Найдена старая запись для роли ${assignedPlayerId} с сокетом ${oldPlayerInfo.socket.id}. Новый сокет: ${socket.id}. Удаляем старую запись.`);
try {
if (oldPlayerInfo.socket.connected) oldPlayerInfo.socket.disconnect(true);
} catch (e) { console.error(`[PCH ${this.gameId}] Ошибка при дисконнекте старого сокета: ${e.message}`); }
delete this.players[oldSocketId];
if (this.playerSockets[assignedPlayerId] === oldPlayerInfo.socket) {
delete this.playerSockets[assignedPlayerId];
}
// Не уменьшаем playerCount здесь, так как это замена, а не уход
}
}
this.players[socket.id] = {
id: assignedPlayerId,
socket: socket,
chosenCharacterKey: actualCharacterKey,
identifier: identifier,
isTemporarilyDisconnected: false,
name: charDataForName?.baseStats?.name || actualCharacterKey
};
this.playerSockets[assignedPlayerId] = socket;
this.playerCount++; // Увеличиваем счетчик активных игроков
try {
socket.join(this.gameId);
console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`);
} catch (e) {
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при socket.join: ${e.message}. Игрок ${identifier} может не получать широковещательные сообщения.`);
// Возможно, стоит откатить добавление игрока или вернуть false
}
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 в removePlayer: ${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}`);
if (this.players[disconnectedSocketId]) {
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId} (без playerEntry по роли/id), удаляем ее.`);
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_main_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];
}
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.gameInstance.turnTimer.getIsConfiguredForAiMove?.())) {
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 timerInfo = this.reconnectTimers[playerIdRole];
if (!timerInfo || timerInfo.timerId === null) { // Если основной таймер уже сработал/очищен
if (timerInfo?.updateIntervalId) clearInterval(timerInfo.updateIntervalId);
if (timerInfo) timerInfo.updateIntervalId = null;
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
return;
}
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
if (remaining <= 0) {
// Даем основному setTimeout сработать, здесь просто останавливаем интервал тиков
clearInterval(timerInfo.updateIntervalId);
timerInfo.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(() => {
const timerInfo = this.reconnectTimers[playerIdRole];
if (timerInfo?.updateIntervalId) {
clearInterval(timerInfo.updateIntervalId);
timerInfo.updateIntervalId = null;
}
if (timerInfo) timerInfo.timerId = null; // Помечаем, что сработал
// this.clearReconnectTimer(playerIdRole) здесь вызовет сам себя рекурсивно, если удалить delete this.reconnectTimers[playerIdRole];
// Поэтому просто удаляем запись, т.к. таймеры уже очищены или помечены.
if (this.reconnectTimers[playerIdRole]) delete this.reconnectTimers[playerIdRole];
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}. Удаляем игрока.`);
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
} else {
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}, но игрок уже не (или не был) isTemporarilyDisconnected.`);
}
}, 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;
const wasTemporarilyDisconnected = playerEntry.isTemporarilyDisconnected;
if (oldSocket && oldSocket.id !== newSocket.id) {
console.log(`[PCH ${this.gameId}] Новый сокет ${newSocket.id} для игрока ${identifier}. Старый сокет: ${oldSocket.id}. Обновляем записи.`);
if (this.players[oldSocket.id]) delete this.players[oldSocket.id];
if (oldSocket.connected) {
console.log(`[PCH ${this.gameId}] Отключаем старый "подвисший" сокет ${oldSocket.id}.`);
oldSocket.disconnect(true);
}
}
playerEntry.socket = newSocket;
this.players[newSocket.id] = playerEntry; // Обновляем/добавляем запись с новым socket.id
// Если старый ID был ключом для playerEntry, и он не равен newSocket.id, удаляем старый ключ
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
delete this.players[oldSocket.id];
}
this.playerSockets[playerIdRole] = newSocket;
try {
newSocket.join(this.gameId);
console.log(`[PCH ${this.gameId}] Сокет ${newSocket.id} (identifier: ${identifier}) присоединен/переприсоединен к комнате ${this.gameId} (handlePlayerReconnected).`);
} catch (e) {
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при newSocket.join в handlePlayerReconnected: ${e.message}.`);
}
if (wasTemporarilyDisconnected) {
console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`);
this.clearReconnectTimer(playerIdRole);
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null });
playerEntry.isTemporarilyDisconnected = false;
this.playerCount++;
} else {
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Старый сокет ID: ${oldSocket?.id}, Новый сокет ID: ${newSocket.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 (wasTemporarilyDisconnected && this.pausedTurnState) {
this.resumeGameLogicAfterReconnect(playerIdRole);
} else if (!wasTemporarilyDisconnected) {
// Игрок не был temp disconnected. Таймер на сервере, если шел, то продолжал идти.
// Клиент получил новое состояние. Нужно, чтобы он начал получать обновления таймера.
// Принудительный join выше должен был помочь.
// Дополнительно заставим таймер отправить текущее состояние.
console.log(`[PCH ${this.gameId}] Player was not temp disconnected. Forcing timer update if active (for socket ${newSocket.id}).`);
if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.onTickCallback) {
const tt = this.gameInstance.turnTimer;
// Если таймер реально работает (не ход AI и не на ручной паузе от другого игрока)
if (tt.isCurrentlyRunning && !tt.isManuallyPausedState && !tt.isConfiguredForAiMove) {
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
console.log(`[PCH ${this.gameId}] Forcing onTickCallback. Remaining: ${currentRemaining}, ForPlayer: ${tt.isConfiguredForPlayerSlotTurn}, ManualPause: ${tt.isManuallyPausedState}`);
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
} else if (tt.isConfiguredForAiMove && !tt.isCurrentlyRunning) { // Если ход AI
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for AI move state.`);
tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false);
} else if (tt.isManuallyPausedState) { // Если на ручной паузе (из-за другого игрока)
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for manually paused state. Remaining: ${tt.segmentDurationMs}`);
tt.onTickCallback(tt.segmentDurationMs, tt.isConfiguredForPlayerSlotTurn, true);
} else if (!tt.isCurrentlyRunning && !tt.isManuallyPausedState && !this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
// Таймер не работает, не на паузе, игра не на паузе - возможно, его нужно запустить
const gs = this.gameInstance.gameState;
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, attempting to start for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`);
this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow);
if (isAiTurnNow && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) {
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 {
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена.`);
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()) {
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;
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;
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 = 'Балард';
else this.gameInstance.gameState[oppRoleKey].name = (this.mode === 'pvp' ? 'Ожидание Оппонента...' : 'Противник');
}
}
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', {
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(); // Обновляем состояние для всех
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
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,
this.pausedTurnState.isAiCurrentlyMoving
);
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.getIsConfiguredForAiMove?.()) {
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]) {
if (this.reconnectTimers[playerIdRole].timerId) {
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;