bc/server/game/instance/PlayerConnectionHandler.js

520 lines
35 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}). Обрабатывается как возможное переподключение (вызов 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 disallowedKeyForOpponent = firstPlayerInfo.chosenCharacterKey === 'balard' ? null : 'balard';
const otherKey = allKeys.find(k => k !== firstPlayerInfo.chosenCharacterKey && k !== disallowedKeyForOpponent);
actualCharacterKey = otherKey || (actualCharacterKey === 'elena' ? 'almagest' : 'elena');
}
} else {
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
return false;
}
}
const oldPlayerSocketEntryForRole = Object.entries(this.players).find(([sid, pInfo]) => pInfo.id === assignedPlayerId);
if (oldPlayerSocketEntryForRole) {
const [oldSocketId, oldPlayerInfo] = oldPlayerSocketEntryForRole;
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}] Ошибка при дисконнекте старого сокета в addPlayer: ${e.message}`); }
delete this.players[oldSocketId];
if (this.playerSockets[assignedPlayerId] === oldPlayerInfo.socket) {
delete this.playerSockets[assignedPlayerId];
}
}
}
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++;
console.log(`[PCH ${this.gameId}] Socket ${socket.id} (identifier: ${identifier}) ATTEMPTING to join room ${this.gameId} in addPlayer.`);
try {
socket.join(this.gameId);
const roomData = this.io.sockets.adapter.rooms.get(this.gameId);
const socketsInRoomAfterJoin = roomData ? Array.from(roomData) : [];
const isSocketInRoom = socketsInRoomAfterJoin.includes(socket.id);
console.log(`[PCH ${this.gameId}] Socket ${socket.id} FINISHED join attempt (addPlayer). Room data type: ${typeof roomData}. Sockets in room ${this.gameId} NOW: [${socketsInRoomAfterJoin.join(', ')}]. Is newSocket (${socket.id}) in room? ${isSocketInRoom}`);
if (!isSocketInRoom) {
console.error(`[PCH ${this.gameId}] CRITICAL FAILURE (addPlayer): Socket ${socket.id} was NOT found in room ${this.gameId} immediately after join command!`);
}
} catch (e) {
console.error(`[PCH ${this.gameId}] CRITICAL ERROR during socket.join in addPlayer for ${identifier}: ${e.message}. Stack: ${e.stack}`);
}
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 {
console.log(`[PCH ${this.gameId}] Попытка socket.leave(${this.gameId}) для ${socketId} в removePlayer.`);
playerInfo.socket.leave(this.gameId);
} catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave в removePlayer для ${socketId}: ${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}] Запись игрока по роли/id (${identifier}/${playerIdRole}) не найдена, или у нее нет сокета. disconnectedSocketId: ${disconnectedSocketId}`);
if (this.players[disconnectedSocketId]) {
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId} (без playerEntry по роли/id), удаляем ее через removePlayer.`);
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_active_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) {
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) {
if (timerInfo.updateIntervalId) {
clearInterval(timerInfo.updateIntervalId);
timerInfo.updateIntervalId = null;
}
timerInfo.timerId = null;
}
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} (${playerIdRole}). Удаляем игрока.`);
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
} else {
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier} (${playerIdRole}), но игрок уже не (или не был) 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 for role ${playerIdRole}, id ${identifier}:`, playerEntry ? {socketId: playerEntry.socket?.id, isTempDisc: playerEntry.isTemporarilyDisconnected} : 'Not Found');
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;
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
delete this.players[oldSocket.id];
}
this.playerSockets[playerIdRole] = newSocket;
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} (identifier: ${identifier}) ATTEMPTING to join room ${this.gameId} in handlePlayerReconnected.`);
try {
newSocket.join(this.gameId);
const roomData = this.io.sockets.adapter.rooms.get(this.gameId);
const socketsInRoomAfterJoin = roomData ? Array.from(roomData) : [];
const isSocketInRoom = socketsInRoomAfterJoin.includes(newSocket.id);
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} FINISHED join attempt (handlePlayerReconnected). Room data type: ${typeof roomData}. Sockets in room ${this.gameId} NOW: [${socketsInRoomAfterJoin.join(', ')}]. Is newSocket (${newSocket.id}) in room? ${isSocketInRoom}`);
if (!isSocketInRoom) {
console.error(`[PCH ${this.gameId}] CRITICAL FAILURE (handlePlayerReconnected): Socket ${newSocket.id} was NOT found in room ${this.gameId} immediately after join command!`);
}
} catch (e) {
console.error(`[PCH ${this.gameId}] CRITICAL ERROR during newSocket.join(${this.gameId}) in handlePlayerReconnected: ${e.message}. Stack: ${e.stack}`);
}
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'. Old socket ID: ${oldSocket?.id}, New socket ID: ${newSocket.id}. Player count (${this.playerCount}) не изменен.`);
}
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} (${playerIdRole}) обновлено/установлено на: ${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) {
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;
if (tt.isCurrentlyRunning && !tt.isManuallyPausedState) {
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
console.log(`[PCH ${this.gameId}] Forcing onTickCallback (not temp disc). Remaining: ${currentRemaining}, ForPlayer: ${tt.isConfiguredForPlayerSlotTurn}, ManualPause: ${tt.isManuallyPausedState}`);
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
} else if (tt.isConfiguredForAiMove && !tt.isCurrentlyRunning) {
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for AI move state (not temp disc).`);
tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false);
} else if (tt.isManuallyPausedState) {
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for manually paused state (not temp disc). 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 && this.mode === 'pvp');
const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn;
if(isHisTurnNow || isAiTurnNow) {
console.log(`[PCH ${this.gameId}] Timer not running & not paused (not temp disc). 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 не найдена. GameManager должен обработать это как нового игрока, если необходимо.`);
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 isAiTurnAndShouldAct = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
let resumedFromPausedState = false;
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
const gsTurnMatchesPausedTurnPlayerPerspective = (currentTurnIsForPlayerInGS === this.pausedTurnState.forPlayerRoleIsPlayer);
if (gsTurnMatchesPausedTurnPlayerPerspective) {
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 ход для таймера: ${isAiTurnAndShouldAct}`);
this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isAiTurnAndShouldAct);
if (isAiTurnAndShouldAct && !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 не использовался/неактуален). Отправляем текущее состояние таймера.`);
// Если таймер уже активен, но мы здесь, значит, это мог быть реконнект НЕ isTemporarilyDisconnected.
// "Пнем" таймер, чтобы он отправил свое состояние.
const tt = this.gameInstance.turnTimer;
if (tt.onTickCallback && tt.isCurrentlyRunning && !tt.isManuallyPausedState) {
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, false);
}
}
} 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;