bc/server/game/instance/GameInstance.js

824 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /server/game/instance/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;