474 lines
28 KiB
JavaScript
474 lines
28 KiB
JavaScript
// /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; |