bc/server/game/instance/GameInstance.js

763 lines
48 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'); // Убедитесь, что это новый TurnTimer.js
const gameLogic = require('../logic');
const dataUtils = require('../../data/dataUtils');
const GAME_CONFIG = require('../../core/config');
const PlayerConnectionHandler = require('./PlayerConnectionHandler');
class GameInstance {
constructor(gameId, io, mode = 'ai', gameManager) {
this.id = gameId;
this.io = io;
this.mode = mode;
this.gameManager = gameManager;
this.playerConnectionHandler = new PlayerConnectionHandler(this);
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.playerCharacterKey = null;
this.opponentCharacterKey = null;
this.ownerIdentifier = null;
this.turnTimer = new TurnTimer(
GAME_CONFIG.TURN_DURATION_MS,
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
() => this.handleTurnTimeout(),
// onTickCallback: (remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic)
(remainingTime, isPlayerTurnForTimer, isTimerLogicPaused) => {
const socketsInRoom = Array.from(this.io.sockets.adapter.rooms.get(this.id) || []);
console.log(`[GI TURN_TIMER_UPDATE_CB ${this.id}] Called! To room ${this.id} (sockets: ${socketsInRoom.join(', ')}). Remaining: ${remainingTime}, isPlayerT_forTimer: ${isPlayerTurnForTimer}, isTimerLogicPaused: ${isTimerLogicPaused}, isGameEffectivelyPaused(GI): ${this.isGameEffectivelyPaused()}`);
this.io.to(this.id).emit('turnTimerUpdate', {
remainingTime,
isPlayerTurn: isPlayerTurnForTimer, // Чей ход с точки зрения таймера
isPaused: isTimerLogicPaused || this.isGameEffectivelyPaused() // Общая пауза
});
},
this.id // gameIdForLogs
);
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: Ссылка на GameManager недействительна.`);
}
console.log(`[GameInstance ${this.id}] Создан. Режим: ${mode}. PlayerConnectionHandler также инициализирован.`);
}
get playerCount() {
return this.playerConnectionHandler.playerCount;
}
get players() {
return this.playerConnectionHandler.getAllPlayersInfo();
}
setPlayerCharacterKey(key) { this.playerCharacterKey = key; }
setOpponentCharacterKey(key) { this.opponentCharacterKey = key; }
setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; }
addPlayer(socket, chosenCharacterKey, identifier) {
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
}
removePlayer(socketId, reason) { // Вызывается из PCH
// PCH сам обрабатывает удаление, GameInstance реагирует через handlePlayerPermanentlyLeft
}
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId);
}
handlePlayerReconnected(playerIdRole, newSocket) {
console.log(`[GameInstance ${this.id}] Делегирование handlePlayerReconnected в PCH для роли ${playerIdRole}, сокет ${newSocket.id}`);
return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket);
}
clearAllReconnectTimers() {
this.playerConnectionHandler.clearAllReconnectTimers();
}
isGameEffectivelyPaused() { // Определяет, приостановлена ли игра из-за дисконнектов
return this.playerConnectionHandler.isGameEffectivelyPaused();
}
handlePlayerPermanentlyLeft(playerRole, characterKey, reason) {
console.log(`[GameInstance ${this.id}] Игрок окончательно покинул игру. Роль: ${playerRole}, Персонаж: ${characterKey}, Причина: ${reason}`);
if (this.gameState && !this.gameState.isGameOver) {
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
} else if (this.mode === 'pvp') {
// playerCount уже должен быть обновлен в PCH
if (this.playerCount < 2) {
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
}
}
} else if (!this.gameState && Object.keys(this.players).length === 0) {
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch");
}
}
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
if (!characterState || !characterState.characterKey) return;
if (!opponentCharacterKey) return;
if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt недоступен!`); return; }
if (!this.gameState) return;
let context = {};
let subTrigger = null;
if (typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number') {
subTrigger = subTriggerOrContext;
} else if (typeof subTriggerOrContext === 'object' && subTriggerOrContext !== null) {
context = { ...subTriggerOrContext };
}
context = { ...context, ...contextOverrides };
if ((triggerType === 'selfCastAbility' || triggerType === 'onOpponentAction') &&
(typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number')) {
context.abilityId = subTriggerOrContext;
subTrigger = subTriggerOrContext;
} else if (triggerType === 'onBattleState' && typeof subTriggerOrContext === 'string') {
subTrigger = subTriggerOrContext;
} else if (triggerType === 'basicAttack' && typeof subTriggerOrContext === 'string') {
subTrigger = subTriggerOrContext;
}
const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey);
if (!opponentFullData) return;
const tauntText = gameLogic.getRandomTaunt(
characterState.characterKey,
triggerType,
subTrigger || context,
GAME_CONFIG,
opponentFullData,
this.gameState
);
if (tauntText && tauntText !== "(Молчание)") {
this.addToLog(`${characterState.name}: "${tauntText}"`, GAME_CONFIG.LOG_TYPE_INFO);
}
}
initializeGame() {
console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков (PCH): ${this.playerCount}. Всего записей в PCH.players: ${Object.keys(this.players).length}. PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`);
const p1ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
const p2ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
if (p1ActiveEntry && !this.playerCharacterKey) this.playerCharacterKey = p1ActiveEntry.chosenCharacterKey;
if (p2ActiveEntry && !this.opponentCharacterKey && this.mode === 'pvp') this.opponentCharacterKey = p2ActiveEntry.chosenCharacterKey;
if (this.mode === 'ai') {
if (!p1ActiveEntry) { this._handleCriticalError('init_ai_no_active_player_gi', 'Инициализация AI игры: Игрок-человек не найден или не активен.'); return false; }
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'Инициализация AI игры: Ключ персонажа игрока не установлен.'); return false;}
if (!this.opponentCharacterKey) this.opponentCharacterKey = 'balard'; // Устанавливаем AI, если еще не установлен
} else { // pvp
if (this.playerCount === 1 && p1ActiveEntry && !this.playerCharacterKey) {
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP инициализация (1 игрок): Ключ персонажа игрока отсутствует.'); return false;
}
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
this._handleCriticalError('init_pvp_char_key_missing_gi', `Инициализация PvP: playerCount=2, но ключ персонажа отсутствует. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
return false;
}
}
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
const isPlayerSlotFilledAndActive = !!(playerData && p1ActiveEntry);
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2ActiveEntry));
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !opponentData) ) {
this._handleCriticalError('init_ai_data_fail_gs_gi', 'Инициализация AI игры: Не удалось загрузить данные игрока или AI для gameState.'); return false;
}
this.logBuffer = [];
const playerName = playerData?.baseStats?.name || (p1ActiveEntry?.name || 'Ожидание Игрока 1...');
let opponentName;
if (this.mode === 'ai') {
opponentName = opponentData?.baseStats?.name || 'Противник AI';
} else {
opponentName = opponentData?.baseStats?.name || (p2ActiveEntry?.name || 'Ожидание Игрока 2...');
}
this.gameState = {
player: isPlayerSlotFilledAndActive ?
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities, playerName) :
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: playerName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], playerName),
opponent: isOpponentSlotFilledAndActive ?
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities, opponentName) :
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: opponentName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], opponentName),
isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true,
isGameOver: false,
turnNumber: 1,
gameMode: this.mode
};
console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Игрок: ${this.gameState.player.name} (${this.gameState.player.characterKey}). Оппонент: ${this.gameState.opponent.name} (${this.gameState.opponent.characterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}. Готово к старту: AI=${isPlayerSlotFilledAndActive && !!opponentData}, PvP1=${isPlayerSlotFilledAndActive}, PvP2=${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && !!opponentData) : isPlayerSlotFilledAndActive;
}
_createFighterState(roleId, baseStats, abilities, explicitName = null) {
const fighterState = {
id: roleId, characterKey: baseStats.characterKey, name: explicitName || 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() {
console.log(`[GameInstance ${this.id}] Попытка запуска игры. Paused: ${this.isGameEffectivelyPaused()}`);
if (this.isGameEffectivelyPaused()) {
console.log(`[GameInstance ${this.id}] Запуск игры отложен: игра на паузе.`);
return;
}
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
console.warn(`[GameInstance ${this.id}] startGame: gameState или ключи персонажей не полностью инициализированы. Попытка повторной инициализации.`);
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) { // initializeGame сама установит gameState
this._handleCriticalError('start_game_reinit_failed_sg_gi', 'Повторная инициализация перед стартом не удалась или ключи все еще отсутствуют в gameState.');
return;
}
}
console.log(`[GameInstance ${this.id}] Запуск игры. Игрок в GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Оппонент в GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}`);
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
if (!pData || !oData) {
this._handleCriticalError('start_char_data_fail_sg_gi', `Не удалось загрузить данные персонажей при старте игры. PData: ${!!pData}, OData: ${!!oData}`);
return;
}
if (this.gameState.player && pData?.baseStats?.name) this.gameState.player.name = pData.baseStats.name;
if (this.gameState.opponent && oData?.baseStats?.name) this.gameState.opponent.name = oData.baseStats.name;
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
} else {
console.warn(`[GameInstance ${this.id}] Не удалось произнести стартовые насмешки во время startGame, gameState акторы/ключи не полностью готовы.`);
}
const initialLog = this.consumeLogBuffer();
Object.values(this.players).forEach(playerInfo => {
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
const dataForThisClient = 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,
...dataForThisClient,
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();
const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
console.log(`[GameInstance ${this.id}] Запуск таймера в startGame. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isFirstTurnAi}`);
this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi);
if (isFirstTurnAi) {
setTimeout(() => {
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) {
this.processAiTurn();
}
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
}
}
processPlayerAction(identifier, actionData) {
console.log(`[GameInstance ${this.id}] processPlayerAction от ${identifier}. Действие: ${actionData.actionType}. Текущий GS.isPlayerTurn: ${this.gameState?.isPlayerTurn}. Paused: ${this.isGameEffectivelyPaused()}`);
const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier);
if (!actingPlayerInfo || !actingPlayerInfo.socket) {
console.error(`[GameInstance ${this.id}] Действие от неизвестного или безсокетного идентификатора ${identifier}.`); return;
}
if (this.isGameEffectivelyPaused()) {
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
return;
}
if (!this.gameState || this.gameState.isGameOver) {
console.warn(`[GameInstance ${this.id}] processPlayerAction: Действие от ${identifier} проигнорировано (нет gameState или игра завершена). GameOver: ${this.gameState?.isGameOver}`);
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}] Неверный ход! Игрок ${identifier} (роль ${actingPlayerRole}) пытался действовать. GS.isPlayerTurn: ${this.gameState.isPlayerTurn}`);
actingPlayerInfo.socket.emit('gameError', { message: "Не ваш ход." });
return;
}
console.log(`[GameInstance ${this.id}] Ход корректен для ${identifier}. Очистка таймера.`);
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) { // Очищаем, даже если на паузе, т.к. действие совершено
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];
if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) {
this._handleCriticalError('action_actor_state_invalid_gi', `Состояние/ключ Атакующего или Защитника недействительны.`); return;
}
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi', 'Ошибка данных персонажа при действии.'); return; }
let actionIsValidAndPerformed = false;
if (actionData.actionType === 'attack') {
this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack');
gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
actionIsValidAndPerformed = true;
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
if (!ability) {
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
} else {
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
if (validityCheck.isValid) {
this._sayTaunt(attackerState, defenderState.characterKey, 'selfCastAbility', ability.id);
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null);
gameLogic.setAbilityCooldown(ability, attackerState, GAME_CONFIG);
actionIsValidAndPerformed = true;
} else {
this.addToLog(validityCheck.reason || `${attackerState.name} не может использовать "${ability.name}".`, GAME_CONFIG.LOG_TYPE_INFO);
actionIsValidAndPerformed = false;
}
}
} else {
actionIsValidAndPerformed = false;
}
if (this.checkGameOver()) return;
this.broadcastLogUpdate(); // Отправляем лог сразу после действия
if (actionIsValidAndPerformed) {
// Небольшая задержка перед сменой хода, чтобы клиент успел увидеть результат действия
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
} else {
// Если действие было невалидным, перезапускаем таймер для текущего игрока
const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn;
console.log(`[GameInstance ${this.id}] Действие не выполнено, перезапуск таймера для ${identifier}. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isAiTurnForTimer}`);
this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer);
}
}
switchTurn() {
console.log(`[GameInstance ${this.id}] Попытка смены хода. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}`);
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Смена хода отложена: игра на паузе.`); return; }
if (!this.gameState || this.gameState.isGameOver) { return; }
// Таймер хода должен быть уже очищен в processPlayerAction или processAiTurn
// Но на всякий случай, если switchTurn вызван из другого места (например, после эффектов)
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) {
this.turnTimer.clear();
}
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const endingTurnActorState = this.gameState[endingTurnActorRole];
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Состояние или ключ актора, завершающего ход, недействительны.`); return; }
const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey);
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Отсутствуют данные персонажа.`); return; }
gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils);
gameLogic.updateBlockingStatus(endingTurnActorState);
if (endingTurnActorState.abilityCooldowns && endingTurnActorData.abilities) gameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnActorData.abilities, endingTurnActorState.name, this.addToLog.bind(this), GAME_CONFIG);
if (endingTurnActorState.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActorState);
if (endingTurnActorState.disabledAbilities?.length > 0 && endingTurnActorData.abilities) gameLogic.processDisabledAbilities(endingTurnActorState.disabledAbilities, endingTurnActorData.abilities, endingTurnActorState.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 currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const currentTurnActorState = this.gameState[currentTurnActorRole];
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Состояние или имя текущего актора недействительны.`); return; }
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate(); // Отправляем обновленное состояние и все накопленные логи
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
console.log(`[GameInstance ${this.id}] Ход перешел к ${currentTurnActorRole}, но игрок ${currentTurnPlayerEntry.identifier} отключен. Таймер не запущен switchTurn.`);
} else {
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
console.log(`[GameInstance ${this.id}] Запуск таймера в switchTurn. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isNextTurnAi}`);
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
if (isNextTurnAi) {
setTimeout(() => {
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) {
this.processAiTurn();
}
}, GAME_CONFIG.DELAY_OPPONENT_TURN);
}
}
}
processAiTurn() {
console.log(`[GameInstance ${this.id}] processAiTurn. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}, IsPlayerTurn(GS): ${this.gameState?.isPlayerTurn}`);
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Ход AI отложен: игра на паузе.`); return; }
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) {
console.error(`[GameInstance ${this.id}] AI не Балард! Персонаж AI: ${this.gameState.opponent?.characterKey}. Принудительная смена хода.`);
this.switchTurn();
return;
}
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) { // Очищаем таймер, так как AI сейчас сделает ход
this.turnTimer.clear();
}
const aiState = this.gameState.opponent;
const playerState = this.gameState.player;
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Состояние игрока недействительно для хода AI.'); return; }
const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this));
let actionIsValidAndPerformedForAI = false;
if (aiDecision.actionType === 'attack') {
this._sayTaunt(aiState, playerState.characterKey, 'basicAttack');
gameLogic.performAttack(aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
actionIsValidAndPerformedForAI = true;
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
this._sayTaunt(aiState, playerState.characterKey, 'selfCastAbility', aiDecision.ability.id);
aiState.currentResource = Math.round(aiState.currentResource - aiDecision.ability.cost);
gameLogic.applyAbilityEffect(aiDecision.ability, aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null);
gameLogic.setAbilityCooldown(aiDecision.ability, aiState, GAME_CONFIG);
actionIsValidAndPerformedForAI = true;
} else if (aiDecision.actionType === 'pass') {
if (aiDecision.logMessage && this.addToLog) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
else if(this.addToLog) this.addToLog(`${aiState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
actionIsValidAndPerformedForAI = true;
}
if (this.checkGameOver()) return;
this.broadcastLogUpdate(); // Отправляем лог после действия AI
if (actionIsValidAndPerformedForAI) {
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
} else {
console.error(`[GameInstance ${this.id}] AI не смог выполнить действие. Принудительная смена хода.`);
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
}
}
checkGameOver() {
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
const player = this.gameState.player; const opponent = this.gameState.opponent;
const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey);
if (pData && oData) {
const nearDefeatThreshold = GAME_CONFIG.OPPONENT_NEAR_DEFEAT_THRESHOLD_PERCENT || 0.2;
if (opponent.currentHp > 0 && (opponent.currentHp / oData.baseStats.maxHp) <= nearDefeatThreshold) {
this._sayTaunt(player, opponent.characterKey, 'onBattleState', 'opponentNearDefeat');
}
if (player.currentHp > 0 && (player.currentHp / pData.baseStats.maxHp) <= nearDefeatThreshold) {
this._sayTaunt(opponent, player.characterKey, 'onBattleState', 'opponentNearDefeat');
}
}
}
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
if (gameOverResult.isOver) {
this.gameState.isGameOver = true;
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear(); // Очищаем таймер, если игра окончена
this.clearAllReconnectTimers();
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
const winnerState = this.gameState[gameOverResult.winnerRole];
const loserState = this.gameState[gameOverResult.loserRole];
if (winnerState?.characterKey && loserState?.characterKey) {
this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat');
}
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, `game_ended_${gameOverResult.reason}`);
return true;
}
return false;
}
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
if (this.gameState && !this.gameState.isGameOver) {
this.gameState.isGameOver = true;
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
this.clearAllReconnectTimers();
let actualWinnerRole = winnerIfAny;
let winnerActuallyExists = false;
if (actualWinnerRole) {
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else if (winnerPlayerEntry) {
winnerActuallyExists = true;
}
}
if (!winnerActuallyExists) {
actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else if (defaultWinnerEntry) {
winnerActuallyExists = true;
}
}
const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null;
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole);
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[GameInstance ${this.id}] Игра завершена из-за отключения: ${reason}. Победитель: ${result.winnerRole || 'Нет'}.`);
this.io.to(this.id).emit('gameOver', {
winnerId: result.winnerRole,
reason: result.reason,
finalGameState: this.gameState,
log: this.consumeLogBuffer(),
loserCharacterKey: disconnectedCharacterKey,
disconnectedCharacterName: (reason === 'opponent_disconnected' || reason === 'player_left_ai_game' || reason === 'opponent_left_pvp_game') ?
(this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined
});
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${result.reason}`);
} else if (this.gameState?.isGameOver) {
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: игра уже была завершена.`);
this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup_gi`);
} else {
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: нет gameState.`);
this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`);
}
}
playerExplicitlyLeftAiGame(identifier) {
if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) {
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame вызван, но не режим AI или игра завершена.`);
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`);
return;
}
const playerEntry = Object.values(this.players).find(p => p.identifier === identifier);
if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) {
console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Идентификатор ${identifier} не является игроком-человеком или не найден.`);
return;
}
console.log(`[GameInstance ${this.id}] Игрок ${identifier} явно покинул AI игру.`);
if (this.gameState) {
this.gameState.isGameOver = true;
this.addToLog(`Игрок покинул битву с ${this.gameState.opponent?.name || 'AI'}.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
} else {
this.addToLog(`Игрок покинул AI игру до ее полного начала.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
}
if (this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.io.to(this.id).emit('gameOver', {
winnerId: GAME_CONFIG.OPPONENT_ID,
reason: "player_left_ai_game",
finalGameState: this.gameState,
log: this.consumeLogBuffer(),
loserCharacterKey: playerEntry.chosenCharacterKey
});
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi');
}
playerDidSurrender(surrenderingPlayerIdentifier) {
console.log(`[GameInstance ${this.id}] playerDidSurrender вызван для идентификатора: ${surrenderingPlayerIdentifier}`);
if (!this.gameState || this.gameState.isGameOver) {
if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished_gi`); }
console.warn(`[GameInstance ${this.id}] Попытка сдачи в неактивной/завершенной игре от ${surrenderingPlayerIdentifier}.`);
return;
}
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
if (!surrenderedPlayerEntry) {
console.error(`[GameInstance ${this.id}] Сдающийся игрок ${surrenderingPlayerIdentifier} не найден.`);
return;
}
const surrenderingPlayerRole = surrenderedPlayerEntry.id;
if (this.mode === 'ai') {
if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) {
console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} "сдался" (покинул) AI игру.`);
this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier);
} else {
console.warn(`[GameInstance ${this.id}] Сдача в AI режиме от не-игрока (роль: ${surrenderingPlayerRole}). Игнорируется.`);
}
return;
}
if (this.mode !== 'pvp') {
console.warn(`[GameInstance ${this.id}] Сдача вызвана в не-PvP, не-AI режиме: ${this.mode}. Игнорируется.`);
return;
}
const surrenderedPlayerName = this.gameState[surrenderingPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey;
const surrenderedPlayerCharKey = this.gameState[surrenderingPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey;
const winnerRole = surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const winnerName = this.gameState[winnerRole]?.name || `Оппонент`;
const winnerCharKey = this.gameState[winnerRole]?.characterKey;
this.gameState.isGameOver = true;
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[GameInstance ${this.id}] Игрок ${surrenderedPlayerName} (Роль: ${surrenderingPlayerRole}) сдался. Победитель: ${winnerName} (Роль: ${winnerRole}).`);
if (winnerCharKey && surrenderedPlayerCharKey && this.gameState[winnerRole]) {
this._sayTaunt(this.gameState[winnerRole], surrenderedPlayerCharKey, 'onBattleState', 'opponentNearDefeat');
}
this.io.to(this.id).emit('gameOver', {
winnerId: winnerRole, reason: "player_surrendered",
finalGameState: this.gameState, log: this.consumeLogBuffer(),
loserCharacterKey: surrenderedPlayerCharKey
});
this.gameManager._cleanupGame(this.id, "player_surrendered_gi");
}
handleTurnTimeout() {
if (!this.gameState || this.gameState.isGameOver) return;
console.log(`[GameInstance ${this.id}] Произошел таймаут хода (вызван из TurnTimer).`);
const timedOutPlayerRole = this.turnTimer.isConfiguredForPlayerSlotTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
// Используем isConfiguredForPlayerSlotTurn из таймера, т.к. gameState.isPlayerTurn мог измениться до фактического вызова этого коллбэка
// или если таймаут произошел во время "думания" AI (хотя таймер AI не должен вызывать этот коллбэк для игрока).
const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
let winnerActuallyExists = false;
if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else {
const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected);
winnerActuallyExists = !!winnerEntry;
}
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole);
this.gameState.isGameOver = true;
// turnTimer.clear() уже должен был быть вызван внутри TurnTimer перед onTimeoutCallback, или будет вызван в checkGameOver
this.clearAllReconnectTimers();
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) {
this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat');
}
console.log(`[GameInstance ${this.id}] Ход истек для ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Победитель: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`);
this.io.to(this.id).emit('gameOver', {
winnerId: result.winnerRole,
reason: result.reason,
finalGameState: this.gameState,
log: this.consumeLogBuffer(),
loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown'
});
this.gameManager._cleanupGame(this.id, `timeout_gi_${result.reason}`);
}
_handleCriticalError(reasonCode, logMessage) {
console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`);
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
else if (!this.gameState) {
this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
}
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('gameOver', {
winnerId: null,
reason: `server_error_${reasonCode}`,
finalGameState: this.gameState,
log: this.consumeLogBuffer(),
loserCharacterKey: 'unknown'
});
this.gameManager._cleanupGame(this.id, `critical_error_gi_${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.isGameEffectivelyPaused()) {
console.log(`[GameInstance ${this.id}] broadcastGameStateUpdate отложено: игра на паузе.`);
return;
}
if (!this.gameState) {
console.warn(`[GameInstance ${this.id}] broadcastGameStateUpdate: gameState отсутствует.`);
return;
}
console.log(`[GameInstance ${this.id}] Отправка gameStateUpdate. IsPlayerTurn(GS): ${this.gameState.isPlayerTurn}`);
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
}
broadcastLogUpdate() {
if (this.isGameEffectivelyPaused() && this.logBuffer.some(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM)) {
const systemLogs = this.logBuffer.filter(log => log.type === GAME_CONFIG.LOG_TYPE_SYSTEM);
if (systemLogs.length > 0) {
this.io.to(this.id).emit('logUpdate', { log: systemLogs });
}
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM);
return;
}
if (this.logBuffer.length > 0) {
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
}
}
}
module.exports = GameInstance;