bc/server/game/instance/PlayerConnectionHandler.js

538 lines
39 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).`);
// Делегируем 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) { // В AI режиме только 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;
}
}
// Удаление старой записи, если сокет для этой роли уже существует, но с другим ID
// Это может произойти при очень быстрой смене сокета до срабатывания disconnect старого.
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]; // Удаляем по старому socket.id
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} (identifier: ${identifier}) присоединен к комнате ${this.gameId} (addPlayer).`);
} catch (e) {
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при socket.join в addPlayer для ${identifier}: ${e.message}.`);
}
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}`);
// Находим запись игрока по роли и идентификатору, т.к. 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}`);
// Если запись по 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}. Игнорируем.`);
// Удаляем запись по устаревшему disconnectedSocketId, если она еще есть
if (this.players[disconnectedSocketId]) {
delete this.players[disconnectedSocketId];
}
return;
}
// Далее, playerEntry.socket.id === disconnectedSocketId (отключился текущий авторитетный сокет)
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;
// Обновляем сокет в playerEntry и в this.players / this.playerSockets, если сокет новый
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); // true - close an underlying connection
}
}
playerEntry.socket = newSocket;
this.players[newSocket.id] = playerEntry; // Обновляем/добавляем запись с новым socket.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;
// Всегда заново присоединяем сокет к комнате (Socket.IO handle'ит дубликаты)
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} (identifier: ${identifier}) attempting to join room ${this.gameId}.`);
try {
newSocket.join(this.gameId);
const socketsInRoomAfterJoin = Array.from(this.io.sockets.adapter.rooms.get(this.gameId) || []);
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} finished join attempt. Sockets in room ${this.gameId} NOW: [${socketsInRoomAfterJoin.join(', ')}]. Expected to include: ${newSocket.id}`);
if (!socketsInRoomAfterJoin.includes(newSocket.id)) {
console.error(`[PCH ${this.gameId}] CRITICAL: Socket ${newSocket.id} DID NOT APPEAR IN ROOM ${this.gameId} immediately after join! Client might not receive room-based events.`);
}
} catch (e) {
console.error(`[PCH ${this.gameId}] CRITICAL ERROR during newSocket.join(${this.gameId}): ${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'. Old socket ID: ${oldSocket?.id}, New socket ID: ${newSocket.id}. Player count (${this.playerCount}) not changed.`);
}
// Обновление имени
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) {
// Игрок не был 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;
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. 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.`);
tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false); // false = not paused by PCH
} 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); // true = paused by PCH
} 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. 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 { // playerEntry не найден для этой роли и идентификатора
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена. Это новый игрок?`);
// Это может быть сценарий, когда игрок впервые присоединяется к игре,
// и GameManager.handleRequestGameState вызвал этот метод.
// В этом случае, нам нужно вызвать addPlayer.
// Однако, если addPlayer уже был вызван и не нашел existingPlayerByIdentifier,
// то эта ветка означает, что что-то пошло не так с логикой GM или состоянием PCH.
// Для чистоты, handlePlayerReconnected не должен добавлять нового игрока.
// Если игрок не найден, это значит, что его сессии нет.
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;
// isAiMakingMove для TurnTimer.resume/start должно быть true, если это ход AI, И он сейчас не на паузе из-за дисконнекта игрока
const isAiTurnAndShouldAct = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
let resumedFromPausedState = false;
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
// pausedTurnState.forPlayerRoleIsPlayer - это isConfiguredForPlayerSlotTurn таймера
// pausedTurnState.isAiCurrentlyMoving - это isConfiguredForAiMove таймера
const gsTurnMatchesPausedTurnPlayerPerspective = (currentTurnIsForPlayerInGS === this.pausedTurnState.forPlayerRoleIsPlayer);
// const gsAiMatchesPausedAi = (isAiTurnAndShouldAct === this.pausedTurnState.isAiCurrentlyMoving);
// Возобновляем, если ход в GS совпадает с тем, для кого был таймер, ИЛИ если это был ход AI
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 // Был ли это AI ход, когда остановили
);
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 не использовался/неактуален). Ничего не делаем с таймером, он должен сам слать обновления.`);
}
} 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;