520 lines
35 KiB
JavaScript
520 lines
35 KiB
JavaScript
// /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; |