bc/server/game/instance/GameInstance.js

474 lines
28 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'); // Импортирует index.js из папки logic
const dataUtils = require('../../data/dataUtils');
const GAME_CONFIG = require('../../core/config'); // <--- УБЕДИТЕСЬ, ЧТО GAME_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.turnTimer = new TurnTimer(
GAME_CONFIG.TURN_DURATION_MS,
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
() => this.handleTurnTimeout(),
(remainingTime, isPlayerTurnForTimer) => {
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) {
if (this.players[socket.id]) {
socket.emit('gameError', { message: 'Ваш сокет уже зарегистрирован в этой игре.' });
return false;
}
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
if (existingPlayerByIdentifier) {
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 (this.playerCount === 0) {
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
this.ownerIdentifier = identifier;
} else {
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
actualCharacterKey = (firstPlayerInfo?.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
}
}
this.players[socket.id] = {
id: assignedPlayerId, socket: socket,
chosenCharacterKey: actualCharacterKey, identifier: identifier
};
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) {
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id;
console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}) покинул игру.`);
if (playerInfo.socket) { try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } }
delete this.players[socketId];
this.playerCount--;
if (this.playerSockets[playerRole]?.id === socketId) {
delete this.playerSockets[playerRole];
}
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();
}
}
}
initializeGame() {
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Игроков: ${this.playerCount}.`);
if (this.mode === 'ai' && this.playerCount === 1) {
this.playerCharacterKey = 'elena'; this.opponentCharacterKey = 'balard';
} else if (this.mode === 'pvp' && this.playerCount === 2) {
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
} else if (this.mode === 'pvp' && this.playerCount === 1) {
const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena';
this.opponentCharacterKey = null;
} else {
console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации!`); return false;
}
const playerData = dataUtils.getCharacterData(this.playerCharacterKey);
let opponentData = null;
const isOpponentDefined = !!this.opponentCharacterKey;
if (isOpponentDefined) opponentData = dataUtils.getCharacterData(this.opponentCharacterKey);
if (!playerData || (isOpponentDefined && !opponentData)) {
this._handleCriticalError('init_char_data_fail', 'Ошибка загрузки данных персонажей при инициализации.');
return false;
}
if (isOpponentDefined && (!opponentData.baseStats.maxHp || opponentData.baseStats.maxHp <= 0)) {
this._handleCriticalError('init_opponent_hp_fail', 'Некорректные HP оппонента при инициализации.');
return false;
}
this.gameState = {
player: this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities),
opponent: isOpponentDefined ?
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока...', 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}`);
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 => { // Добавлена проверка abilities
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.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; }
Object.values(this.players).forEach(playerInfo => {
if (playerInfo.socket?.connected) {
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: this.consumeLogBuffer(), 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.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()) {
this.broadcastGameStateUpdate(); return;
}
if (actionValid) {
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.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()) { this.broadcastGameStateUpdate(); 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.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
if (this.gameState && !this.gameState.isGameOver) this.switchTurn();
return;
}
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()) { this.broadcastGameStateUpdate(); return; }
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);
} // 'pass' уже залогирован в decideAiAction
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
}
checkGameOver() {
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
if (!this.gameState.player || !this.gameState.opponent?.characterKey) return false;
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
if (gameOverResult.isOver) {
this.gameState.isGameOver = true;
this.turnTimer.clear();
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(disconnectedSocketId, disconnectedPlayerRole, disconnectedCharacterKey) {
if (this.gameState && !this.gameState.isGameOver) {
this.gameState.isGameOver = true;
this.turnTimer.clear();
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'opponent_disconnected',
disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID, // winner
disconnectedPlayerRole // loser
);
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта. Победитель: ${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);
}
}
handleTurnTimeout() {
if (!this.gameState || this.gameState.isGameOver) return;
// this.turnTimer.clear(); // TurnTimer сам себя очистит при вызове onTimeout
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) { // Если победитель не определен (например, ожидание в PvP)
this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`);
return;
}
this.gameState.isGameOver = true; // Устанавливаем здесь, т.к. getGameOverResult мог не знать, что игра уже окончена
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.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;
this.logBuffer.push({ message, type, timestamp: Date.now() });
}
consumeLogBuffer() {
const logs = [...this.logBuffer]; this.logBuffer = []; return logs;
}
broadcastGameStateUpdate() {
if (!this.gameState) return;
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
}
broadcastLogUpdate() {
if (this.logBuffer.length > 0) {
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
}
}
}
module.exports = GameInstance;