788 lines
54 KiB
JavaScript
788 lines
54 KiB
JavaScript
// /server_modules/gameInstance.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const gameData = require('./data');
|
||
const GAME_CONFIG = require('./config');
|
||
const serverGameLogic = require('./gameLogic');
|
||
|
||
class GameInstance {
|
||
constructor(gameId, io, mode = 'ai', gameManager) {
|
||
this.id = gameId;
|
||
this.io = io;
|
||
this.mode = mode;
|
||
this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest', identifier: userId|socketId } }
|
||
this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject }
|
||
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.turnTimerId = null; // ID для setTimeout (обработка таймаута)
|
||
this.turnTimerUpdateIntervalId = null; // ID для setInterval (обновление клиента)
|
||
this.turnStartTime = 0; // Время начала текущего хода (Date.now())
|
||
|
||
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: GameInstance created without valid GameManager reference! Cleanup will fail.`);
|
||
}
|
||
}
|
||
|
||
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'; // В AI режиме игрок всегда Елена
|
||
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 characterData = this._getCharacterBaseData(actualCharacterKey);
|
||
console.log(`[Game ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerIdentifier || 'N/A'}`);
|
||
return true;
|
||
}
|
||
|
||
removePlayer(socketId) {
|
||
const playerInfo = this.players[socketId];
|
||
if (playerInfo) {
|
||
const playerRole = playerInfo.id;
|
||
const identifierOfLeavingPlayer = playerInfo.identifier;
|
||
const characterKeyOfLeavingPlayer = playerInfo.chosenCharacterKey;
|
||
const characterData = this._getCharacterBaseData(characterKeyOfLeavingPlayer);
|
||
|
||
console.log(`[Game ${this.id}] Игрок ${identifierOfLeavingPlayer} (сокет: ${socketId}, роль: ${playerRole}, персонаж: ${characterKeyOfLeavingPlayer || 'N/A'}) покинул игру.`);
|
||
|
||
if (playerInfo.socket) {
|
||
const actualSocket = this.io.sockets.sockets.get(socketId);
|
||
if (actualSocket && actualSocket.id === socketId) {
|
||
actualSocket.leave(this.id);
|
||
} else if (playerInfo.socket.id === socketId) {
|
||
try { playerInfo.socket.leave(this.id); } catch (e) { console.warn(`[Game ${this.id}] Error leaving room for old socket ${socketId}: ${e.message}`); }
|
||
}
|
||
}
|
||
|
||
delete this.players[socketId];
|
||
this.playerCount--;
|
||
|
||
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
|
||
delete this.playerSockets[playerRole];
|
||
}
|
||
|
||
// Если игра не окончена и это был ход отключившегося игрока, очищаем таймер
|
||
if (this.gameState && !this.gameState.isGameOver) {
|
||
const isTurnOfDisconnectedPlayer = (this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.PLAYER_ID) ||
|
||
(!this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.OPPONENT_ID);
|
||
if (isTurnOfDisconnectedPlayer) {
|
||
this.clearTurnTimer();
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`[Game ${this.id}] removePlayer called for unknown socketId ${socketId}.`);
|
||
}
|
||
}
|
||
|
||
initializeGame() {
|
||
console.log(`[Game ${this.id}] Initializing game state. Mode: ${this.mode}. Current PlayerCount: ${this.playerCount}.`);
|
||
if (this.mode === 'ai' && this.playerCount === 1) {
|
||
this.playerCharacterKey = 'elena';
|
||
this.opponentCharacterKey = 'balard';
|
||
} else if (this.mode === 'pvp' && this.playerCount === 2) {
|
||
const player1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
const player2Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
this.playerCharacterKey = player1Info?.chosenCharacterKey || 'elena';
|
||
const expectedOpponentKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
|
||
if (player2Info && player2Info.chosenCharacterKey !== expectedOpponentKey) {
|
||
console.warn(`[Game ${this.id}] initializeGame: Expected opponent character ${expectedOpponentKey} but player2Info had ${player2Info.chosenCharacterKey}.`);
|
||
}
|
||
this.opponentCharacterKey = expectedOpponentKey;
|
||
} else if (this.mode === 'pvp' && this.playerCount === 1) {
|
||
const player1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
this.playerCharacterKey = player1Info?.chosenCharacterKey || 'elena';
|
||
this.opponentCharacterKey = null;
|
||
} else {
|
||
console.error(`[Game ${this.id}] Unexpected state for initialization! Mode: ${this.mode}, PlayerCount: ${this.playerCount}. Cannot initialize gameState.`);
|
||
this.gameState = null;
|
||
return false;
|
||
}
|
||
|
||
console.log(`[Game ${this.id}] Finalizing characters for gameState - Player Slot ('${GAME_CONFIG.PLAYER_ID}'): ${this.playerCharacterKey}, Opponent Slot ('${GAME_CONFIG.OPPONENT_ID}'): ${this.opponentCharacterKey || 'N/A (Waiting)'}`);
|
||
const playerBase = this._getCharacterBaseData(this.playerCharacterKey);
|
||
const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey);
|
||
let opponentBase = null;
|
||
let opponentAbilities = null;
|
||
const isOpponentDefined = !!this.opponentCharacterKey;
|
||
|
||
if (isOpponentDefined) {
|
||
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
|
||
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
|
||
}
|
||
|
||
if (!playerBase || !playerAbilities || (isOpponentDefined && (!opponentBase || !opponentAbilities))) {
|
||
const errorMsg = `[Game ${this.id}] CRITICAL ERROR: initializeGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}.`;
|
||
console.error(errorMsg);
|
||
this.logBuffer = [];
|
||
this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
Object.values(this.players).forEach(p => p.socket.emit('gameError', { message: 'Критическая ошибка сервера: не удалось загрузить данные персонажей.' }));
|
||
this.gameState = null;
|
||
return false;
|
||
}
|
||
if (isOpponentDefined && (!opponentBase.maxHp || opponentBase.maxHp <= 0)) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: initializeGame - Opponent has invalid maxHp (${opponentBase.maxHp}) for key ${this.opponentCharacterKey}.`);
|
||
this.logBuffer = [];
|
||
this.addToLog('Критическая ошибка сервера: некорректные данные оппонента!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
Object.values(this.players).forEach(p => p.socket.emit('gameError', { message: 'Критическая ошибка сервера: некорректные данные персонажа оппонента.' }));
|
||
this.gameState = null;
|
||
return false;
|
||
}
|
||
|
||
|
||
this.gameState = {
|
||
player: {
|
||
id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name,
|
||
currentHp: playerBase.maxHp, maxHp: playerBase.maxHp,
|
||
currentResource: playerBase.maxResource, maxResource: playerBase.maxResource,
|
||
resourceName: playerBase.resourceName, attackPower: playerBase.attackPower,
|
||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
||
},
|
||
opponent: {
|
||
id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey,
|
||
name: opponentBase?.name || 'Ожидание игрока...',
|
||
currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1,
|
||
currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0,
|
||
resourceName: opponentBase?.resourceName || 'Ресурс', attackPower: opponentBase?.attackPower || 0,
|
||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {},
|
||
silenceCooldownTurns: (this.opponentCharacterKey === 'balard') ? 0 : undefined,
|
||
manaDrainCooldownTurns: (this.opponentCharacterKey === 'balard') ? 0 : undefined,
|
||
},
|
||
isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true,
|
||
isGameOver: false,
|
||
turnNumber: 1,
|
||
gameMode: this.mode
|
||
};
|
||
|
||
if (playerAbilities) {
|
||
playerAbilities.forEach(ability => {
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
this.gameState.player.abilityCooldowns[ability.id] = 0;
|
||
}
|
||
});
|
||
}
|
||
if (isOpponentDefined && opponentAbilities) {
|
||
opponentAbilities.forEach(ability => {
|
||
let cd = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) cd = ability.cooldown;
|
||
if (this.opponentCharacterKey === 'balard') {
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') cd = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;
|
||
else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') cd = ability.internalCooldownValue;
|
||
}
|
||
if (cd > 0) this.gameState.opponent.abilityCooldowns[ability.id] = 0;
|
||
});
|
||
}
|
||
|
||
if (isOpponentDefined) {
|
||
const isRestart = this.logBuffer.length > 0;
|
||
this.logBuffer = [];
|
||
this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
const playerCharKey = this.gameState.player.characterKey;
|
||
if (playerCharKey === 'elena' || playerCharKey === 'almagest') {
|
||
const startTaunt = serverGameLogic.getRandomTaunt(playerCharKey, 'battleStart', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState?.isGameOver}. First turn (if ready): ${this.gameState?.isPlayerTurn ? this.gameState?.player?.name : (this.gameState?.opponent?.name || 'Оппонент')}. Opponent Defined (Ready for Start): ${isOpponentDefined}`);
|
||
return isOpponentDefined;
|
||
}
|
||
|
||
startGame() {
|
||
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание игрока...' || !this.gameState.opponent.maxHp || this.gameState.opponent.maxHp <= 0) {
|
||
console.error(`[Game ${this.id}] startGame: Game state is not fully ready for start. Aborting.`);
|
||
return;
|
||
}
|
||
if (this.playerCount === 0 || (this.mode === 'pvp' && this.playerCount === 1)) {
|
||
console.warn(`[Game ${this.id}] startGame called with insufficient players (${this.playerCount}). Mode: ${this.mode}. Aborting start.`);
|
||
return;
|
||
}
|
||
|
||
console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver: ${this.gameState.isGameOver}`);
|
||
const playerCharDataForSlotPlayer = this._getCharacterData(this.playerCharacterKey);
|
||
const opponentCharDataForSlotOpponent = this._getCharacterData(this.opponentCharacterKey);
|
||
|
||
if (!playerCharDataForSlotPlayer || !opponentCharDataForSlotOpponent) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: startGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`);
|
||
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при старте игры (не удалось загрузить данные персонажей).' });
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'start_data_load_failed');
|
||
}
|
||
return;
|
||
}
|
||
|
||
Object.values(this.players).forEach(playerInfo => {
|
||
if (playerInfo.socket && playerInfo.socket.connected) {
|
||
let dataForThisClient;
|
||
if (playerInfo.id === GAME_CONFIG.PLAYER_ID) {
|
||
dataForThisClient = {
|
||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||
playerBaseStats: playerCharDataForSlotPlayer.baseStats, opponentBaseStats: opponentCharDataForSlotOpponent.baseStats,
|
||
playerAbilities: playerCharDataForSlotPlayer.abilities, opponentAbilities: opponentCharDataForSlotOpponent.abilities,
|
||
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||
};
|
||
} else {
|
||
dataForThisClient = {
|
||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||
playerBaseStats: opponentCharDataForSlotOpponent.baseStats, opponentBaseStats: playerCharDataForSlotPlayer.baseStats,
|
||
playerAbilities: opponentCharDataForSlotOpponent.abilities, opponentAbilities: playerCharDataForSlotPlayer.abilities,
|
||
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||
};
|
||
}
|
||
playerInfo.socket.emit('gameStarted', dataForThisClient);
|
||
console.log(`[Game ${this.id}] Sent gameStarted to ${playerInfo.identifier} (socket ${playerInfo.socket.id}).`);
|
||
} else {
|
||
console.warn(`[Game ${this.id}] Player ${playerInfo.identifier} (socket ${playerInfo.socket?.id}) is disconnected. Cannot send gameStarted.`);
|
||
}
|
||
});
|
||
|
||
const firstTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${firstTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
this.broadcastLogUpdate(); // Отправляем лог с сообщением о начале хода
|
||
this.startTurnTimer(); // Запускаем таймер для первого хода
|
||
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||
console.log(`[Game ${this.id}] AI (Балард) ходит первым. Запускаем AI turn.`);
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN || 1200);
|
||
} else {
|
||
console.log(`[Game ${this.id}] Ход реального игрока ${firstTurnActorState.name} (роль: ${firstTurnActorState.id}).`);
|
||
}
|
||
}
|
||
|
||
processPlayerAction(requestingSocketId, actionData) {
|
||
if (!this.gameState || this.gameState.isGameOver) {
|
||
const playerSocket = this.io.sockets.sockets.get(requestingSocketId);
|
||
if (playerSocket) playerSocket.emit('gameError', { message: 'Игра уже завершена или неактивна.' });
|
||
return;
|
||
}
|
||
const actingPlayerInfo = this.players[requestingSocketId];
|
||
if (!actingPlayerInfo) {
|
||
console.error(`[Game ${this.id}] Action from socket ${requestingSocketId} not found in players map.`);
|
||
const playerSocket = this.io.sockets.sockets.get(requestingSocketId);
|
||
if (playerSocket && playerSocket.connected) playerSocket.disconnect(true);
|
||
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(`[Game ${this.id}] Action from ${actingPlayerInfo.identifier} (socket ${requestingSocketId}): Not their turn.`);
|
||
return; // Игнорируем действие, таймер хода не сбрасывается и не перезапускается
|
||
}
|
||
|
||
// Игрок сделал ход, очищаем таймер
|
||
this.clearTurnTimer();
|
||
|
||
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 = this._getCharacterData(attackerState.characterKey);
|
||
const defenderData = this._getCharacterData(defenderState.characterKey);
|
||
|
||
if (!attackerData || !defenderData) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: processPlayerAction - Failed to load character data! AttackerKey: ${attackerState.characterKey}, DefenderKey: ${defenderState.characterKey}`);
|
||
this.addToLog('Критическая ошибка сервера при обработке действия (не найдены данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.broadcastLogUpdate();
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'action_data_load_failed');
|
||
}
|
||
return;
|
||
}
|
||
let actionValid = true;
|
||
|
||
if (actionData.actionType === 'attack') {
|
||
const taunt = serverGameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
const delayedAttackBuffEffect = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK));
|
||
if (delayedAttackBuffEffect && !delayedAttackBuffEffect.justCast) {
|
||
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource);
|
||
if (actualRegen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${delayedAttackBuffEffect.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) {
|
||
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
|
||
console.warn(`[Game ${this.id}] Игрок ${actingPlayerInfo.identifier} (сокет ${requestingSocketId}) попытался использовать неизвестную способность ID: ${actionData.abilityId}.`);
|
||
this.startTurnTimer(); // Перезапускаем таймер, так как ход не был совершен
|
||
return;
|
||
}
|
||
const hasEnoughResource = attackerState.currentResource >= ability.cost;
|
||
const isOnCooldown = (attackerState.abilityCooldowns?.[ability.id] || 0) > 0;
|
||
const isCasterFullySilenced = attackerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||
const isAbilitySpecificallySilenced = attackerState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0);
|
||
const isSilenced = isCasterFullySilenced || isAbilitySpecificallySilenced;
|
||
let isOnSpecialCooldown = false;
|
||
if (attackerState.characterKey === 'balard') {
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns !== undefined && attackerState.silenceCooldownTurns > 0) isOnSpecialCooldown = true;
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns !== undefined && attackerState.manaDrainCooldownTurns > 0) isOnSpecialCooldown = true;
|
||
}
|
||
const isBuffAlreadyActive = ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id);
|
||
const isTargetedDebuff = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
|
||
const effectIdForDebuff = 'effect_' + ability.id;
|
||
const isDebuffAlreadyOnTarget = isTargetedDebuff && defenderState.activeEffects.some(e => e.id === effectIdForDebuff);
|
||
|
||
if (!hasEnoughResource) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && (isOnCooldown || isOnSpecialCooldown)) { this.addToLog(`"${ability.name}" еще на перезарядке.`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isSilenced) { this.addToLog(`${attackerState.name} не может использовать способности из-за безмолвия!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isBuffAlreadyActive) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isDebuffAlreadyOnTarget) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
|
||
if (actionValid) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
|
||
const taunt = serverGameLogic.getRandomTaunt(attackerState.characterKey, 'selfCastAbility', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
let baseCooldown = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) baseCooldown = ability.cooldown;
|
||
if (attackerState.characterKey === 'balard') {
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; }
|
||
else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
|
||
}
|
||
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
|
||
}
|
||
} else {
|
||
actingPlayerInfo.socket.emit('gameError', { message: `Неизвестный тип действия: ${actionData?.actionType}` });
|
||
console.warn(`[Game ${this.id}] Получен неизвестный тип действия от ${actingPlayerInfo.identifier} (сокет ${requestingSocketId}): ${actionData?.actionType}`);
|
||
actionValid = false;
|
||
}
|
||
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние, включая лог
|
||
// Очистка игры и таймеров происходит в checkGameOver
|
||
return;
|
||
}
|
||
|
||
if (actionValid) {
|
||
console.log(`[Game ${this.id}] Player action valid. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500);
|
||
} else {
|
||
// Если действие было невалидным, ход не передается.
|
||
// Отправляем лог и перезапускаем таймер для текущего игрока.
|
||
console.log(`[Game ${this.id}] Player action invalid. Broadcasting log update and restarting timer for current player.`);
|
||
this.broadcastLogUpdate();
|
||
this.startTurnTimer(); // Перезапускаем таймер, так как ход не был совершен
|
||
}
|
||
}
|
||
|
||
switchTurn() {
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
|
||
// Очищаем таймер предыдущего хода ПЕРЕД обработкой эффектов,
|
||
// так как эффекты могут изменить состояние игры и повлиять на следующий запуск таймера.
|
||
this.clearTurnTimer();
|
||
|
||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||
const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey);
|
||
|
||
if (!endingTurnCharacterData) {
|
||
console.error(`[Game ${this.id}] SwitchTurn Error: No character data found for ending turn actor role ${endingTurnActorRole} with key ${endingTurnActorState.characterKey}. Cannot process end-of-turn effects.`);
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'switch_turn_data_error');
|
||
}
|
||
return;
|
||
} else {
|
||
serverGameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnCharacterData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
serverGameLogic.updateBlockingStatus(this.gameState.player);
|
||
serverGameLogic.updateBlockingStatus(this.gameState.opponent);
|
||
if (endingTurnActorState.abilityCooldowns) serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this));
|
||
if (endingTurnActorState.characterKey === 'balard') {
|
||
if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--;
|
||
if (endingTurnActorState.manaDrainCooldownTurns !== undefined && endingTurnActorState.manaDrainCooldownTurns > 0) endingTurnActorState.manaDrainCooldownTurns--;
|
||
}
|
||
if (endingTurnActorState.disabledAbilities?.length > 0) {
|
||
const charAbilitiesForDisabledCheck = this._getCharacterAbilities(endingTurnActorState.characterKey);
|
||
if (charAbilitiesForDisabledCheck) serverGameLogic.processDisabledAbilities(endingTurnActorState.disabledAbilities, charAbilitiesForDisabledCheck, endingTurnActorState.name, this.addToLog.bind(this));
|
||
else console.warn(`[Game ${this.id}] SwitchTurn: Cannot process disabledAbilities for ${endingTurnActorState.name}: character abilities data not found.`);
|
||
}
|
||
}
|
||
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние, включая лог
|
||
// Очистка игры и таймеров происходит в checkGameOver
|
||
return;
|
||
}
|
||
|
||
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
|
||
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
|
||
|
||
const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
this.broadcastGameStateUpdate(); // Отправляем обновленное состояние и лог
|
||
this.startTurnTimer(); // Запускаем таймер для нового хода
|
||
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||
console.log(`[Game ${this.id}] Ход AI (Балард). Запускаем AI turn.`);
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN || 1200);
|
||
} else {
|
||
console.log(`[Game ${this.id}] Ход реального игрока ${currentTurnActorState.name} (роль: ${currentTurnActorState.id}).`);
|
||
}
|
||
}
|
||
|
||
processAiTurn() {
|
||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
// Если по какой-то причине сюда попали не во время хода AI, переключаем ход
|
||
console.warn(`[Game ${this.id}] processAiTurn called incorrectly. Attempting to switch turn.`);
|
||
this.switchTurn(); // Это запустит таймер для игрока, если это его ход
|
||
return;
|
||
}
|
||
// AI не использует таймер, поэтому this.clearTurnTimer() не нужен перед его действием.
|
||
// Таймер для игрока был очищен в switchTurn(), когда ход перешел к AI.
|
||
|
||
const attackerState = this.gameState.opponent;
|
||
const defenderState = this.gameState.player;
|
||
const attackerData = this._getCharacterData('balard');
|
||
const defenderData = this._getCharacterData('elena');
|
||
|
||
if (!attackerData || !defenderData) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: processAiTurn - Failed to load character data!`);
|
||
this.addToLog("AI не может действовать: ошибка данных персонажа.", GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.broadcastLogUpdate();
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'ai_data_load_failed');
|
||
}
|
||
this.switchTurn(); // Переключаем ход обратно (запустит таймер для игрока)
|
||
return;
|
||
}
|
||
const isBalardFullySilenced = attackerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||
if (isBalardFullySilenced) {
|
||
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
|
||
console.log(`[Game ${this.id}] AI (Балард) attacked while silenced. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500); // switchTurn запустит таймер для игрока
|
||
return;
|
||
}
|
||
|
||
const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this));
|
||
if (aiDecision.actionType === 'attack') {
|
||
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
|
||
const ability = aiDecision.ability;
|
||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
|
||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
let baseCooldown = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) baseCooldown = ability.cooldown;
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; }
|
||
else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
|
||
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
|
||
} else if (aiDecision.actionType === 'pass') {
|
||
if (aiDecision.logMessage) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
|
||
else this.addToLog(`${attackerState.name} обдумывает свой следующий ход...`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
} else {
|
||
console.error(`[Game ${this.id}] AI (Балард) chose an invalid action type: ${aiDecision.actionType}. Defaulting to pass and logging error.`);
|
||
this.addToLog(`AI ${attackerState.name} не смог выбрать действие из-за ошибки. Пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние, включая лог
|
||
// Очистка игры и таймеров происходит в checkGameOver
|
||
return;
|
||
}
|
||
console.log(`[Game ${this.id}] AI action complete. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500); // switchTurn запустит таймер для игрока
|
||
}
|
||
|
||
checkGameOver() {
|
||
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true;
|
||
if (!this.gameState.player || !this.gameState.opponent || this.gameState.opponent.maxHp <= 0) return false;
|
||
|
||
const isOver = serverGameLogic.checkGameOverInternal(this.gameState, GAME_CONFIG, gameData);
|
||
if (isOver && !this.gameState.isGameOver) { // Игра только что завершилась
|
||
this.gameState.isGameOver = true;
|
||
this.clearTurnTimer(); // Очищаем все таймеры
|
||
|
||
const playerDead = this.gameState.player?.currentHp <= 0;
|
||
const opponentDead = this.gameState.opponent?.currentHp <= 0;
|
||
let winnerRole = null; let loserRole = null;
|
||
if (this.mode === 'ai') {
|
||
winnerRole = playerDead ? null : GAME_CONFIG.PLAYER_ID; // В AI игре AI не "побеждает", побеждает только игрок
|
||
loserRole = playerDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
} else { // PvP
|
||
if (playerDead && opponentDead) { winnerRole = GAME_CONFIG.PLAYER_ID; loserRole = GAME_CONFIG.OPPONENT_ID; } // Ничья - победа игрока 1
|
||
else if (playerDead) { winnerRole = GAME_CONFIG.OPPONENT_ID; loserRole = GAME_CONFIG.PLAYER_ID; }
|
||
else if (opponentDead) { winnerRole = GAME_CONFIG.PLAYER_ID; loserRole = GAME_CONFIG.OPPONENT_ID; }
|
||
else { this.gameState.isGameOver = false; this.startTurnTimer(); return false; } // Ошибка, игра не окончена, перезапускаем таймер
|
||
}
|
||
|
||
const winnerState = this.gameState[winnerRole];
|
||
const loserState = this.gameState[loserRole];
|
||
const winnerName = winnerState?.name || (winnerRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
|
||
const loserName = loserState?.name || (loserRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
|
||
const loserCharacterKey = loserState?.characterKey || 'unknown';
|
||
|
||
if (this.mode === 'ai') {
|
||
if (winnerRole === GAME_CONFIG.PLAYER_ID) this.addToLog(`🏁 ПОБЕДА! Вы одолели ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
else this.addToLog(`😭 ПОРАЖЕНИЕ! ${winnerName} оказался(лась) сильнее! 😭`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else {
|
||
this.addToLog(`🏁 ПОБЕДА! ${winnerName} одолел(а) ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
const winningCharacterKey = winnerState?.characterKey;
|
||
if (this.mode === 'ai' && winningCharacterKey === 'elena') {
|
||
const taunt = serverGameLogic.getRandomTaunt(winningCharacterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerName}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
} else if (this.mode === 'pvp' && (winningCharacterKey === 'elena' || winningCharacterKey === 'almagest')) {
|
||
const taunt = serverGameLogic.getRandomTaunt(winningCharacterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerName}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
if (loserCharacterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
else if (loserCharacterKey === 'almagest') this.addToLog(`Над полем битвы воцаряется тишина. ${loserName} побежден(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
else if (loserCharacterKey === 'elena') this.addToLog(`Свет погас. ${loserName} повержен(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
|
||
console.log(`[Game ${this.id}] Game is over. Winner: ${winnerName} (${winnerRole}). Loser: ${loserName} (${loserRole}). Reason: HP <= 0.`);
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: this.mode === 'ai' ? (winnerRole === GAME_CONFIG.PLAYER_ID ? winnerRole : null) : winnerRole,
|
||
reason: 'hp_zero', // Используем стандартизированную причину
|
||
finalGameState: this.gameState,
|
||
log: this.consumeLogBuffer(),
|
||
loserCharacterKey: loserCharacterKey
|
||
});
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'hp_zero');
|
||
}
|
||
return true;
|
||
}
|
||
return isOver;
|
||
}
|
||
|
||
endGameDueToDisconnect(disconnectedSocketId, disconnectedPlayerRole, disconnectedCharacterKey) {
|
||
if (this.gameState && !this.gameState.isGameOver) {
|
||
this.gameState.isGameOver = true;
|
||
this.clearTurnTimer(); // Очищаем все таймеры
|
||
|
||
const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey);
|
||
const winnerCharacterKey = (winnerRole === GAME_CONFIG.PLAYER_ID) ? this.playerCharacterKey : this.opponentCharacterKey;
|
||
const winnerCharacterData = this._getCharacterBaseData(winnerCharacterKey);
|
||
|
||
this.addToLog(`🔌 Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} (${disconnectedPlayerRole}) отключился. Игра завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
if (this.mode === 'pvp') { // Только в PvP присуждаем победу оставшемуся
|
||
this.addToLog(`🏁 Победа присуждается ${winnerCharacterData?.name || winnerRole}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else { // В AI игре, если игрок отключается, AI не "побеждает"
|
||
this.addToLog(`Игра завершена досрочно.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
|
||
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: this.mode === 'pvp' ? winnerRole : null, // В AI режиме нет победителя при дисконнекте игрока
|
||
reason: 'opponent_disconnected',
|
||
finalGameState: this.gameState,
|
||
log: this.consumeLogBuffer(),
|
||
loserCharacterKey: disconnectedCharacterKey
|
||
});
|
||
console.log(`[Game ${this.id}] Game ended due to disconnect. Winner (PvP only): ${winnerCharacterData?.name || winnerRole}. Disconnected: ${disconnectedCharacterData?.name || disconnectedPlayerRole}.`);
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'opponent_disconnected');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Методы для управления таймером хода ---
|
||
startTurnTimer() {
|
||
this.clearTurnTimer();
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
|
||
// Определяем, является ли текущий ход ходом реального игрока
|
||
let isRealPlayerTurn = false;
|
||
if (this.gameState.isPlayerTurn) { // Ход слота 'player'
|
||
const playerInSlot = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
// Если это AI режим, и в слоте 'player' не Балард (т.е. это человек), то это ход реального игрока
|
||
if (this.mode === 'ai' && playerInSlot?.chosenCharacterKey !== 'balard') isRealPlayerTurn = true;
|
||
// Если это PvP режим, и в слоте 'player' есть игрок, это ход реального игрока
|
||
else if (this.mode === 'pvp' && playerInSlot) isRealPlayerTurn = true;
|
||
} else { // Ход слота 'opponent'
|
||
const opponentInSlot = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
// Если это AI режим, то в слоте 'opponent' всегда AI, это НЕ ход реального игрока
|
||
// Если это PvP режим, и в слоте 'opponent' есть игрок, это ход реального игрока
|
||
if (this.mode === 'pvp' && opponentInSlot) isRealPlayerTurn = true;
|
||
}
|
||
|
||
|
||
if (!isRealPlayerTurn) { // Если это ход AI или слот пуст
|
||
// console.log(`[Game ${this.id}] AI's turn or empty slot. Timer not started for player.`);
|
||
// Сообщаем клиентам, что таймер неактивен или это ход AI
|
||
// Передаем isPlayerTurn, чтобы клиент знал, чей ход вообще.
|
||
// Если isPlayerTurn=true, но isRealPlayerTurn=false (не должно быть, но на всякий),
|
||
// то это ошибка, и таймер все равно не стартует.
|
||
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime: null, isPlayerTurn: this.gameState.isPlayerTurn });
|
||
return;
|
||
}
|
||
|
||
this.turnStartTime = Date.now();
|
||
const turnDuration = GAME_CONFIG.TURN_DURATION_MS;
|
||
const currentTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
|
||
console.log(`[Game ${this.id}] Starting turn timer (${turnDuration / 1000}s) for ${currentTurnActor.name}.`);
|
||
|
||
this.turnTimerId = setTimeout(() => {
|
||
this.handleTurnTimeout();
|
||
}, turnDuration);
|
||
|
||
this.turnTimerUpdateIntervalId = setInterval(() => {
|
||
if (!this.gameState || this.gameState.isGameOver) {
|
||
this.clearTurnTimer();
|
||
return;
|
||
}
|
||
const elapsedTime = Date.now() - this.turnStartTime;
|
||
const remainingTime = Math.max(0, turnDuration - elapsedTime);
|
||
// Отправляем оставшееся время и чей сейчас ход
|
||
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: this.gameState.isPlayerTurn });
|
||
}, GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS);
|
||
|
||
// Отправляем начальное значение таймера сразу
|
||
this.io.to(this.id).emit('turnTimerUpdate', { remainingTime: turnDuration, isPlayerTurn: this.gameState.isPlayerTurn });
|
||
}
|
||
|
||
clearTurnTimer() {
|
||
if (this.turnTimerId) {
|
||
clearTimeout(this.turnTimerId);
|
||
this.turnTimerId = null;
|
||
}
|
||
if (this.turnTimerUpdateIntervalId) {
|
||
clearInterval(this.turnTimerUpdateIntervalId);
|
||
this.turnTimerUpdateIntervalId = null;
|
||
}
|
||
// console.log(`[Game ${this.id}] Turn timer cleared.`);
|
||
}
|
||
|
||
handleTurnTimeout() {
|
||
if (!this.gameState || this.gameState.isGameOver) return;
|
||
|
||
this.clearTurnTimer();
|
||
|
||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
const timedOutPlayerState = this.gameState[timedOutPlayerRole];
|
||
const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const winnerPlayerState = this.gameState[winnerPlayerRole];
|
||
|
||
// Убедимся, что у победителя есть имя
|
||
if (!winnerPlayerState || !winnerPlayerState.name || winnerPlayerState.name === 'Ожидание игрока...') {
|
||
// Это может произойти, если игрок выходит из игры PvP, когда он один, и его таймер истекает
|
||
// (хотя дисконнект должен был обработать это раньше).
|
||
// Или если в AI игре timedOutPlayer был AI (что не должно случаться с текущей логикой таймера).
|
||
console.error(`[Game ${this.id}] Turn timeout, but winner state is invalid. Timed out: ${timedOutPlayerState?.name}. Game will be cleaned up.`);
|
||
this.gameState.isGameOver = true; // Помечаем как оконченную
|
||
this.addToLog(`⏱️ Время хода для ${timedOutPlayerState?.name || 'игрока'} истекло. Игра завершена некорректно.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: null, // Неопределенный победитель
|
||
reason: 'timeout_error_state',
|
||
finalGameState: this.gameState,
|
||
log: this.consumeLogBuffer(),
|
||
loserCharacterKey: timedOutPlayerState?.characterKey || 'unknown'
|
||
});
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'timeout_error_state');
|
||
}
|
||
return;
|
||
}
|
||
|
||
|
||
this.gameState.isGameOver = true;
|
||
this.addToLog(`⏱️ Время хода для ${timedOutPlayerState.name} истекло!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.addToLog(`🏁 Победа присуждается ${winnerPlayerState.name}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
console.log(`[Game ${this.id}] Turn timed out for ${timedOutPlayerState.name}. Winner: ${winnerPlayerState.name}.`);
|
||
|
||
this.io.to(this.id).emit('gameOver', {
|
||
winnerId: winnerPlayerRole,
|
||
reason: 'turn_timeout',
|
||
finalGameState: this.gameState,
|
||
log: this.consumeLogBuffer(),
|
||
loserCharacterKey: timedOutPlayerState.characterKey
|
||
});
|
||
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'turn_timeout');
|
||
}
|
||
}
|
||
|
||
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() });
|
||
}
|
||
}
|
||
_getCharacterData(key) {
|
||
if (!key) { console.warn("GameInstance::_getCharacterData called with null/undefined key."); return null; }
|
||
switch (key) {
|
||
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities };
|
||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities };
|
||
default: console.error(`GameInstance::_getCharacterData: Unknown character key "${key}"`); return null;
|
||
}
|
||
}
|
||
_getCharacterBaseData(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.baseStats : null;
|
||
}
|
||
_getCharacterAbilities(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.abilities : null;
|
||
}
|
||
}
|
||
|
||
module.exports = GameInstance; |