bc/server_modules/gameInstance.js
2025-05-09 12:11:07 +00:00

661 lines
34 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_modules/gameInstance.js
const { v4: uuidv4 } = require('uuid');
const GAME_CONFIG = require('./config');
const gameData = require('./data');
const serverGameLogic = require('./gameLogic');
class GameInstance {
constructor(gameId, io, mode = 'ai') {
this.id = gameId;
this.io = io;
this.players = {};
this.playerSockets = {};
this.playerCount = 0;
this.mode = mode;
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.restartVotes = new Set();
}
addPlayer(socket) {
// ... (код без изменений)
if (this.playerCount >= 2) {
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
return false;
}
let assignedPlayerId;
let characterName;
if (this.playerCount === 0) {
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
characterName = gameData.playerBaseStats.name;
} else {
if (this.mode === 'ai') {
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false;
}
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
characterName = gameData.opponentBaseStats.name;
}
this.players[socket.id] = {
id: assignedPlayerId,
socket: socket,
characterName: characterName
};
this.playerSockets[assignedPlayerId] = socket;
this.playerCount++;
socket.join(this.id);
console.log(`[Game ${this.id}] Игрок ${socket.id} (${characterName}) присоединился как ${assignedPlayerId}.`);
if (this.mode === 'pvp' && this.playerCount < 2) {
socket.emit('waitingForOpponent');
}
if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) {
this.initializeGame();
this.startGame();
}
return true;
}
removePlayer(socketId) {
// ... (код без изменений)
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id;
console.log(`[Game ${this.id}] Игрок ${socketId} (${playerInfo.characterName}) покинул игру.`);
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
delete this.playerSockets[playerRole];
}
delete this.players[socketId];
this.playerCount--;
if (this.gameState && !this.gameState.isGameOver) {
this.endGameDueToDisconnect(playerRole);
}
}
}
endGameDueToDisconnect(disconnectedPlayerRole) {
// ... (код без изменений)
if (this.gameState && !this.gameState.isGameOver) {
this.gameState.isGameOver = true;
const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const disconnectedName = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name;
this.addToLog(`Игрок ${disconnectedName} покинул игру.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('gameOver', {
winnerId: this.mode === 'pvp' ? winnerRole : GAME_CONFIG.OPPONENT_ID,
reason: 'opponent_disconnected',
finalGameState: this.gameState,
log: this.consumeLogBuffer()
});
}
}
initializeGame() {
console.log(`[Game ${this.id}] Initializing game state for (re)start...`);
const playerBase = gameData.playerBaseStats;
const opponentBase = gameData.opponentBaseStats;
this.gameState = {
player: {
id: GAME_CONFIG.PLAYER_ID, 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, name: opponentBase.name,
currentHp: opponentBase.maxHp, maxHp: opponentBase.maxHp,
currentResource: opponentBase.maxResource, maxResource: opponentBase.maxResource,
resourceName: opponentBase.resourceName, attackPower: opponentBase.attackPower,
isBlocking: false, activeEffects: [],
silenceCooldownTurns: 0, manaDrainCooldownTurns: 0, abilityCooldowns: {}
},
isPlayerTurn: Math.random() < 0.5,
isGameOver: false, // Очень важно для рестарта!
turnNumber: 1,
gameMode: this.mode
};
gameData.playerAbilities.forEach(ability => {
if (typeof ability.cooldown === 'number') {
this.gameState.player.abilityCooldowns[ability.id] = 0;
}
});
gameData.opponentAbilities.forEach(ability => {
let cd = 0;
// For opponent abilities, specific cooldowns like silenceCooldownTurns are often handled separately.
// The general abilityCooldowns map can still be used for other abilities or as a backup.
if (ability.cooldown) { // General cooldown field (if you add it to data.js for opponent abilities)
cd = ability.cooldown;
} else if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
cd = GAME_CONFIG[ability.internalCooldownFromConfig];
} else if (typeof ability.internalCooldownValue === 'number') {
cd = ability.internalCooldownValue;
}
// Initialize general cooldown to 0 (ready) for abilities that have a cooldown value.
// Specific cooldowns like silenceCooldownTurns are initialized separately.
if (cd > 0) {
this.gameState.opponent.abilityCooldowns[ability.id] = 0;
}
});
// Specific cooldowns for Balard - ensuring they are reset
this.gameState.opponent.silenceCooldownTurns = 0;
this.gameState.opponent.manaDrainCooldownTurns = 0;
this.restartVotes.clear();
const isRestart = this.logBuffer.length > 0; // Если лог не пуст, вероятно это рестарт
this.logBuffer = [];
this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState.isGameOver}. First turn: ${this.gameState.isPlayerTurn ? 'Елена' : 'Балард'}`);
}
startGame() {
console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver should be false: ${this.gameState.isGameOver}`);
Object.values(this.players).forEach(pInfo => {
pInfo.socket.emit('gameStarted', {
gameId: this.id,
yourPlayerId: pInfo.id,
initialGameState: this.gameState, // Отправляем свежеинициализированное состояние
playerBaseStats: gameData.playerBaseStats,
opponentBaseStats: gameData.opponentBaseStats,
playerAbilities: gameData.playerAbilities,
opponentAbilities: gameData.opponentAbilities,
log: this.consumeLogBuffer(), // Отправляем лог
clientConfig: { ...GAME_CONFIG } // Отправляем полный GAME_CONFIG
});
});
const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name;
this.addToLog(`--- ${firstTurnName} ходит первым! ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate(); // Отправляем состояние с первым логом хода
if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent) {
console.log(`[Game ${this.id}] AI's turn. Scheduling AI action.`);
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else {
console.log(`[Game ${this.id}] Opponent's turn (PvP). Notifying client.`);
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else {
console.log(`[Game ${this.id}] Player's turn (Елена). Notifying client.`);
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
handleVoteRestart(requestingSocketId) {
console.log(`[Game ${this.id}] handleVoteRestart called by ${requestingSocketId}. isGameOver: ${this.gameState?.isGameOver}`);
if (!this.gameState || !this.gameState.isGameOver) {
console.warn(`[Game ${this.id}] Vote restart rejected: game not over or no game state.`);
const playerSocket = this.io.sockets.sockets.get(requestingSocketId);
if(playerSocket) playerSocket.emit('gameError', {message: "Нельзя рестартовать игру, которая не завершена."});
return;
}
if (!this.players[requestingSocketId]) {
console.warn(`[Game ${this.id}] Vote restart rejected: unknown player ${requestingSocketId}.`);
return;
}
this.restartVotes.add(requestingSocketId);
const voterInfo = this.players[requestingSocketId];
this.addToLog(`Игрок ${voterInfo.characterName} (${voterInfo.id}) голосует за рестарт.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate();
// Для PvP, requiredVotes должен быть равен текущему количеству игроков в комнате.
// Если один игрок, то 1 голос. Если два, то 2 голоса.
const requiredVotes = this.playerCount > 0 ? this.playerCount : 1;
console.log(`[Game ${this.id}] Votes for restart: ${this.restartVotes.size}/${requiredVotes}. Players in game: ${this.playerCount}`);
if (this.restartVotes.size >= requiredVotes) {
console.log(`[Game ${this.id}] All players voted for restart or single player restart. Restarting game...`);
this.initializeGame();
this.startGame();
} else if (this.mode === 'pvp') { // Отправляем уведомление о голосовании только в PvP, если не все проголосовали
this.io.to(this.id).emit('waitingForRestartVote', {
voterCharacterName: voterInfo.characterName,
voterRole: voterInfo.id,
votesNeeded: requiredVotes - this.restartVotes.size
});
}
}
processPlayerAction(requestingSocketId, actionData) {
// ... (код без изменений)
if (!this.gameState || this.gameState.isGameOver) return;
const actingPlayerInfo = this.players[requestingSocketId];
if (!actingPlayerInfo) {
console.error(`[Game ${this.id}] Action from unknown socket ${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) {
actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" });
return;
}
const attackerState = this.gameState[actingPlayerRole];
const attackerBaseStats = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats;
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const defenderState = this.gameState[defenderRole];
const defenderBaseStats = defenderRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats;
let actionValid = true;
if (actionData.actionType === 'attack') {
if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) {
const taunt = serverGameLogic.getElenaTaunt(
'playerBasicAttack',
{ opponentHpPerc: (defenderState.currentHp / defenderBaseStats.maxHp) * 100 },
GAME_CONFIG, gameData, this.gameState
);
this.addToLog(`${attackerState.name} атакует: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
const natureEffectIndex = attackerState.activeEffects.findIndex(
eff => eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH && !eff.justCast
);
if (natureEffectIndex !== -1) {
this.addToLog(`✨ Сила Природы активна! Атака восстановит ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_EFFECT);
}
} else {
this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
}
serverGameLogic.performAttack(
attackerState, defenderState, attackerBaseStats, defenderBaseStats,
this.gameState, this.addToLog.bind(this), GAME_CONFIG
);
if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) {
const natureEffectIndex = attackerState.activeEffects.findIndex(
eff => eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH && !eff.justCast
);
if (natureEffectIndex !== -1) {
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerBaseStats.maxResource - attackerState.currentResource);
if (actualRegen > 0) {
attackerState.currentResource += actualRegen;
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от Силы Природы!`, GAME_CONFIG.LOG_TYPE_HEAL);
}
}
}
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
const abilityList = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerAbilities : gameData.opponentAbilities;
const ability = abilityList.find(ab => ab.id === actionData.abilityId);
if (!ability) {
console.error(`[Game ${this.id}] Неизвестная способность ID: ${actionData.abilityId} для игрока ${actingPlayerRole}`);
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
return;
}
if (attackerState.currentResource < ability.cost) {
this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
// Check general cooldowns
if (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) {
this.addToLog(`"${ability.name}" еще не готова (КД: ${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
// Check specific opponent cooldowns if this is the opponent
if (actingPlayerRole === GAME_CONFIG.OPPONENT_ID) {
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) {
this.addToLog(`"${ability.name}" еще не готова (спец. КД: ${attackerState.silenceCooldownTurns} х.).`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) {
this.addToLog(`"${ability.name}" еще не готова (спец. КД: ${attackerState.manaDrainCooldownTurns} х.).`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
}
if (actionValid && actingPlayerRole === GAME_CONFIG.PLAYER_ID && ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS) {
const effectIdForDebuff = 'effect_' + ability.id;
if (defenderState.activeEffects.some(e => e.id === effectIdForDebuff)) {
this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
}
if (actionValid && ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id)) {
this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
if (actionValid) {
attackerState.currentResource -= ability.cost;
// Set cooldowns
let baseCooldown = 0;
if (ability.cooldown) { // For player abilities mainly
baseCooldown = ability.cooldown;
} else if (actingPlayerRole === GAME_CONFIG.OPPONENT_ID) { // For Balard
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) {
attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Set specific CD
baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Also set general CD for consistency if needed elsewhere
} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN) {
attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; // Set specific CD
baseCooldown = ability.internalCooldownValue; // Also set general CD
} else { // For other potential Balard abilities with general CD
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig];
} else if (typeof ability.internalCooldownValue === 'number') {
baseCooldown = ability.internalCooldownValue;
}
}
}
if (baseCooldown > 0 && attackerState.abilityCooldowns) {
attackerState.abilityCooldowns[ability.id] = baseCooldown +1; // +1 because it ticks down at end of current turn
}
let logMessage = `${attackerState.name} колдует "${ability.name}" (-${ability.cost} ${attackerState.resourceName})`;
if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) {
const taunt = serverGameLogic.getElenaTaunt(
'playerActionCast', { abilityId: ability.id },
GAME_CONFIG, gameData, this.gameState
);
logMessage += `: "${taunt}"`;
}
let logType = ability.type === GAME_CONFIG.ACTION_TYPE_HEAL ? GAME_CONFIG.LOG_TYPE_HEAL :
ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE ? GAME_CONFIG.LOG_TYPE_DAMAGE :
GAME_CONFIG.LOG_TYPE_EFFECT;
this.addToLog(logMessage, logType);
const targetIdForAbility = (
ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE ||
ability.type === GAME_CONFIG.ACTION_TYPE_DEBUFF ||
ability.id === GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE ||
(actingPlayerRole === GAME_CONFIG.OPPONENT_ID && ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) ||
(actingPlayerRole === GAME_CONFIG.OPPONENT_ID && ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN)
) ? defenderRole : actingPlayerRole;
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF)
? attackerState
: defenderState;
const targetBaseStatsForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF)
? attackerBaseStats
: defenderBaseStats;
serverGameLogic.applyAbilityEffect(
ability, attackerState, targetForAbility, // Corrected target state
attackerBaseStats,
targetBaseStatsForAbility, // Corrected target base stats
this.gameState, this.addToLog.bind(this), GAME_CONFIG
);
}
}
if (!actionValid) {
this.broadcastLogUpdate();
return;
}
if (this.checkGameOver()) {
this.broadcastGameStateUpdate();
return;
}
// Action was valid and processed, delay next step if needed
setTimeout(() => {
this.switchTurn();
}, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
}
switchTurn() {
// ... (код без изменений)
if (!this.gameState || this.gameState.isGameOver) return;
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const endingTurnActorState = this.gameState[endingTurnActorRole];
const endingTurnActorBaseStats = endingTurnActorRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats;
serverGameLogic.processEffects(
endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorBaseStats,
endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG
);
if (serverGameLogic.updateBlockingStatus) {
serverGameLogic.updateBlockingStatus(this.gameState.player);
serverGameLogic.updateBlockingStatus(this.gameState.opponent);
}
if (this.gameState.isPlayerTurn) { // Player (Elena) just finished her turn
if (serverGameLogic.processPlayerAbilityCooldowns) {
serverGameLogic.processPlayerAbilityCooldowns(
this.gameState.player.abilityCooldowns,
this.addToLog.bind(this),
this.gameState.player.name,
gameData.playerAbilities
);
}
} else { // Opponent (Balard) just finished his turn
if (serverGameLogic.processDisabledAbilities) { // Process effects on Elena like silence
serverGameLogic.processDisabledAbilities(
this.gameState.player.disabledAbilities,
this.addToLog.bind(this)
);
}
// Decrement Balard's specific cooldowns
if (this.gameState.opponent.silenceCooldownTurns > 0) { this.gameState.opponent.silenceCooldownTurns--; }
if (this.gameState.opponent.manaDrainCooldownTurns > 0) { this.gameState.opponent.manaDrainCooldownTurns--; }
// Decrement Balard's general cooldowns
if (this.gameState.opponent.abilityCooldowns) {
serverGameLogic.processPlayerAbilityCooldowns( // Re-using this function for opponent
this.gameState.opponent.abilityCooldowns,
this.addToLog.bind(this),
this.gameState.opponent.name,
gameData.opponentAbilities
);
}
}
if (this.checkGameOver()) { // Check game over after effects and cooldowns processed
this.broadcastGameStateUpdate();
return;
}
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
this.gameState.turnNumber++;
const currentTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name;
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnName} ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate();
if (!this.gameState.isPlayerTurn) { // If it's now Opponent's turn
if (this.aiOpponent) {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else { // PvP opponent's turn
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else { // If it's now Player's turn
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
processAiTurn() {
// ... (код без изменений)
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) return;
console.log(`[Game ${this.id}] AI Opponent (Балард) turn`);
const aiDecision = serverGameLogic.decideAiAction(
this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this)
);
if (!aiDecision || aiDecision.actionType === 'pass') {
if (aiDecision && aiDecision.logMessage) {
this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
} else {
this.addToLog(`${this.gameState.opponent.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
}
this.switchTurn(); // Directly switch turn, no delay needed for AI's own pass
return;
}
let actionProcessed = false;
if (aiDecision.actionType === 'attack') {
this.addToLog(`${this.gameState.opponent.name} атакует ${this.gameState.player.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
serverGameLogic.performAttack(
this.gameState.opponent, this.gameState.player,
gameData.opponentBaseStats, gameData.playerBaseStats,
this.gameState, this.addToLog.bind(this), GAME_CONFIG
);
actionProcessed = true;
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
const ability = aiDecision.ability;
// Double check resource and cooldowns here, though AI should already consider it
if (this.gameState.opponent.currentResource < ability.cost) {
this.addToLog(`${this.gameState.opponent.name} пытается применить "${ability.name}", но не хватает ${this.gameState.opponent.resourceName}! (AI Ошибка?)`, GAME_CONFIG.LOG_TYPE_INFO);
} else if ((ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && this.gameState.opponent.silenceCooldownTurns > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && this.gameState.opponent.manaDrainCooldownTurns > 0) ||
(this.gameState.opponent.abilityCooldowns && this.gameState.opponent.abilityCooldowns[ability.id] > 0))
{
this.addToLog(`AI попытался использовать "${ability.name}" на кулдауне. (AI Ошибка?)`, GAME_CONFIG.LOG_TYPE_INFO);
}
else {
this.gameState.opponent.currentResource -= ability.cost;
// Set cooldowns
let baseCooldown = 0;
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) {
this.gameState.opponent.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;
baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Also for general CD map
} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) {
this.gameState.opponent.manaDrainCooldownTurns = ability.internalCooldownValue;
baseCooldown = ability.internalCooldownValue; // Also for general CD map
} else { // For other abilities that might use the general map
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig];
} else if (typeof ability.internalCooldownValue === 'number') {
baseCooldown = ability.internalCooldownValue;
}
}
if (baseCooldown > 0 && this.gameState.opponent.abilityCooldowns) {
// +1 because it ticks down at end of *this current* turn
this.gameState.opponent.abilityCooldowns[ability.id] = baseCooldown +1;
}
this.addToLog(`${this.gameState.opponent.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT);
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL)
? this.gameState.opponent
: this.gameState.player;
const targetBaseStatsForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL)
? gameData.opponentBaseStats
: gameData.playerBaseStats;
serverGameLogic.applyAbilityEffect(
ability, this.gameState.opponent, targetForAbility,
gameData.opponentBaseStats,
targetBaseStatsForAbility,
this.gameState, this.addToLog.bind(this), GAME_CONFIG
);
actionProcessed = true;
}
}
if (!actionProcessed) { // If AI chose an ability but it was invalid (e.g. due to error in AI logic)
this.addToLog(`${this.gameState.opponent.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
this.switchTurn(); // No delay, just switch
return;
}
if (this.checkGameOver()) {
this.broadcastGameStateUpdate();
return;
}
// AI action processed, now switch turn (serverGameLogic.switchTurn handles its own delays/timing)
this.switchTurn();
}
checkGameOver() {
// ... (код без изменений)
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true;
const playerDead = this.gameState.player.currentHp <= 0;
const opponentDead = this.gameState.opponent.currentHp <= 0;
if (playerDead || opponentDead) {
this.gameState.isGameOver = true;
const winnerId = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const winner = this.gameState[winnerId];
const loserId = winnerId === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const loser = this.gameState[loserId];
const winnerName = winnerId === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name;
const loserName = loserId === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name;
this.addToLog(`ПОБЕДА! ${winnerName} одолел(а) ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
if (winnerId === GAME_CONFIG.PLAYER_ID && this.mode === 'ai') {
const opponentNearDefeatTaunt = serverGameLogic.getElenaTaunt(
'opponentNearDefeatCheck', {},
GAME_CONFIG, gameData, this.gameState
);
if (opponentNearDefeatTaunt && opponentNearDefeatTaunt !== "(Молчание)") {
this.addToLog(`${this.gameState.player.name}: "${opponentNearDefeatTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
}
this.addToLog(`Елена исполнила свой тяжкий долг. ${gameData.opponentBaseStats.name} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
}
this.io.to(this.id).emit('gameOver', {
winnerId: winnerId,
reason: playerDead ? `${loserName} побежден(а)` : `${winnerName} побежден(а)`, // Corrected: Loser is playerDead means player lost
finalGameState: this.gameState,
log: this.consumeLogBuffer()
});
return true;
}
return false;
}
addToLog(message, type) {
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;