Разделение gameInstance. Добавление PlayerConnectionHandler
This commit is contained in:
parent
ec459f65d1
commit
daccc60689
@ -1,6 +1,6 @@
|
||||
// /server/game/GameManager.js
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const GameInstance = require('./instance/GameInstance');
|
||||
const GameInstance = require('./instance/GameInstance'); // Путь к GameInstance с геттерами
|
||||
const dataUtils = require('../data/dataUtils');
|
||||
const GAME_CONFIG = require('../core/config');
|
||||
|
||||
@ -19,9 +19,9 @@ class GameManager {
|
||||
|
||||
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||||
const gameToRemove = this.games[oldPendingGameId];
|
||||
// Используем game.playerCount (или аналогичный метод GameInstance, если он инкапсулирует это)
|
||||
// Используем gameToRemove.playerCount (через геттер)
|
||||
if (gameToRemove.mode === 'pvp' &&
|
||||
gameToRemove.playerCount === 1 && // Предполагаем, GameInstance.playerCount - это активные игроки
|
||||
gameToRemove.playerCount === 1 &&
|
||||
gameToRemove.ownerIdentifier === identifier &&
|
||||
this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} creating/joining new. Removing previous pending PvP game: ${oldPendingGameId}`);
|
||||
@ -36,10 +36,11 @@ class GameManager {
|
||||
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||
if (existingGameId && this.games[existingGameId]) {
|
||||
const existingGame = this.games[existingGameId];
|
||||
// Используем game.playerCount
|
||||
// Используем existingGame.playerCount (через геттер)
|
||||
console.warn(`[GameManager.createGame] User ${identifier} already in game ${existingGameId}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
|
||||
|
||||
if (existingGame.gameState && !existingGame.gameState.isGameOver) {
|
||||
// Используем existingGame.playerCount (через геттер)
|
||||
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
||||
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
||||
} else {
|
||||
@ -60,10 +61,9 @@ class GameManager {
|
||||
|
||||
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
||||
|
||||
// addPlayer в GameInstance теперь bool, а не объект с результатом
|
||||
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameId;
|
||||
// Получаем роль и актуальный ключ из GameInstance после добавления
|
||||
// Получаем роль и актуальный ключ из GameInstance через геттер game.players
|
||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
const assignedPlayerId = playerInfo?.id;
|
||||
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
||||
@ -90,7 +90,7 @@ class GameManager {
|
||||
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
||||
}
|
||||
} else if (mode === 'pvp') {
|
||||
game.initializeGame(); // Инициализирует первого игрока
|
||||
game.initializeGame();
|
||||
if (!this.pendingPvPGames.includes(gameId)) {
|
||||
this.pendingPvPGames.push(gameId);
|
||||
}
|
||||
@ -111,7 +111,9 @@ class GameManager {
|
||||
if (game.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_${identifier}`); return; }
|
||||
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
|
||||
|
||||
// Используем геттер game.players
|
||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
// Используем game.playerCount (через геттер)
|
||||
if (game.playerCount >= 2 && !playerInfoInGame?.isTemporarilyDisconnected) {
|
||||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||||
}
|
||||
@ -130,12 +132,11 @@ class GameManager {
|
||||
const charKeyForJoin = chosenCharacterKey || 'elena';
|
||||
if (game.addPlayer(socket, charKeyForJoin, identifier)) {
|
||||
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||||
const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier); // Получаем инфо после добавления
|
||||
// Используем геттер game.players
|
||||
const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
||||
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}. Cleaning up.`);
|
||||
// Не вызываем _cleanupGame здесь, т.к. игра могла быть уже с одним игроком.
|
||||
// GameInstance.addPlayer должен был бы вернуть false и не изменить playerCount.
|
||||
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
||||
return;
|
||||
}
|
||||
@ -147,7 +148,8 @@ class GameManager {
|
||||
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
|
||||
});
|
||||
|
||||
if (game.playerCount === 2) { // Используем game.playerCount из GameInstance
|
||||
// Используем game.playerCount (через геттер)
|
||||
if (game.playerCount === 2) {
|
||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
|
||||
if (game.initializeGame()) {
|
||||
game.startGame();
|
||||
@ -162,7 +164,6 @@ class GameManager {
|
||||
}
|
||||
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||||
// ... (Логика findAndJoinRandomPvPGame без изменений, использует game.playerCount)
|
||||
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||||
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||
if (existingGameId && this.games[existingGameId]) {
|
||||
@ -179,6 +180,7 @@ class GameManager {
|
||||
let gameIdToJoin = null;
|
||||
for (const id of [...this.pendingPvPGames]) {
|
||||
const pendingGame = this.games[id];
|
||||
// Используем pendingGame.playerCount (через геттер)
|
||||
if (pendingGame && pendingGame.mode === 'pvp' &&
|
||||
pendingGame.playerCount === 1 &&
|
||||
pendingGame.ownerIdentifier !== identifier &&
|
||||
@ -205,11 +207,12 @@ class GameManager {
|
||||
const game = this.games[gameId];
|
||||
if (game) {
|
||||
if (game.gameState?.isGameOver) {
|
||||
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; // Находим сокет по identifier
|
||||
// Используем геттер game.players
|
||||
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
|
||||
if (playerSocket) this.handleRequestGameState(playerSocket, identifier);
|
||||
return;
|
||||
}
|
||||
game.processPlayerAction(identifier, actionData); // Передаем identifier
|
||||
game.processPlayerAction(identifier, actionData);
|
||||
} else {
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||
@ -218,7 +221,6 @@ class GameManager {
|
||||
}
|
||||
|
||||
handlePlayerSurrender(identifier) {
|
||||
// ... (Логика handlePlayerSurrender без изменений)
|
||||
const gameId = this.userIdentifierToGameId[identifier];
|
||||
console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`);
|
||||
const game = this.games[gameId];
|
||||
@ -260,7 +262,6 @@ class GameManager {
|
||||
}
|
||||
|
||||
_findClientSocketByIdentifier(identifier) {
|
||||
// ... (код без изменений)
|
||||
for (const sid of this.io.sockets.sockets.keys()) {
|
||||
const s = this.io.sockets.sockets.get(sid);
|
||||
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
||||
@ -278,13 +279,11 @@ class GameManager {
|
||||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Находим информацию об игроке в инстансе игры по identifier
|
||||
// Используем геттер game.players
|
||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
|
||||
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
||||
// Передаем роль, идентификатор и ключ персонажа в GameInstance
|
||||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||||
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey);
|
||||
} else {
|
||||
@ -296,7 +295,7 @@ class GameManager {
|
||||
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
|
||||
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already temp disconnected. Reconnect timer handles final cleanup.`);
|
||||
} else if (!playerInfoInGame) {
|
||||
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. Clearing map.`);
|
||||
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game's player list. Clearing map.`);
|
||||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
} else {
|
||||
@ -307,7 +306,6 @@ class GameManager {
|
||||
}
|
||||
|
||||
_cleanupGame(gameId, reason = 'unknown') {
|
||||
// ... (Код _cleanupGame почти без изменений, но использует game.playerCount и game.ownerIdentifier)
|
||||
console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`);
|
||||
const game = this.games[gameId];
|
||||
|
||||
@ -317,24 +315,26 @@ class GameManager {
|
||||
for (const idKey in this.userIdentifierToGameId) { if (this.userIdentifierToGameId[idKey] === gameId) delete this.userIdentifierToGameId[idKey]; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Используем game.playerCount (через геттер)
|
||||
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`);
|
||||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||||
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); // Вызываем у GameInstance
|
||||
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
|
||||
|
||||
if (game.gameState && !game.gameState.isGameOver) {
|
||||
game.gameState.isGameOver = true;
|
||||
}
|
||||
|
||||
let playersCleanedFromMap = 0;
|
||||
Object.values(game.players).forEach(pInfo => { // Игроки теперь в game.players
|
||||
// Используем геттер game.players
|
||||
Object.values(game.players).forEach(pInfo => {
|
||||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||||
delete this.userIdentifierToGameId[pInfo.identifier];
|
||||
playersCleanedFromMap++;
|
||||
}
|
||||
});
|
||||
// Используем геттер game.players
|
||||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId &&
|
||||
!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Проверка, если владелец не в списке игроков
|
||||
!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) {
|
||||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||||
playersCleanedFromMap++;
|
||||
}
|
||||
@ -349,12 +349,12 @@ class GameManager {
|
||||
}
|
||||
|
||||
getAvailablePvPGamesListForClient() {
|
||||
// ... (Код без изменений, использует game.playerCount и game.ownerIdentifier)
|
||||
return this.pendingPvPGames
|
||||
.map(gameId => {
|
||||
const game = this.games[gameId];
|
||||
// Используем game.playerCount (через геттер)
|
||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||
// Находим первого игрока (владельца) в инстансе игры
|
||||
// Используем геттер game.players
|
||||
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||
let p1Username = 'Игрок';
|
||||
let p1CharName = 'Неизвестный';
|
||||
@ -364,10 +364,10 @@ class GameManager {
|
||||
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
||||
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
|
||||
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
|
||||
} else if (ownerId){ // Фоллбэк на поиск по ownerId, если p1Entry не найден или без userData
|
||||
} else if (ownerId){
|
||||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||||
const ownerCharKey = game.playerCharacterKey; // Ключ персонажа первого игрока из GameInstance
|
||||
const ownerCharKey = game.playerCharacterKey;
|
||||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||||
}
|
||||
@ -389,7 +389,8 @@ class GameManager {
|
||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||
|
||||
if (game) {
|
||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); // Ищем по identifier
|
||||
// Используем геттер game.players
|
||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
|
||||
|
||||
if (playerInfoInGame) {
|
||||
@ -398,7 +399,6 @@ class GameManager {
|
||||
if(this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||
return;
|
||||
}
|
||||
// Передаем РОЛЬ и НОВЫЙ СОКЕТ в GameInstance для обработки реконнекта
|
||||
if (typeof game.handlePlayerReconnected === 'function') {
|
||||
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
||||
// ... (обработка результата reconnected, если нужно)
|
||||
@ -416,21 +416,18 @@ class GameManager {
|
||||
}
|
||||
|
||||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||||
// ... (код без изменений)
|
||||
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
||||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||
if (gameId && this.games[gameId]) { // Проверяем, что игра еще существует перед очисткой
|
||||
if (gameId && this.games[gameId]) {
|
||||
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
||||
} else if (this.userIdentifierToGameId[identifier]) {
|
||||
// Если игра уже удалена, но пользователь все еще к ней привязан
|
||||
const problematicGameId = this.userIdentifierToGameId[identifier];
|
||||
if (this.games[problematicGameId]) { // Если она все же есть
|
||||
if (this.games[problematicGameId]) {
|
||||
this._cleanupGame(problematicGameId, `recovery_error_stale_map_${identifier}_reason_${reasonCode}`);
|
||||
} else { // Если ее нет, просто чистим карту
|
||||
} else {
|
||||
delete this.userIdentifierToGameId[identifier];
|
||||
}
|
||||
}
|
||||
// Если после _cleanupGame пользователь все еще привязан (маловероятно, но для гарантии)
|
||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const TurnTimer = require('./TurnTimer');
|
||||
const gameLogic = require('../logic');
|
||||
const dataUtils = require('../../data/dataUtils');
|
||||
const GAME_CONFIG = require('../../core/config');
|
||||
const PlayerConnectionHandler = require('./PlayerConnectionHandler');
|
||||
|
||||
class GameInstance {
|
||||
constructor(gameId, io, mode = 'ai', gameManager) {
|
||||
@ -12,9 +13,7 @@ class GameInstance {
|
||||
this.mode = mode;
|
||||
this.gameManager = gameManager;
|
||||
|
||||
this.players = {};
|
||||
this.playerSockets = {};
|
||||
this.playerCount = 0;
|
||||
this.playerConnectionHandler = new PlayerConnectionHandler(this);
|
||||
|
||||
this.gameState = null;
|
||||
this.aiOpponent = (mode === 'ai');
|
||||
@ -24,9 +23,6 @@ class GameInstance {
|
||||
this.opponentCharacterKey = null;
|
||||
this.ownerIdentifier = null;
|
||||
|
||||
this.reconnectTimers = {};
|
||||
this.pausedTurnState = null;
|
||||
|
||||
this.turnTimer = new TurnTimer(
|
||||
GAME_CONFIG.TURN_DURATION_MS,
|
||||
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
||||
@ -35,7 +31,7 @@ class GameInstance {
|
||||
this.io.to(this.id).emit('turnTimerUpdate', {
|
||||
remainingTime,
|
||||
isPlayerTurn: isPlayerTurnForTimer,
|
||||
isPaused: isPaused
|
||||
isPaused: isPaused || this.isGameEffectivelyPaused()
|
||||
});
|
||||
},
|
||||
this.id
|
||||
@ -44,23 +40,73 @@ class GameInstance {
|
||||
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
||||
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: GameManager reference invalid.`);
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}.`);
|
||||
console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}. PlayerConnectionHandler also initialized.`);
|
||||
}
|
||||
|
||||
// --- Геттеры для GameManager и внутреннего использования ---
|
||||
get playerCount() {
|
||||
return this.playerConnectionHandler.playerCount;
|
||||
}
|
||||
|
||||
// Этот геттер может быть полезен, если GameManager или другая часть GameInstance
|
||||
// захочет получить доступ ко всем данным игроков, не зная о PCH.
|
||||
get players() {
|
||||
return this.playerConnectionHandler.getAllPlayersInfo();
|
||||
}
|
||||
|
||||
// --- Сеттеры для PCH ---
|
||||
setPlayerCharacterKey(key) { this.playerCharacterKey = key; }
|
||||
setOpponentCharacterKey(key) { this.opponentCharacterKey = key; }
|
||||
setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; }
|
||||
|
||||
// --- Методы, делегирующие PCH ---
|
||||
addPlayer(socket, chosenCharacterKey, identifier) {
|
||||
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
|
||||
}
|
||||
|
||||
removePlayer(socketId, reason) {
|
||||
this.playerConnectionHandler.removePlayer(socketId, reason);
|
||||
}
|
||||
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||
this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey);
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||
return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket);
|
||||
}
|
||||
|
||||
clearAllReconnectTimers() {
|
||||
this.playerConnectionHandler.clearAllReconnectTimers();
|
||||
}
|
||||
|
||||
isGameEffectivelyPaused() {
|
||||
return this.playerConnectionHandler.isGameEffectivelyPaused();
|
||||
}
|
||||
|
||||
handlePlayerPermanentlyLeft(playerRole, characterKey, reason) {
|
||||
console.log(`[GameInstance ${this.id}] Player permanently left. Role: ${playerRole}, Reason: ${reason}`);
|
||||
if (this.gameState && !this.gameState.isGameOver) {
|
||||
// Используем геттер playerCount
|
||||
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
|
||||
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
|
||||
} else if (this.mode === 'pvp') {
|
||||
if (this.playerCount < 2) {
|
||||
// Используем геттер players для поиска оставшегося
|
||||
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
|
||||
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
|
||||
}
|
||||
}
|
||||
} else if (!this.gameState && Object.keys(this.players).length === 0) { // Используем геттер players
|
||||
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch");
|
||||
}
|
||||
}
|
||||
|
||||
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
|
||||
if (!characterState || !characterState.characterKey) {
|
||||
return;
|
||||
}
|
||||
if (!opponentCharacterKey) {
|
||||
return;
|
||||
}
|
||||
if (!gameLogic.getRandomTaunt) {
|
||||
console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`);
|
||||
return;
|
||||
}
|
||||
if (!this.gameState) {
|
||||
return;
|
||||
}
|
||||
if (!characterState || !characterState.characterKey) return;
|
||||
if (!opponentCharacterKey) return;
|
||||
if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`); return; }
|
||||
if (!this.gameState) return;
|
||||
|
||||
let context = {};
|
||||
let subTrigger = null;
|
||||
@ -83,9 +129,7 @@ class GameInstance {
|
||||
}
|
||||
|
||||
const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey);
|
||||
if (!opponentFullData) {
|
||||
return;
|
||||
}
|
||||
if (!opponentFullData) return;
|
||||
|
||||
const tauntText = gameLogic.getRandomTaunt(
|
||||
characterState.characterKey,
|
||||
@ -101,320 +145,25 @@ class GameInstance {
|
||||
}
|
||||
}
|
||||
|
||||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||||
// ... (Код addPlayer без изменений из предыдущего вашего файла) ...
|
||||
console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
|
||||
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
|
||||
if (existingPlayerByIdentifier) {
|
||||
console.warn(`[GameInstance ${this.id}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`);
|
||||
if (this.gameState && this.gameState.isGameOver) {
|
||||
console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
|
||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||
this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||
}
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
|
||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
let assignedPlayerId;
|
||||
let actualCharacterKey = chosenCharacterKey || 'elena';
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
return false;
|
||||
}
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
} else {
|
||||
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
} else if (!this.playerSockets[GAME_CONFIG.OPPONENT_ID]) {
|
||||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
|
||||
if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
||||
else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id);
|
||||
if (oldPlayerSocketIdForRole) {
|
||||
const oldPlayerInfo = this.players[oldPlayerSocketIdForRole];
|
||||
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.id); } catch(e){} }
|
||||
delete this.players[oldPlayerSocketIdForRole];
|
||||
}
|
||||
|
||||
this.players[socket.id] = {
|
||||
id: assignedPlayerId,
|
||||
socket: socket,
|
||||
chosenCharacterKey: actualCharacterKey,
|
||||
identifier: identifier,
|
||||
isTemporarilyDisconnected: false
|
||||
};
|
||||
this.playerSockets[assignedPlayerId] = socket;
|
||||
this.playerCount++;
|
||||
socket.join(this.id);
|
||||
|
||||
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.playerCharacterKey = actualCharacterKey;
|
||||
else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.opponentCharacterKey = actualCharacterKey;
|
||||
|
||||
if (!this.ownerIdentifier && (this.mode === 'ai' || (this.mode === 'pvp' && assignedPlayerId === GAME_CONFIG.PLAYER_ID))) {
|
||||
this.ownerIdentifier = identifier;
|
||||
}
|
||||
|
||||
const charData = dataUtils.getCharacterData(actualCharacterKey);
|
||||
console.log(`[GameInstance ${this.id}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${this.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(`[GameInstance ${this.id}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`);
|
||||
|
||||
if (playerInfo.socket) {
|
||||
try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (!playerInfo.isTemporarilyDisconnected) {
|
||||
this.playerCount--;
|
||||
}
|
||||
|
||||
delete this.players[socketId];
|
||||
if (this.playerSockets[playerRole]?.id === socketId) {
|
||||
delete this.playerSockets[playerRole];
|
||||
}
|
||||
this.clearReconnectTimer(playerRole);
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`);
|
||||
|
||||
if (this.gameState && !this.gameState.isGameOver) {
|
||||
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
|
||||
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_ai_game");
|
||||
} else if (this.mode === 'pvp') {
|
||||
const remainingActivePlayer = Object.values(this.players).find(p => p.socket && p.socket.connected && !p.isTemporarilyDisconnected);
|
||||
if (this.playerCount < 2) {
|
||||
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", remainingActivePlayer?.id);
|
||||
}
|
||||
}
|
||||
} else if (!this.gameState && Object.keys(this.players).length === 0) {
|
||||
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi");
|
||||
}
|
||||
}
|
||||
}
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { /* ... Код без изменений, вызывает turnTimer.pause() ... */
|
||||
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`);
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
if (!playerEntry || !playerEntry.socket) { return; }
|
||||
if (this.gameState && this.gameState.isGameOver) { return; }
|
||||
if (playerEntry.isTemporarilyDisconnected) { return; }
|
||||
|
||||
playerEntry.isTemporarilyDisconnected = true;
|
||||
this.playerCount--;
|
||||
console.log(`[GameInstance ${this.id}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`);
|
||||
|
||||
const disconnectedName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
|
||||
this.addToLog(`🔌 Игрок ${disconnectedName} отключился. Ожидание переподключения...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.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.turnTimer.isActive() || (this.mode === 'ai' && this.turnTimer.isAiCurrentlyMakingMove) ) {
|
||||
this.pausedTurnState = this.turnTimer.pause();
|
||||
console.log(`[GameInstance ${this.id}] Turn timer paused due to disconnect. State:`, JSON.stringify(this.pausedTurnState));
|
||||
} else {
|
||||
this.pausedTurnState = null;
|
||||
}
|
||||
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
|
||||
const reconnectStartTime = Date.now();
|
||||
|
||||
const updateInterval = setInterval(() => {
|
||||
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
|
||||
if (remaining <= 0) {
|
||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
|
||||
return;
|
||||
}
|
||||
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: Math.ceil(remaining) });
|
||||
}, 1000);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
|
||||
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
|
||||
}
|
||||
}, reconnectDuration);
|
||||
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) { /* ... Код без изменений, вызывает turnTimer.resume() или start() ... */
|
||||
const identifier = newSocket.userData?.userId;
|
||||
console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
|
||||
|
||||
if (this.gameState && this.gameState.isGameOver) {
|
||||
newSocket.emit('gameError', { message: 'Игра уже завершена.' });
|
||||
this.gameManager._cleanupGame(this.id, `reconnect_to_finished_game_gi_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null });
|
||||
|
||||
const oldSocketId = playerEntry.socket.id;
|
||||
if (this.players[oldSocketId] && oldSocketId !== newSocket.id) delete this.players[oldSocketId];
|
||||
|
||||
playerEntry.socket = newSocket;
|
||||
playerEntry.isTemporarilyDisconnected = false;
|
||||
this.players[newSocket.id] = playerEntry;
|
||||
this.playerSockets[playerIdRole] = newSocket;
|
||||
this.playerCount++;
|
||||
|
||||
newSocket.join(this.id);
|
||||
const reconnectedName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey;
|
||||
console.log(`[GameInstance ${this.id}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`);
|
||||
this.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
|
||||
if (!this.gameState) {
|
||||
if (!this.initializeGame()) { this._handleCriticalError('reconnect_no_gs_after_init_v2', 'GS null after re-init.'); return false; }
|
||||
}
|
||||
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
|
||||
playerBaseStats: pData?.baseStats,
|
||||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities || [],
|
||||
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
|
||||
const otherSocket = this.playerSockets[oppRoleKey];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey);
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
otherSocket.emit('playerReconnected', { reconnectedPlayerId: playerIdRole, reconnectedPlayerName: reconnectedName });
|
||||
if (this.logBuffer.length > 0) otherSocket.emit('logUpdate', { log: this.consumeLogBuffer() });
|
||||
}
|
||||
|
||||
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) {
|
||||
this.broadcastGameStateUpdate();
|
||||
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
|
||||
this.turnTimer.resume(
|
||||
this.pausedTurnState.remainingTime,
|
||||
this.pausedTurnState.forPlayerRoleIsPlayer,
|
||||
this.pausedTurnState.isAiCurrentlyMoving
|
||||
);
|
||||
this.pausedTurnState = null;
|
||||
} else {
|
||||
const currentTurnIsForPlayer = this.gameState.isPlayerTurn;
|
||||
const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer;
|
||||
this.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
|
||||
if (playerEntry.socket.id !== newSocket.id) {
|
||||
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); return false;
|
||||
}
|
||||
if (!this.gameState) { if (!this.initializeGame()) {this._handleCriticalError('reconnect_same_socket_no_gs','GS null on same socket'); return false;} }
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
|
||||
playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats,
|
||||
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities,
|
||||
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена).' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
clearReconnectTimer(playerIdRole) { /* ... Код без изменений ... */
|
||||
if (this.reconnectTimers[playerIdRole]) {
|
||||
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
|
||||
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
|
||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
}
|
||||
delete this.reconnectTimers[playerIdRole];
|
||||
}
|
||||
}
|
||||
clearAllReconnectTimers() { /* ... Код без изменений ... */
|
||||
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;
|
||||
}
|
||||
|
||||
initializeGame() {
|
||||
// Используем геттеры
|
||||
console.log(`[GameInstance ${this.id}] Initializing game state. Mode: ${this.mode}. Active players: ${this.playerCount}. Total entries: ${Object.keys(this.players).length}`);
|
||||
|
||||
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_v3', 'AI game init: Human player not found or not active.'); return false; }
|
||||
this.playerCharacterKey = p1Entry.chosenCharacterKey;
|
||||
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_gi_v4', 'AI game init: Human player not found or not active.'); return false; }
|
||||
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'AI game init: Player character key not set.'); return false;}
|
||||
this.opponentCharacterKey = 'balard';
|
||||
} else {
|
||||
this.playerCharacterKey = p1Entry ? p1Entry.chosenCharacterKey : null;
|
||||
this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null;
|
||||
|
||||
// Используем геттер playerCount
|
||||
if (this.playerCount === 1 && p1Entry && !this.playerCharacterKey) {
|
||||
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP init (1 player): Player char key missing.'); return false;
|
||||
}
|
||||
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
|
||||
console.error(`[GameInstance ${this.id}] PvP init error: playerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_v3', `PvP init: playerCount is 2, but a charKey is missing.`);
|
||||
console.error(`[GameInstance ${this.id}] PvP init error: activePlayerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_gi_v4', `PvP init: activePlayerCount is 2, but a charKey is missing.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -422,11 +171,11 @@ class GameInstance {
|
||||
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
|
||||
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
|
||||
|
||||
const isPlayerSlotFilledAndActive = !!playerData;
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); // p2Entry будет null если его нет
|
||||
const isPlayerSlotFilledAndActive = !!(playerData && p1Entry);
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry));
|
||||
|
||||
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
|
||||
this._handleCriticalError('init_ai_data_fail_gs_v3', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
|
||||
this._handleCriticalError('init_ai_data_fail_gs_gi_v4', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
|
||||
}
|
||||
|
||||
this.logBuffer = [];
|
||||
@ -437,19 +186,18 @@ class GameInstance {
|
||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание Игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
|
||||
opponent: isOpponentSlotFilledAndActive ?
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание Игрока 2...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
|
||||
isPlayerTurn: isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive ? Math.random() < 0.5 : true,
|
||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: (this.mode === 'pvp' ? 'Ожидание Игрока 2...' : 'Противник AI'), maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
|
||||
isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true,
|
||||
isGameOver: false,
|
||||
turnNumber: 1,
|
||||
gameMode: this.mode
|
||||
};
|
||||
|
||||
// Не добавляем "Новая битва начинается" здесь, это будет в startGame, когда точно оба готовы
|
||||
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name}. Opponent: ${this.gameState.opponent.name}. Ready for start if both active: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
|
||||
return isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive;
|
||||
return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) : isPlayerSlotFilledAndActive;
|
||||
}
|
||||
|
||||
_createFighterState(roleId, baseStats, abilities) { /* ... Код без изменений ... */
|
||||
_createFighterState(roleId, baseStats, abilities) {
|
||||
const fighterState = {
|
||||
id: roleId, characterKey: baseStats.characterKey, name: baseStats.name,
|
||||
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
|
||||
@ -474,12 +222,11 @@ class GameInstance {
|
||||
console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`);
|
||||
return;
|
||||
}
|
||||
// Перед стартом игры, убедимся, что gameState полностью инициализирован и содержит обоих персонажей.
|
||||
// initializeGame должен был это сделать, но на всякий случай.
|
||||
|
||||
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
|
||||
console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init one last time.`);
|
||||
console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init.`);
|
||||
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_v4', 'Re-initialization before start failed or keys still missing in gameState.');
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_gi_v5', 'Re-initialization before start failed or keys still missing in gameState.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -489,24 +236,22 @@ class GameInstance {
|
||||
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
||||
|
||||
if (!pData || !oData) {
|
||||
this._handleCriticalError('start_char_data_fail_sg_v5', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
|
||||
this._handleCriticalError('start_char_data_fail_sg_gi_v6', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем лог о начале битвы здесь, когда уверены, что оба игрока есть
|
||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
// --- Начальные насмешки ---
|
||||
// Убеждаемся, что объекты gameState.player и gameState.opponent существуют и имеют characterKey
|
||||
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
|
||||
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
|
||||
} else {
|
||||
console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready. GSPlayer: ${this.gameState.player?.name}, GSOpponent: ${this.gameState.opponent?.name}`);
|
||||
console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready.`);
|
||||
}
|
||||
|
||||
const initialLog = this.consumeLogBuffer();
|
||||
|
||||
// Используем геттер this.players
|
||||
Object.values(this.players).forEach(playerInfo => {
|
||||
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
|
||||
const dataForThisClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
|
||||
@ -533,6 +278,7 @@ class GameInstance {
|
||||
}
|
||||
|
||||
processPlayerAction(identifier, actionData) {
|
||||
// Используем геттер this.players
|
||||
const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
if (!actingPlayerInfo || !actingPlayerInfo.socket) {
|
||||
console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return;
|
||||
@ -556,11 +302,11 @@ class GameInstance {
|
||||
const defenderState = this.gameState[defenderRole];
|
||||
|
||||
if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) {
|
||||
this._handleCriticalError('action_actor_state_invalid_v3', `Attacker or Defender state/key invalid. Attacker: ${attackerState?.characterKey}, Defender: ${defenderState?.characterKey}`); return;
|
||||
this._handleCriticalError('action_actor_state_invalid_gi_v4', `Attacker or Defender state/key invalid.`); return;
|
||||
}
|
||||
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
|
||||
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
|
||||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_v3', 'Ошибка данных персонажа при действии.'); return; }
|
||||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi_v4', 'Ошибка данных персонажа при действии.'); return; }
|
||||
|
||||
let actionIsValidAndPerformed = false;
|
||||
|
||||
@ -568,11 +314,6 @@ class GameInstance {
|
||||
this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack');
|
||||
gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
|
||||
actionIsValidAndPerformed = true;
|
||||
// --- ИСПРАВЛЕНИЕ ДЛЯ СИЛЫ ПРИРОДЫ ---
|
||||
// Логика бонуса (реген маны) теперь полностью внутри performAttack в combatLogic.js.
|
||||
// GameInstance НЕ ДОЛЖЕН здесь "потреблять" эффект (обнулять turnsLeft или удалять).
|
||||
// Длительность эффекта управляется в effectsLogic.js.
|
||||
// --- КОНЕЦ ИСПРАВЛЕНИЯ ---
|
||||
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
|
||||
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
|
||||
if (!ability) {
|
||||
@ -580,7 +321,6 @@ class GameInstance {
|
||||
} else {
|
||||
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
|
||||
if (validityCheck.isValid) {
|
||||
console.log(`[GameInstance Taunt Pre-Call] SelfCast: ${attackerState.name} uses ${ability.name} (${ability.id}) against ${defenderState.name} (${defenderState.characterKey})`);
|
||||
this._sayTaunt(attackerState, defenderState.characterKey, 'selfCastAbility', ability.id);
|
||||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
|
||||
gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null);
|
||||
@ -605,16 +345,16 @@ class GameInstance {
|
||||
}
|
||||
}
|
||||
|
||||
switchTurn() { /* ... Код без изменений ... */
|
||||
switchTurn() {
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Switch turn deferred: game paused.`); return; }
|
||||
if (!this.gameState || this.gameState.isGameOver) { return; }
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
|
||||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||||
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid', `Ending turn actor state or key invalid for role ${endingTurnActorRole}.`); return; }
|
||||
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Ending turn actor state or key invalid.`); return; }
|
||||
const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey);
|
||||
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail', `Char data missing for ${endingTurnActorState.characterKey}.`); return; }
|
||||
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Char data missing.`); return; }
|
||||
|
||||
gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils);
|
||||
gameLogic.updateBlockingStatus(endingTurnActorState);
|
||||
@ -629,21 +369,24 @@ class GameInstance {
|
||||
|
||||
const currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const currentTurnActorState = this.gameState[currentTurnActorRole];
|
||||
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid', `Current turn actor state or name invalid for role ${currentTurnActorRole}.`); return; }
|
||||
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Current turn actor state or name invalid.`); return; }
|
||||
|
||||
// Используем геттер this.players
|
||||
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
|
||||
|
||||
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||
this.broadcastGameStateUpdate();
|
||||
|
||||
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
|
||||
console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player disconnected. Timer not started.`);
|
||||
console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player ${currentTurnPlayerEntry.identifier} disconnected. Timer not started by switchTurn.`);
|
||||
} else {
|
||||
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
|
||||
if (isNextTurnAi) setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
}
|
||||
}
|
||||
processAiTurn() { /* ... Код без изменений ... */
|
||||
|
||||
processAiTurn() { // Остается без изменений, так как использует this.gameState
|
||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] AI turn deferred: game paused.`); return; }
|
||||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
|
||||
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) { console.error(`[GameInstance ${this.id}] AI is not Balard!`); this.switchTurn(); return; }
|
||||
@ -651,7 +394,7 @@ class GameInstance {
|
||||
|
||||
const aiState = this.gameState.opponent;
|
||||
const playerState = this.gameState.player;
|
||||
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid', 'Player state invalid for AI taunt.'); return; }
|
||||
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Player state invalid for AI turn.'); return; }
|
||||
|
||||
const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this));
|
||||
let actionIsValidAndPerformedForAI = false;
|
||||
@ -678,8 +421,9 @@ class GameInstance {
|
||||
else { console.error(`[GameInstance ${this.id}] AI failed action. Forcing switch.`); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); }
|
||||
}
|
||||
|
||||
checkGameOver() { /* ... Код без изменений ... */
|
||||
checkGameOver() { // Остается без изменений, так как использует this.gameState
|
||||
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
|
||||
|
||||
if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||
const player = this.gameState.player; const opponent = this.gameState.opponent;
|
||||
const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey);
|
||||
@ -696,44 +440,90 @@ class GameInstance {
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
this.clearAllReconnectTimers();
|
||||
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
const winnerState = this.gameState[gameOverResult.winnerRole]; const loserState = this.gameState[gameOverResult.loserRole];
|
||||
if (winnerState?.characterKey && loserState?.characterKey) this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
|
||||
const winnerState = this.gameState[gameOverResult.winnerRole];
|
||||
const loserState = this.gameState[gameOverResult.loserRole];
|
||||
if (winnerState?.characterKey && loserState?.characterKey) {
|
||||
this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
if (loserState?.characterKey) { /* ... сюжетные логи ... */ }
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Game over. Winner: ${gameOverResult.winnerRole || 'None'}. Reason: ${gameOverResult.reason}.`);
|
||||
this.io.to(this.id).emit('gameOver', { winnerId: gameOverResult.winnerRole, reason: gameOverResult.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: loserState?.characterKey || 'unknown'});
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: gameOverResult.winnerRole,
|
||||
reason: gameOverResult.reason,
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: loserState?.characterKey || 'unknown'
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, `game_ended_${gameOverResult.reason}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { /* ... Код без изменений ... */
|
||||
|
||||
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
|
||||
if (this.gameState && !this.gameState.isGameOver) {
|
||||
this.gameState.isGameOver = true;
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
this.clearAllReconnectTimers();
|
||||
const actualWinnerRole = winnerIfAny !== null ? winnerIfAny : (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
|
||||
|
||||
let actualWinnerRole = winnerIfAny;
|
||||
let winnerActuallyExists = false;
|
||||
|
||||
if (actualWinnerRole) {
|
||||
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole);
|
||||
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
else if (winnerPlayerEntry && !winnerPlayerEntry.isTemporarilyDisconnected) winnerActuallyExists = true;
|
||||
// Используем геттер this.players
|
||||
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
|
||||
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
} else if (winnerPlayerEntry) {
|
||||
winnerActuallyExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!winnerActuallyExists) {
|
||||
actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
|
||||
// Используем геттер this.players
|
||||
const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
|
||||
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
} else if (defaultWinnerEntry) {
|
||||
winnerActuallyExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null;
|
||||
|
||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole);
|
||||
|
||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
console.log(`[GameInstance ${this.id}] Game ended: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`);
|
||||
this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: disconnectedCharacterKey, disconnectedCharacterName: reason === 'opponent_disconnected' || reason === 'player_left_ai_game' ? (this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined });
|
||||
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_${result.reason}`);
|
||||
} else if (this.gameState?.isGameOver) { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`); this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup`); }
|
||||
else { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`); this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup`); }
|
||||
console.log(`[GameInstance ${this.id}] Game ended by disconnect: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: result.winnerRole,
|
||||
reason: result.reason,
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: disconnectedCharacterKey,
|
||||
disconnectedCharacterName: (reason === 'opponent_disconnected' || reason === 'player_left_ai_game' || reason === 'opponent_left_pvp_game') ?
|
||||
(this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${result.reason}`);
|
||||
} else if (this.gameState?.isGameOver) {
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`);
|
||||
this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup_gi`);
|
||||
} else {
|
||||
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`);
|
||||
this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`);
|
||||
}
|
||||
}
|
||||
|
||||
playerExplicitlyLeftAiGame(identifier) {
|
||||
if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) {
|
||||
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over. Identifier: ${identifier}`);
|
||||
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over`);
|
||||
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over.`);
|
||||
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`);
|
||||
return;
|
||||
}
|
||||
// Используем геттер this.players
|
||||
const playerEntry = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) {
|
||||
console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Identifier ${identifier} is not the human player or not found.`);
|
||||
@ -751,32 +541,39 @@ class GameInstance {
|
||||
if (this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
this.clearAllReconnectTimers();
|
||||
|
||||
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly');
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: GAME_CONFIG.OPPONENT_ID,
|
||||
reason: "player_left_ai_game",
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: playerEntry.chosenCharacterKey
|
||||
});
|
||||
|
||||
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi');
|
||||
}
|
||||
|
||||
playerDidSurrender(surrenderingPlayerIdentifier) {
|
||||
console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`);
|
||||
|
||||
if (!this.gameState || this.gameState.isGameOver) {
|
||||
if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished`); }
|
||||
if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished_gi`); }
|
||||
console.warn(`[GameInstance ${this.id}] Surrender attempt on inactive/finished game by ${surrenderingPlayerIdentifier}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Используем геттер this.players
|
||||
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||||
if (!surrenderedPlayerEntry) {
|
||||
console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found in this.players.`);
|
||||
console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const surrenderingPlayerRole = surrenderedPlayerEntry.id; // ОПРЕДЕЛЯЕМ ЗДЕСЬ
|
||||
const surrenderingPlayerRole = surrenderedPlayerEntry.id;
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) {
|
||||
console.log(`[GameInstance ${this.id}] Player ${surrenderingPlayerIdentifier} "surrendered" (left) AI game.`);
|
||||
this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier);
|
||||
} else {
|
||||
console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}) or unexpected: ${surrenderingPlayerIdentifier}`);
|
||||
console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -809,54 +606,93 @@ class GameInstance {
|
||||
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: surrenderedPlayerCharKey
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, "player_surrendered");
|
||||
this.gameManager._cleanupGame(this.id, "player_surrendered_gi");
|
||||
}
|
||||
|
||||
handleTurnTimeout() { /* ... Код без изменений ... */
|
||||
handleTurnTimeout() {
|
||||
if (!this.gameState || this.gameState.isGameOver) return;
|
||||
console.log(`[GameInstance ${this.id}] Turn timeout occurred.`);
|
||||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
|
||||
const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
let winnerActuallyExists = false;
|
||||
if (this.mode === 'ai' && winnerPlayerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
else { const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRole && !p.isTemporarilyDisconnected); winnerActuallyExists = !!winnerEntry; }
|
||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRole : null, timedOutPlayerRole);
|
||||
|
||||
if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) {
|
||||
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
|
||||
} else {
|
||||
// Используем геттер this.players
|
||||
const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected);
|
||||
winnerActuallyExists = !!winnerEntry;
|
||||
}
|
||||
|
||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole);
|
||||
|
||||
this.gameState.isGameOver = true;
|
||||
this.clearAllReconnectTimers();
|
||||
|
||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) {
|
||||
this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat');
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Turn timed out for ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Winner: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`);
|
||||
this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' });
|
||||
this.gameManager._cleanupGame(this.id, `timeout_${result.reason}`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: result.winnerRole,
|
||||
reason: result.reason,
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown'
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, `timeout_gi_${result.reason}`);
|
||||
}
|
||||
_handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений ... */
|
||||
|
||||
_handleCriticalError(reasonCode, logMessage) {
|
||||
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`);
|
||||
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
||||
else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
||||
|
||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
||||
this.clearAllReconnectTimers();
|
||||
|
||||
this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.io.to(this.id).emit('gameOver', { winnerId: null, reason: `server_error_${reasonCode}`, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: 'unknown'});
|
||||
this.gameManager._cleanupGame(this.id, `critical_error_${reasonCode}`);
|
||||
this.io.to(this.id).emit('gameOver', {
|
||||
winnerId: null,
|
||||
reason: `server_error_${reasonCode}`,
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer(),
|
||||
loserCharacterKey: 'unknown'
|
||||
});
|
||||
this.gameManager._cleanupGame(this.id, `critical_error_gi_${reasonCode}`);
|
||||
}
|
||||
|
||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { /* ... Код без изменений ... */
|
||||
if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||||
if (!message) return;
|
||||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||||
}
|
||||
consumeLogBuffer() { /* ... Код без изменений ... */
|
||||
const logs = [...this.logBuffer]; this.logBuffer = []; return logs;
|
||||
|
||||
consumeLogBuffer() {
|
||||
const logs = [...this.logBuffer];
|
||||
this.logBuffer = [];
|
||||
return logs;
|
||||
}
|
||||
broadcastGameStateUpdate() { /* ... Код без изменений ... */
|
||||
if (this.isGameEffectivelyPaused()) { return; } if (!this.gameState) return;
|
||||
|
||||
broadcastGameStateUpdate() {
|
||||
if (this.isGameEffectivelyPaused()) { return; }
|
||||
if (!this.gameState) return;
|
||||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
||||
}
|
||||
broadcastLogUpdate() { /* ... Код без изменений ... */
|
||||
|
||||
broadcastLogUpdate() {
|
||||
if (this.isGameEffectivelyPaused() && this.logBuffer.some(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM)) {
|
||||
const systemLogs = this.logBuffer.filter(log => log.type === GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
if (systemLogs.length > 0) this.io.to(this.id).emit('logUpdate', { log: systemLogs });
|
||||
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); return;
|
||||
if (systemLogs.length > 0) {
|
||||
this.io.to(this.id).emit('logUpdate', { log: systemLogs });
|
||||
}
|
||||
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
return;
|
||||
}
|
||||
if (this.logBuffer.length > 0) {
|
||||
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
|
||||
}
|
||||
if (this.logBuffer.length > 0) this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
|
||||
}
|
||||
}
|
||||
|
||||
|
397
server/game/instance/PlayerConnectionHandler.js
Normal file
397
server/game/instance/PlayerConnectionHandler.js
Normal file
@ -0,0 +1,397 @@
|
||||
// /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 } }
|
||||
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}] Initialized.`);
|
||||
}
|
||||
|
||||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||||
console.log(`[PCH ${this.gameId}] addPlayer attempt. 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 ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`);
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
console.warn(`[PCH ${this.gameId}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
|
||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||
this.gameInstance.gameManager._cleanupGame(this.gameId, `rejoin_attempt_to_finished_game_pch_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||
}
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
|
||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
let assignedPlayerId;
|
||||
let actualCharacterKey = chosenCharacterKey || 'elena';
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
return false;
|
||||
}
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
} else { // pvp
|
||||
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
} else if (!this.playerSockets[GAME_CONFIG.OPPONENT_ID]) {
|
||||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
|
||||
if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
||||
else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
||||
// Добавьте другие пары, если нужно, или более общую логику выбора другого персонажа
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если для этой роли уже был игрок (например, старый сокет), удаляем его
|
||||
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id);
|
||||
if (oldPlayerSocketIdForRole) {
|
||||
const oldPlayerInfo = this.players[oldPlayerSocketIdForRole];
|
||||
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); } catch(e){} } // Убедимся, что старый сокет покинул комнату
|
||||
delete this.players[oldPlayerSocketIdForRole];
|
||||
}
|
||||
|
||||
|
||||
this.players[socket.id] = {
|
||||
id: assignedPlayerId,
|
||||
socket: socket,
|
||||
chosenCharacterKey: actualCharacterKey,
|
||||
identifier: identifier,
|
||||
isTemporarilyDisconnected: false
|
||||
};
|
||||
this.playerSockets[assignedPlayerId] = socket;
|
||||
this.playerCount++;
|
||||
socket.join(this.gameId);
|
||||
|
||||
// Сообщаем GameInstance об установленных ключах и владельце
|
||||
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);
|
||||
}
|
||||
|
||||
const charData = dataUtils.getCharacterData(actualCharacterKey); // Используем dataUtils напрямую
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${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}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`);
|
||||
|
||||
if (playerInfo.socket) {
|
||||
try { playerInfo.socket.leave(this.gameId); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
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}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`);
|
||||
|
||||
// Сигнализируем GameInstance, чтобы он решил, нужно ли завершать игру
|
||||
this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason);
|
||||
|
||||
} else {
|
||||
console.warn(`[PCH ${this.gameId}] removePlayer called for unknown socketId: ${socketId}`);
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||
console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`);
|
||||
// Находим запись игрока по роли и идентификатору, так как сокет мог уже измениться или быть удален
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
if (!playerEntry || !playerEntry.socket) {
|
||||
console.warn(`[PCH ${this.gameId}] No player entry or socket found for ${identifier} (role ${playerIdRole}) during potential left.`);
|
||||
return;
|
||||
}
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
console.log(`[PCH ${this.gameId}] Game already over, not handling potential left for ${identifier}.`);
|
||||
return;
|
||||
}
|
||||
if (playerEntry.isTemporarilyDisconnected) {
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} already marked as temp disconnected.`);
|
||||
return;
|
||||
}
|
||||
|
||||
playerEntry.isTemporarilyDisconnected = true;
|
||||
this.playerCount--; // Уменьшаем счетчик активных игроков
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`);
|
||||
|
||||
const disconnectedName = 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]; // Берем сокет из нашего this.playerSockets
|
||||
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.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isAiCurrentlyMakingMove) ) {
|
||||
this.pausedTurnState = this.gameInstance.turnTimer.pause();
|
||||
console.log(`[PCH ${this.gameId}] Turn timer paused due to disconnect. State:`, JSON.stringify(this.pausedTurnState));
|
||||
} else {
|
||||
this.pausedTurnState = null; // Явно сбрасываем, если таймер не был активен
|
||||
}
|
||||
|
||||
this.clearReconnectTimer(playerIdRole); // Очищаем старый таймер, если был
|
||||
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
|
||||
const reconnectStartTime = Date.now();
|
||||
|
||||
// Таймер для обновления UI клиента
|
||||
const updateInterval = setInterval(() => {
|
||||
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
|
||||
if (remaining <= 0) { // Если основной таймаут уже сработал или время вышло
|
||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
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(() => {
|
||||
this.clearReconnectTimer(playerIdRole); // Очищаем таймеры (включая updateInterval)
|
||||
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
|
||||
// Передаем socket.id из записи, а не старый socketId, который мог быть от предыдущего сокета
|
||||
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
|
||||
}
|
||||
}, reconnectDuration);
|
||||
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||
const identifier = newSocket.userData?.userId; // Получаем идентификатор из нового сокета
|
||||
console.log(`[PCH ${this.gameId}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
|
||||
|
||||
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
|
||||
newSocket.emit('gameError', { message: 'Игра уже завершена.' });
|
||||
this.gameInstance.gameManager._cleanupGame(this.gameId, `reconnect_to_finished_game_pch_${identifier}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Находим запись игрока по роли и идентификатору
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
|
||||
this.clearReconnectTimer(playerIdRole);
|
||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сигнал, что таймер остановлен
|
||||
|
||||
// Удаляем старую запись по socket.id, если сокет действительно новый
|
||||
const oldSocketId = playerEntry.socket.id;
|
||||
if (this.players[oldSocketId] && oldSocketId !== newSocket.id) {
|
||||
delete this.players[oldSocketId];
|
||||
}
|
||||
|
||||
// Обновляем запись игрока
|
||||
playerEntry.socket = newSocket;
|
||||
playerEntry.isTemporarilyDisconnected = false;
|
||||
this.players[newSocket.id] = playerEntry; // Добавляем/обновляем запись с новым socket.id
|
||||
this.playerSockets[playerIdRole] = newSocket; // Обновляем активный сокет для роли
|
||||
this.playerCount++; // Восстанавливаем счетчик активных игроков
|
||||
|
||||
newSocket.join(this.gameId);
|
||||
const reconnectedName = this.gameInstance.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey;
|
||||
console.log(`[PCH ${this.gameId}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`);
|
||||
this.gameInstance.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
// Получаем ключ персонажа оппонента из gameState ИЛИ из предварительно сохраненных ключей в GameInstance
|
||||
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
||||
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
|
||||
// Если gameState нет (маловероятно при реконнекте в активную игру, но возможно если это был первый игрок PvP)
|
||||
// GameInstance должен сам решить, нужно ли ему initializeGame()
|
||||
if (!this.gameInstance.gameState) {
|
||||
// Пытаемся инициализировать игру, если она не была инициализирована
|
||||
// Это важно, если первый игрок в PvP отключался до подключения второго
|
||||
if (!this.gameInstance.initializeGame()) {
|
||||
this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch', 'PCH: GS null after re-init on reconnect.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.gameId,
|
||||
yourPlayerId: playerIdRole,
|
||||
initialGameState: this.gameInstance.gameState, // Отправляем текущее состояние
|
||||
playerBaseStats: pData?.baseStats, // Данные для этого игрока
|
||||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||
playerAbilities: pData?.abilities,
|
||||
opponentAbilities: oData?.abilities || [],
|
||||
log: this.gameInstance.consumeLogBuffer(),
|
||||
clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
|
||||
});
|
||||
|
||||
// Уведомляем другого игрока
|
||||
const otherSocket = this.playerSockets[oppRoleKey];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey);
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
otherSocket.emit('playerReconnected', {
|
||||
reconnectedPlayerId: playerIdRole,
|
||||
reconnectedPlayerName: reconnectedName
|
||||
});
|
||||
if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи, если есть
|
||||
otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() });
|
||||
}
|
||||
}
|
||||
|
||||
// Если игра не на "эффективной" паузе и не закончена, возобновляем игру
|
||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
||||
this.gameInstance.broadcastGameStateUpdate(); // Обновляем состояние для всех
|
||||
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
|
||||
this.gameInstance.turnTimer.resume(
|
||||
this.pausedTurnState.remainingTime,
|
||||
this.pausedTurnState.forPlayerRoleIsPlayer,
|
||||
this.pausedTurnState.isAiCurrentlyMoving
|
||||
);
|
||||
this.pausedTurnState = null; // Сбрасываем сохраненное состояние таймера
|
||||
} else {
|
||||
// Если pausedTurnState нет, значит, таймер не был активен или это первый ход
|
||||
// GameInstance.startGame или switchTurn должны запустить таймер корректно
|
||||
// Но если это реконнект в середину игры, где ход уже чей-то, нужно запустить таймер
|
||||
const currentTurnIsForPlayer = this.gameInstance.gameState.isPlayerTurn;
|
||||
const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer;
|
||||
this.gameInstance.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
|
||||
// Игрок уже был подключен и не был отмечен как isTemporarilyDisconnected
|
||||
// Это может быть попытка открыть игру в новой вкладке или "обновить сессию"
|
||||
if (playerEntry.socket.id !== newSocket.id) {
|
||||
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."});
|
||||
return false; // Не позволяем подключиться с нового сокета, если старый активен
|
||||
}
|
||||
// Если это тот же сокет (например, клиент запросил состояние), просто отправляем ему данные
|
||||
if (!this.gameInstance.gameState) { // На всякий случай, если gameState вдруг нет
|
||||
if (!this.gameInstance.initializeGame()) {
|
||||
this.gameInstance._handleCriticalError('reconnect_same_socket_no_gs_pch','PCH: GS null on same socket reconnect.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.gameId,
|
||||
yourPlayerId: playerIdRole,
|
||||
initialGameState: this.gameInstance.gameState,
|
||||
playerBaseStats: pData?.baseStats,
|
||||
opponentBaseStats: oData?.baseStats, // Могут быть неполными, если оппонент еще не подключился
|
||||
playerAbilities: pData?.abilities,
|
||||
opponentAbilities: oData?.abilities,
|
||||
log: this.gameInstance.consumeLogBuffer(),
|
||||
clientConfig: { ...GAME_CONFIG }
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
// Запись игрока не найдена или он не был помечен как isTemporarilyDisconnected, но сокет новый.
|
||||
// Это может быть попытка реконнекта к игре, из которой игрок был уже удален (например, по таймауту).
|
||||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена или сессия устарела).' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
clearReconnectTimer(playerIdRole) {
|
||||
if (this.reconnectTimers[playerIdRole]) {
|
||||
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
|
||||
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
|
||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
}
|
||||
delete this.reconnectTimers[playerIdRole];
|
||||
console.log(`[PCH ${this.gameId}] Cleared reconnect timer for role ${playerIdRole}.`);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllReconnectTimers() {
|
||||
console.log(`[PCH ${this.gameId}] Clearing ALL reconnect timers.`);
|
||||
for (const roleId in this.reconnectTimers) {
|
||||
this.clearReconnectTimer(roleId);
|
||||
}
|
||||
}
|
||||
|
||||
isGameEffectivelyPaused() {
|
||||
if (this.mode === 'pvp') {
|
||||
// Если игроков меньше 2, И есть хотя бы один игрок в this.players (ожидающий или в процессе дисконнекта)
|
||||
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') {
|
||||
// В AI режиме игра на паузе, если единственный человек-игрок временно отключен
|
||||
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния.
|
||||
}
|
||||
return false; // В остальных случаях игра не считается на паузе из-за дисконнектов
|
||||
}
|
||||
|
||||
// Вспомогательный метод для получения информации о всех игроках (может пригодиться GameInstance)
|
||||
getAllPlayersInfo() {
|
||||
return { ...this.players };
|
||||
}
|
||||
|
||||
// Вспомогательный метод для получения сокетов (может пригодиться GameInstance)
|
||||
getPlayerSockets() {
|
||||
return { ...this.playerSockets };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayerConnectionHandler;
|
Loading…
x
Reference in New Issue
Block a user