824 lines
50 KiB
JavaScript
824 lines
50 KiB
JavaScript
// /server/game/instance/GameInstance.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const TurnTimer = require('./TurnTimer');
|
||
const gameLogic = require('../logic');
|
||
const dataUtils = require('../../data/dataUtils');
|
||
const GAME_CONFIG = require('../../core/config');
|
||
|
||
class GameInstance {
|
||
constructor(gameId, io, mode = 'ai', gameManager) {
|
||
this.id = gameId;
|
||
this.io = io;
|
||
this.mode = mode;
|
||
this.players = {};
|
||
this.playerSockets = {};
|
||
this.playerCount = 0;
|
||
this.gameState = null;
|
||
this.aiOpponent = (mode === 'ai');
|
||
this.logBuffer = [];
|
||
this.playerCharacterKey = null;
|
||
this.opponentCharacterKey = null;
|
||
this.ownerIdentifier = null;
|
||
this.gameManager = gameManager;
|
||
this.reconnectTimers = {};
|
||
|
||
this.turnTimer = new TurnTimer(
|
||
GAME_CONFIG.TURN_DURATION_MS,
|
||
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
||
() => this.handleTurnTimeout(),
|
||
(remainingTime, isPlayerTurnForTimer) => {
|
||
if (!this.isGameEffectivelyPaused()) {
|
||
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer });
|
||
}
|
||
}
|
||
);
|
||
|
||
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}.`);
|
||
}
|
||
|
||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||
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 (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||
}
|
||
// Если игра уже завершена, и игрок пытается "добавиться" (что маловероятно, если GameManager.handleRequestGameState работает корректно),
|
||
// то addPlayer не должен успешно завершаться.
|
||
if (this.gameState && this.gameState.isGameOver) {
|
||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||
return false;
|
||
}
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||
return false;
|
||
}
|
||
|
||
if (this.playerCount >= 2) {
|
||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||
return false;
|
||
}
|
||
|
||
let assignedPlayerId;
|
||
let actualCharacterKey;
|
||
|
||
if (this.mode === 'ai') {
|
||
if (this.playerCount > 0) {
|
||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||
return false;
|
||
}
|
||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||
actualCharacterKey = 'elena';
|
||
this.ownerIdentifier = identifier;
|
||
} else { // PvP
|
||
if (!Object.values(this.players).some(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected)) {
|
||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||
actualCharacterKey = (chosenCharacterKey === 'almagest' || chosenCharacterKey === 'balard') ? chosenCharacterKey : 'elena';
|
||
this.ownerIdentifier = identifier;
|
||
} else if (!Object.values(this.players).some(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected)) {
|
||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
if (firstPlayerInfo?.chosenCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
||
else if (firstPlayerInfo?.chosenCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
||
else actualCharacterKey = 'balard'; // Default if first player is Balard or something else
|
||
} else {
|
||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId);
|
||
if (oldPlayerSocketIdForRole) {
|
||
console.log(`[GameInstance ${this.id}] Role ${assignedPlayerId} was previously occupied by socket ${oldPlayerSocketIdForRole}. Removing old entry.`);
|
||
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);
|
||
|
||
const characterBaseStats = dataUtils.getCharacterBaseStats(actualCharacterKey);
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterBaseStats?.name || 'N/A'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Активных игроков: ${this.playerCount}.`);
|
||
return true;
|
||
}
|
||
|
||
removePlayer(socketId, reason = "unknown_reason_for_removal") {
|
||
const playerInfo = this.players[socketId];
|
||
if (playerInfo) {
|
||
const playerRole = playerInfo.id;
|
||
console.log(`[GameInstance ${this.id}] Окончательное удаление игрока ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}). Причина: ${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];
|
||
}
|
||
|
||
console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} удален. Активных игроков: ${this.playerCount}.`);
|
||
|
||
// Завершаем игру, если она была активна и стала неиграбельной
|
||
if (this.gameState && !this.gameState.isGameOver) {
|
||
const isTurnOfDisconnected = (this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.PLAYER_ID) ||
|
||
(!this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.OPPONENT_ID);
|
||
if (isTurnOfDisconnected) this.turnTimer.clear();
|
||
|
||
if (this.mode === 'ai' && this.playerCount === 0) {
|
||
console.log(`[GameInstance ${this.id}] AI игра стала пустой после удаления игрока. Завершение игры.`);
|
||
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_empty_ai_game");
|
||
} else if (this.mode === 'pvp' && this.playerCount < 2) {
|
||
// Убедимся, что остался хотя бы один игрок, чтобы ему присудить победу.
|
||
// Если playerCount стал 0, то победителя нет.
|
||
const remainingPlayer = Object.values(this.players).find(p => !p.isTemporarilyDisconnected);
|
||
const winnerRoleIfAny = remainingPlayer ? remainingPlayer.id : null;
|
||
|
||
console.log(`[GameInstance ${this.id}] PvP игра стала неполной (${this.playerCount} игроков) после удаления игрока ${playerInfo.identifier} (роль ${playerRole}).`);
|
||
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", winnerRoleIfAny);
|
||
}
|
||
}
|
||
} else {
|
||
console.log(`[GameInstance ${this.id}] Попытка удалить игрока по socketId ${socketId}, но он не найден.`);
|
||
}
|
||
}
|
||
|
||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft CALLED for role ${playerIdRole}, identifier ${identifier}, charKey ${characterKey}`);
|
||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||
|
||
if (!playerEntry || !playerEntry.socket) {
|
||
console.warn(`[GameInstance ${this.id}] Не найден активный игрок ${identifier} (роль: ${playerIdRole}) для пометки как отключенного.`);
|
||
return;
|
||
}
|
||
if (this.gameState && this.gameState.isGameOver) {
|
||
console.log(`[GameInstance ${this.id}] Игра уже завершена, игнорируем 'potentiallyLeft' для ${identifier}.`);
|
||
return;
|
||
}
|
||
if (playerEntry.isTemporarilyDisconnected) {
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) уже помечен как временно отключенный. Таймер должен быть активен.`);
|
||
return;
|
||
}
|
||
|
||
playerEntry.isTemporarilyDisconnected = true;
|
||
this.playerCount--; // Уменьшаем счетчик АКТИВНЫХ игроков
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) помечен как временно отключенный. Активных игроков: ${this.playerCount}. Запуск таймера реконнекта.`);
|
||
|
||
const disconnectedPlayerName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (роль ${playerIdRole})`;
|
||
const disconnectLogMessage = `🔌 Игрок ${disconnectedPlayerName} отключился. Ожидание переподключения...`;
|
||
this.addToLog(disconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
|
||
this.io.to(this.id).emit('opponentDisconnected', {
|
||
disconnectedPlayerId: playerIdRole,
|
||
disconnectedCharacterName: disconnectedPlayerName,
|
||
});
|
||
this.broadcastLogUpdate(); // Отправляем лог об отключении
|
||
|
||
const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
if (currentTurnPlayerRole === playerIdRole) {
|
||
this.turnTimer.clear();
|
||
console.log(`[GameInstance ${this.id}] Ход был за отключившимся игроком ${playerIdRole}, таймер хода остановлен.`);
|
||
}
|
||
|
||
this.clearReconnectTimer(playerIdRole);
|
||
this.reconnectTimers[playerIdRole] = setTimeout(() => {
|
||
console.log(`[GameInstance ${this.id}] Таймер реконнекта для игрока ${identifier} (роль: ${playerIdRole}) истек.`);
|
||
delete this.reconnectTimers[playerIdRole];
|
||
const stillDisconnectedPlayerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier && p.isTemporarilyDisconnected);
|
||
if (stillDisconnectedPlayerEntry) {
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) не переподключился. Удаляем окончательно.`);
|
||
this.removePlayer(stillDisconnectedPlayerEntry.socket.id, "reconnect_timeout");
|
||
} else {
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) уже переподключился или был удален ранее. Таймер истек без действия.`);
|
||
}
|
||
}, GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000);
|
||
}
|
||
|
||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||
const identifier = newSocket.userData?.userId;
|
||
console.log(`[GameInstance ${this.id}] handlePlayerReconnected CALLED for role ${playerIdRole}, identifier ${identifier}, newSocket ${newSocket.id}`);
|
||
|
||
if (this.gameState && this.gameState.isGameOver) {
|
||
console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) пытается переподключиться к уже ЗАВЕРШЕННОЙ игре. Отправка gameError.`);
|
||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию: игра уже завершена.' });
|
||
// GameManager.handleRequestGameState должен был это перехватить, но на всякий случай.
|
||
return false;
|
||
}
|
||
|
||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||
|
||
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
|
||
this.clearReconnectTimer(playerIdRole);
|
||
const oldSocketId = playerEntry.socket.id;
|
||
|
||
if (this.players[oldSocketId]) {
|
||
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 reconnectedPlayerName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey || `Игрок (роль ${playerIdRole})`;
|
||
console.log(`[GameInstance ${this.id}] Игрок ${identifier} (${reconnectedPlayerName}) успешно переподключен с новым сокетом ${newSocket.id}. Старый сокет: ${oldSocketId}. Активных игроков: ${this.playerCount}.`);
|
||
|
||
const reconnectLogMessage = `🔌 Игрок ${reconnectedPlayerName} снова в игре!`;
|
||
this.addToLog(reconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
|
||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||
const opponentRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const oCharKey = this.gameState?.[opponentRoleKey]?.characterKey;
|
||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||
|
||
const logForReconnectedPlayer = this.consumeLogBuffer();
|
||
newSocket.emit('gameStarted', { // Используем '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: logForReconnectedPlayer,
|
||
clientConfig: { ...GAME_CONFIG }
|
||
});
|
||
|
||
const otherPlayerSocket = Object.values(this.players).find(p =>
|
||
p.id !== playerIdRole &&
|
||
p.socket && p.socket.connected &&
|
||
!p.isTemporarilyDisconnected
|
||
)?.socket;
|
||
|
||
if (otherPlayerSocket) {
|
||
otherPlayerSocket.emit('playerReconnected', {
|
||
playerId: playerIdRole,
|
||
playerName: reconnectedPlayerName
|
||
});
|
||
// Логи, которые могли накопиться для другого игрока (например, сообщение о реконнекте), уйдут со следующим broadcastGameStateUpdate
|
||
}
|
||
|
||
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) {
|
||
console.log(`[GameInstance ${this.id}] Игра возобновлена после переподключения ${identifier} (роль: ${playerIdRole}). Отправка gameStateUpdate всем и перезапуск таймера.`);
|
||
this.broadcastGameStateUpdate(); // Отправит оставшиеся логи
|
||
|
||
const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
const otherPlayerEntry = Object.values(this.players).find(p => p.id !== playerIdRole); // Проверяем другого игрока в целом
|
||
|
||
// Таймер запускаем, если сейчас ход реконнектнувшегося ИЛИ если другой игрок активен (не isTemporarilyDisconnected)
|
||
if (currentTurnPlayerRole === playerIdRole || (otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected)) {
|
||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||
} else {
|
||
console.log(`[GameInstance ${this.id}] Другой игрок (${otherPlayerEntry?.id}) отключен, таймер хода не запускается после реконнекта ${playerIdRole}.`);
|
||
}
|
||
}
|
||
return true;
|
||
|
||
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
|
||
console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) пытается переподключиться, но не был помечен как отключенный.`);
|
||
if (playerEntry.socket.id !== newSocket.id) {
|
||
newSocket.emit('gameError', {message: "Вы уже активно подключены к этой игре."});
|
||
}
|
||
return false;
|
||
} else {
|
||
console.warn(`[GameInstance ${this.id}] Не удалось найти игрока ${identifier} (роль: ${playerIdRole}) для переподключения, или он не был помечен как отключенный.`);
|
||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию в этой игре.'});
|
||
return false;
|
||
}
|
||
}
|
||
|
||
clearReconnectTimer(playerIdRole) {
|
||
if (this.reconnectTimers[playerIdRole]) {
|
||
clearTimeout(this.reconnectTimers[playerIdRole]);
|
||
delete this.reconnectTimers[playerIdRole];
|
||
console.log(`[GameInstance ${this.id}] Таймер реконнекта для роли ${playerIdRole} очищен.`);
|
||
}
|
||
}
|
||
|
||
clearAllReconnectTimers() {
|
||
console.log(`[GameInstance ${this.id}] Очистка ВСЕХ таймеров реконнекта.`);
|
||
for (const roleId in this.reconnectTimers) {
|
||
this.clearReconnectTimer(roleId);
|
||
}
|
||
}
|
||
|
||
isGameEffectivelyPaused() {
|
||
if (this.mode === 'pvp') {
|
||
// Игра на паузе, если хотя бы один из ДВУХ ожидаемых игроков временно отключен
|
||
const player1 = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
const player2 = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
return (player1?.isTemporarilyDisconnected || false) || (player2?.isTemporarilyDisconnected || false);
|
||
} else if (this.mode === 'ai') {
|
||
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
return humanPlayer?.isTemporarilyDisconnected ?? (this.playerCount === 0 && Object.keys(this.players).length > 0);
|
||
}
|
||
return false; // По умолчанию не на паузе
|
||
}
|
||
|
||
initializeGame() {
|
||
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков: ${this.playerCount}. Всего записей в players: ${Object.keys(this.players).length}`);
|
||
if (this.mode === 'ai' && this.playerCount === 1) {
|
||
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
|
||
this.opponentCharacterKey = 'balard';
|
||
} else if (this.mode === 'pvp') {
|
||
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||
const p2Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||
|
||
if (p1Info) this.playerCharacterKey = p1Info.chosenCharacterKey;
|
||
else this.playerCharacterKey = null;
|
||
|
||
if (p2Info) this.opponentCharacterKey = p2Info.chosenCharacterKey;
|
||
else this.opponentCharacterKey = null;
|
||
|
||
if (this.playerCount === 1 && p1Info && !p2Info) {
|
||
// this.opponentCharacterKey остается null
|
||
} else if (this.playerCount === 2 && (!p1Info || !p2Info)) {
|
||
console.error(`[GameInstance ${this.id}] Ошибка инициализации PvP: playerCount=2, но один из игроков не найден как активный.`);
|
||
return false;
|
||
} else if (this.playerCount < 2 && !p1Info) {
|
||
console.log(`[GameInstance ${this.id}] Инициализация PvP без активного первого игрока. playerCharacterKey будет ${this.playerCharacterKey}.`);
|
||
}
|
||
} else {
|
||
console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации! Активных: ${this.playerCount}`); return false;
|
||
}
|
||
|
||
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
|
||
let opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
|
||
const isOpponentDefined = !!this.opponentCharacterKey;
|
||
|
||
if (!playerData && (this.mode === 'ai' || (this.mode === 'pvp' && this.playerCount > 0))) {
|
||
this._handleCriticalError('init_player_data_fail', 'Ошибка загрузки данных основного игрока при инициализации.');
|
||
return false;
|
||
}
|
||
if (isOpponentDefined && !opponentData) {
|
||
this._handleCriticalError('init_opponent_data_fail', 'Ошибка загрузки данных оппонента при инициализации.');
|
||
return false;
|
||
}
|
||
|
||
this.gameState = {
|
||
player: playerData ?
|
||
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities) :
|
||
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []),
|
||
opponent: isOpponentDefined && opponentData ?
|
||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
|
||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока 2...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []),
|
||
isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true,
|
||
isGameOver: false, turnNumber: 1, gameMode: this.mode
|
||
};
|
||
|
||
if (isOpponentDefined) {
|
||
this.logBuffer = [];
|
||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
const pCharKey = this.gameState.player.characterKey;
|
||
const oCharKey = this.gameState.opponent.characterKey;
|
||
if ((pCharKey === 'elena' || pCharKey === 'almagest') && oCharKey) {
|
||
const opponentFullDataForTaunt = dataUtils.getCharacterData(oCharKey);
|
||
const startTaunt = gameLogic.getRandomTaunt(pCharKey, 'battleStart', {}, GAME_CONFIG, opponentFullDataForTaunt, this.gameState);
|
||
if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Готовность к старту (isOpponentDefined): ${isOpponentDefined}`);
|
||
return isOpponentDefined;
|
||
}
|
||
|
||
_createFighterState(roleId, baseStats, abilities) {
|
||
const fighterState = {
|
||
id: roleId, characterKey: baseStats.characterKey, name: baseStats.name,
|
||
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
|
||
currentResource: baseStats.maxResource, maxResource: baseStats.maxResource,
|
||
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
||
};
|
||
(abilities || []).forEach(ability => {
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
fighterState.abilityCooldowns[ability.id] = 0;
|
||
}
|
||
});
|
||
if (baseStats.characterKey === 'balard') {
|
||
fighterState.silenceCooldownTurns = 0;
|
||
fighterState.manaDrainCooldownTurns = 0;
|
||
}
|
||
return fighterState;
|
||
}
|
||
|
||
startGame() {
|
||
if (this.isGameEffectivelyPaused()) {
|
||
console.log(`[GameInstance ${this.id}] Попытка старта игры, но она на паузе из-за дисконнекта. Старт отложен.`);
|
||
return;
|
||
}
|
||
if (!this.gameState || !this.gameState.opponent?.characterKey) {
|
||
this._handleCriticalError('start_game_not_ready', 'Попытка старта не полностью готовой игры (нет оппонента).');
|
||
return;
|
||
}
|
||
console.log(`[GameInstance ${this.id}] Запуск игры.`);
|
||
|
||
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
|
||
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
||
if (!pData || !oData) { this._handleCriticalError('start_char_data_fail', 'Ошибка данных персонажей при старте.'); return; }
|
||
|
||
const initialLog = this.consumeLogBuffer();
|
||
|
||
Object.values(this.players).forEach(playerInfo => {
|
||
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
|
||
const dataForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
|
||
{ playerBaseStats: pData.baseStats, opponentBaseStats: oData.baseStats, playerAbilities: pData.abilities, opponentAbilities: oData.abilities } :
|
||
{ playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities };
|
||
playerInfo.socket.emit('gameStarted', {
|
||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||
...dataForClient, log: [...initialLog],
|
||
clientConfig: { ...GAME_CONFIG }
|
||
});
|
||
}
|
||
});
|
||
|
||
const firstTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${firstTurnActor.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
this.broadcastLogUpdate();
|
||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent) {
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||
}
|
||
}
|
||
|
||
processPlayerAction(requestingSocketId, actionData) {
|
||
if (this.isGameEffectivelyPaused()) {
|
||
console.warn(`[GameInstance ${this.id}] Действие от сокета ${requestingSocketId}, но игра на паузе. Действие отклонено.`);
|
||
const playerInfo = this.players[requestingSocketId];
|
||
if (playerInfo?.socket) playerInfo.socket.emit('gameError', {message: "Действие невозможно: другой игрок отключен."});
|
||
return;
|
||
}
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
const actingPlayerInfo = this.players[requestingSocketId];
|
||
if (!actingPlayerInfo) { console.error(`[GameInstance ${this.id}] Действие от неизвестного сокета ${requestingSocketId}`); return; }
|
||
|
||
const actingPlayerRole = actingPlayerInfo.id;
|
||
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
|
||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
|
||
if (!isCorrectTurn) { console.warn(`[GameInstance ${this.id}] Действие от ${actingPlayerInfo.identifier}: не его ход.`); return; }
|
||
|
||
this.turnTimer.clear();
|
||
|
||
const attackerState = this.gameState[actingPlayerRole];
|
||
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const defenderState = this.gameState[defenderRole];
|
||
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
|
||
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
|
||
|
||
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail', 'Ошибка данных персонажа при действии.'); return; }
|
||
|
||
let actionValid = true;
|
||
let tauntContextTargetData = defenderData;
|
||
|
||
if (actionData.actionType === 'attack') {
|
||
const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, tauntContextTargetData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
||
const delayedBuff = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK));
|
||
if (delayedBuff && !delayedBuff.justCast) {
|
||
const regen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource);
|
||
if (regen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + regen);
|
||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${regen} ${attackerState.resourceName} от "${delayedBuff.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||
}
|
||
}
|
||
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
|
||
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
|
||
if (!ability) {
|
||
actionValid = false;
|
||
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
|
||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||
return;
|
||
}
|
||
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
|
||
if (!validityCheck.isValid) {
|
||
this.addToLog(validityCheck.reason, GAME_CONFIG.LOG_TYPE_INFO);
|
||
actionValid = false;
|
||
}
|
||
|
||
if (actionValid) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
|
||
const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'selfCastAbility', { abilityId: ability.id }, GAME_CONFIG, tauntContextTargetData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
||
gameLogic.setAbilityCooldown(ability, attackerState, GAME_CONFIG);
|
||
}
|
||
} else {
|
||
console.warn(`[GameInstance ${this.id}] Неизвестный тип действия: ${actionData?.actionType}`);
|
||
actionValid = false;
|
||
}
|
||
|
||
if (this.checkGameOver()) {
|
||
return;
|
||
}
|
||
if (actionValid) {
|
||
this.broadcastLogUpdate();
|
||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||
} else {
|
||
this.broadcastLogUpdate();
|
||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||
}
|
||
}
|
||
|
||
switchTurn() {
|
||
if (this.isGameEffectivelyPaused()) {
|
||
console.log(`[GameInstance ${this.id}] Попытка сменить ход, но игра на паузе. Смена хода отложена.`);
|
||
return;
|
||
}
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
this.turnTimer.clear();
|
||
|
||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
const endingTurnActor = this.gameState[endingTurnActorRole];
|
||
const endingTurnData = dataUtils.getCharacterData(endingTurnActor.characterKey);
|
||
|
||
if (!endingTurnData) { this._handleCriticalError('switch_turn_data_fail', 'Ошибка данных при смене хода.'); return; }
|
||
|
||
gameLogic.processEffects(endingTurnActor.activeEffects, endingTurnActor, endingTurnData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils);
|
||
gameLogic.updateBlockingStatus(this.gameState.player);
|
||
gameLogic.updateBlockingStatus(this.gameState.opponent);
|
||
if (endingTurnActor.abilityCooldowns && endingTurnData.abilities) gameLogic.processPlayerAbilityCooldowns(endingTurnActor.abilityCooldowns, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG);
|
||
if (endingTurnActor.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActor);
|
||
if (endingTurnActor.disabledAbilities?.length > 0 && endingTurnData.abilities) gameLogic.processDisabledAbilities(endingTurnActor.disabledAbilities, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG);
|
||
|
||
if (this.checkGameOver()) {
|
||
return;
|
||
}
|
||
|
||
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
|
||
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
|
||
|
||
const currentTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActor.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
this.broadcastGameStateUpdate();
|
||
this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn));
|
||
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent) {
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||
}
|
||
}
|
||
|
||
processAiTurn() {
|
||
if (this.isGameEffectivelyPaused()) {
|
||
console.log(`[GameInstance ${this.id}] Попытка хода AI, но игра на паузе. Ход AI отложен.`);
|
||
return;
|
||
}
|
||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
|
||
if (this.gameState && !this.gameState.isGameOver) this.switchTurn();
|
||
return;
|
||
}
|
||
|
||
this.turnTimer.clear();
|
||
const attacker = this.gameState.opponent;
|
||
const defender = this.gameState.player;
|
||
const attackerData = dataUtils.getCharacterData('balard');
|
||
const defenderData = dataUtils.getCharacterData(defender.characterKey);
|
||
|
||
if (!attackerData || !defenderData) { this._handleCriticalError('ai_char_data_fail', 'Ошибка данных AI.'); this.switchTurn(); return; }
|
||
|
||
if (gameLogic.isCharacterFullySilenced(attacker, GAME_CONFIG)) {
|
||
this.addToLog(`😵 ${attacker.name} под действием Безмолвия! Атакует в смятении.`, GAME_CONFIG.LOG_TYPE_EFFECT);
|
||
gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, defenderData);
|
||
if (this.checkGameOver()) { return; }
|
||
this.broadcastLogUpdate();
|
||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||
return;
|
||
}
|
||
|
||
const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this));
|
||
let tauntContextTargetData = defenderData;
|
||
|
||
if (aiDecision.actionType === 'attack') {
|
||
gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
||
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
|
||
attacker.currentResource = Math.round(attacker.currentResource - aiDecision.ability.cost);
|
||
gameLogic.applyAbilityEffect(aiDecision.ability, attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData);
|
||
gameLogic.setAbilityCooldown(aiDecision.ability, attacker, GAME_CONFIG);
|
||
}
|
||
|
||
if (this.checkGameOver()) {
|
||
return;
|
||
}
|
||
this.broadcastLogUpdate();
|
||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||
}
|
||
|
||
checkGameOver() {
|
||
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
|
||
if (this.mode === 'pvp' && (!this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey)) {
|
||
return false;
|
||
}
|
||
if (this.mode === 'ai' && !this.gameState.player?.characterKey) return false;
|
||
|
||
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
|
||
if (gameOverResult.isOver) {
|
||
this.gameState.isGameOver = true;
|
||
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 && (winnerState.characterKey === 'elena' || winnerState.characterKey === 'almagest') && loserState) {
|
||
const loserFullData = dataUtils.getCharacterData(loserState.characterKey);
|
||
if (loserFullData) {
|
||
const taunt = gameLogic.getRandomTaunt(winnerState.characterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, loserFullData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
if (loserState) {
|
||
if (loserState.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserState.name} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
else if (loserState.characterKey === 'almagest') this.addToLog(`Над полем битвы воцаряется тишина. ${loserState.name} побежден(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
else if (loserState.characterKey === 'elena') this.addToLog(`Свет погас. ${loserState.name} повержен(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
|
||
console.log(`[GameInstance ${this.id}] Игра окончена. Победитель: ${gameOverResult.winnerRole || 'Нет'}. Причина: ${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.gameManager._cleanupGame(this.id, gameOverResult.reason);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
|
||
if (this.gameState && !this.gameState.isGameOver) {
|
||
this.gameState.isGameOver = true;
|
||
this.turnTimer.clear();
|
||
this.clearAllReconnectTimers();
|
||
|
||
const actualWinnerRole = winnerIfAny !== null ? winnerIfAny :
|
||
(disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
|
||
|
||
const winnerExists = Object.values(this.players).some(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected) ||
|
||
(this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID && this.gameState.opponent?.characterKey);
|
||
|
||
const result = gameLogic.getGameOverResult(
|
||
this.gameState, GAME_CONFIG, this.mode, reason,
|
||
winnerExists ? actualWinnerRole : null,
|
||
disconnectedPlayerRole
|
||
);
|
||
|
||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
|
||
console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта/ухода. Причина: ${reason}. Победитель: ${result.winnerRole || 'Нет'}. Отключился/ушел: ${disconnectedPlayerRole}.`);
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: result.winnerRole, reason: result.reason,
|
||
finalGameState: this.gameState, log: this.consumeLogBuffer(),
|
||
loserCharacterKey: disconnectedCharacterKey
|
||
});
|
||
this.gameManager._cleanupGame(this.id, result.reason);
|
||
} else if (this.gameState && this.gameState.isGameOver) {
|
||
console.log(`[GameInstance ${this.id}] Попытка завершить игру из-за дисконнекта, но она уже завершена.`);
|
||
}
|
||
}
|
||
|
||
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||
playerDidSurrender(surrenderingPlayerIdentifier) {
|
||
console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`);
|
||
|
||
if (!this.gameState || this.gameState.isGameOver) {
|
||
console.warn(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} попытался сдаться, но игра неактивна или уже завершена.`);
|
||
return;
|
||
}
|
||
|
||
if (this.mode !== 'pvp') {
|
||
console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} сдался в не-PvP игре. Просто завершаем игру, если это AI режим и игрок один.`);
|
||
if (this.mode === 'ai') {
|
||
const playerInfo = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||
if (playerInfo) {
|
||
this.endGameDueToDisconnect(playerInfo.id, playerInfo.chosenCharacterKey, "player_left_ai_game");
|
||
} else {
|
||
this.gameManager._cleanupGame(this.id, "surrender_ai_player_not_found");
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
|
||
if (!surrenderedPlayerEntry) {
|
||
console.error(`[GameInstance ${this.id}] Не найден игрок с identifier ${surrenderingPlayerIdentifier} для обработки сдачи.`);
|
||
return;
|
||
}
|
||
|
||
const surrenderedPlayerRole = surrenderedPlayerEntry.id;
|
||
const surrenderedPlayerName = this.gameState[surrenderedPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey || `Игрок (ID: ${surrenderingPlayerIdentifier})`;
|
||
const surrenderedPlayerCharKey = this.gameState[surrenderedPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey;
|
||
|
||
const winnerRole = surrenderedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const winnerName = this.gameState[winnerRole]?.name || `Оппонент (Роль: ${winnerRole})`;
|
||
|
||
this.gameState.isGameOver = true;
|
||
this.turnTimer.clear();
|
||
this.clearAllReconnectTimers(); // Также очищаем таймеры реконнекта
|
||
|
||
const surrenderMessage = `🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`;
|
||
this.addToLog(surrenderMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
console.log(`[GameInstance ${this.id}] ${surrenderMessage}`);
|
||
|
||
const reasonForGameOver = "player_surrendered";
|
||
|
||
console.log(`[GameInstance ${this.id}] Игра ${this.id} завершена из-за сдачи игрока ${surrenderedPlayerName} (роль: ${surrenderedPlayerRole}). Победитель: ${winnerName} (роль: ${winnerRole}).`);
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: winnerRole,
|
||
reason: reasonForGameOver,
|
||
finalGameState: this.gameState,
|
||
log: this.consumeLogBuffer(),
|
||
loserCharacterKey: surrenderedPlayerCharKey
|
||
});
|
||
|
||
// Вызываем cleanup в GameManager, чтобы удалить игру из активных списков
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, reasonForGameOver);
|
||
} else {
|
||
console.error(`[GameInstance ${this.id}] CRITICAL: gameManager or _cleanupGame method not found after surrender!`);
|
||
}
|
||
}
|
||
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||
|
||
handleTurnTimeout() {
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
|
||
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 result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerPlayerRole, timedOutPlayerRole);
|
||
|
||
if (!this.gameState[winnerPlayerRole]?.characterKey) {
|
||
this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`);
|
||
return;
|
||
}
|
||
this.gameState.isGameOver = true;
|
||
this.clearAllReconnectTimers();
|
||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
console.log(`[GameInstance ${this.id}] Таймаут хода для ${this.gameState[timedOutPlayerRole]?.name}. Победитель: ${this.gameState[winnerPlayerRole]?.name}.`);
|
||
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, result.reason);
|
||
}
|
||
|
||
_handleCriticalError(reasonCode, logMessage) {
|
||
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`);
|
||
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
||
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'
|
||
});
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, `critical_error_${reasonCode}`);
|
||
}
|
||
}
|
||
|
||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||
if (!message) return;
|
||
const logEntry = { message, type, timestamp: Date.now() };
|
||
this.logBuffer.push(logEntry);
|
||
}
|
||
consumeLogBuffer() {
|
||
const logs = [...this.logBuffer];
|
||
this.logBuffer = [];
|
||
return logs;
|
||
}
|
||
broadcastGameStateUpdate() {
|
||
if (this.isGameEffectivelyPaused()) {
|
||
console.log(`[GameInstance ${this.id}] Попытка broadcastGameStateUpdate, но игра на паузе. Обновление не отправлено.`);
|
||
return;
|
||
}
|
||
if (!this.gameState) return;
|
||
const logsToSend = this.consumeLogBuffer();
|
||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: logsToSend });
|
||
}
|
||
broadcastLogUpdate() {
|
||
if (this.logBuffer.length > 0) {
|
||
const logsToSend = this.consumeLogBuffer();
|
||
this.io.to(this.id).emit('logUpdate', { log: logsToSend });
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = GameInstance; |