bc/server_modules/gameInstance.js
2025-05-13 04:14:01 +00:00

578 lines
39 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 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.mode = mode;
this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest' } }
this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject }
this.playerCount = 0;
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.restartVotes = new Set();
this.playerCharacterKey = null;
this.opponentCharacterKey = null;
this.ownerUserId = null; // userId создателя игры
}
addPlayer(socket, chosenCharacterKey = 'elena') {
// Проверка, не пытается ли игрок присоединиться к игре, в которой он уже есть
if (this.players[socket.id]) {
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`);
return false;
}
if (this.playerCount >= 2) {
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
return false;
}
let assignedPlayerId;
let actualCharacterKey;
if (this.mode === 'ai') {
if (this.playerCount > 0) { // В AI игру может войти только один реальный игрок
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false;
}
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = 'elena';
if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId;
}
} else { // PvP
if (this.playerCount === 0) { // Первый игрок PvP
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId;
}
} else { // Второй игрок PvP
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players)[0]; // Информация о первом игроке
actualCharacterKey = (firstPlayerInfo.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
}
}
this.players[socket.id] = {
id: assignedPlayerId,
socket: socket,
chosenCharacterKey: actualCharacterKey
};
this.playerSockets[assignedPlayerId] = socket;
this.playerCount++;
socket.join(this.id);
const characterData = this._getCharacterBaseData(actualCharacterKey);
console.log(`[Game ${this.id}] Игрок ${socket.id} (userId: ${socket.userData?.userId || 'N/A'}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerUserId || 'N/A'}`);
if (this.mode === 'pvp' && this.playerCount < 2) {
socket.emit('waitingForOpponent');
}
// Если игра готова к старту (2 игрока в PvP, или 1 в AI)
if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) {
this.initializeGame();
if (this.gameState) {
this.startGame();
} else {
// Ошибка инициализации уже должна была быть залогирована и отправлена клиенту
console.error(`[Game ${this.id}] Не удалось запустить игру, так как gameState не был инициализирован.`);
}
}
return true;
}
removePlayer(socketId) {
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id;
let characterKeyToRemove = playerInfo.chosenCharacterKey;
const userIdOfLeavingPlayer = playerInfo.socket?.userData?.userId;
if (this.mode === 'ai' && playerRole === GAME_CONFIG.OPPONENT_ID) { // AI оппонент не имеет chosenCharacterKey в this.players
characterKeyToRemove = 'balard';
} else if (!characterKeyToRemove && this.gameState) { // Фоллбэк, если ключ не был в playerInfo
characterKeyToRemove = (playerRole === GAME_CONFIG.PLAYER_ID)
? this.gameState.player?.characterKey
: this.gameState.opponent?.characterKey;
}
const characterData = this._getCharacterBaseData(characterKeyToRemove);
console.log(`[Game ${this.id}] Игрок ${socketId} (userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyToRemove || 'N/A'}) покинул игру.`);
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
delete this.playerSockets[playerRole];
}
delete this.players[socketId];
this.playerCount--;
if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) {
const remainingPlayerSocketId = Object.keys(this.players)[0];
const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket;
this.ownerUserId = remainingPlayerSocket?.userData?.userId || null;
console.log(`[Game ${this.id}] Owner left. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`);
} else if (this.playerCount === 0) {
this.ownerUserId = null;
}
if (this.gameState && !this.gameState.isGameOver) {
this.endGameDueToDisconnect(playerRole, characterKeyToRemove);
}
}
}
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey) {
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 disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey);
this.addToLog(`Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} покинул игру.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: disconnectedPlayerRole });
this.io.to(this.id).emit('gameOver', {
winnerId: (this.mode === 'pvp' || winnerRole === GAME_CONFIG.OPPONENT_ID) ? 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... Mode: ${this.mode}`);
if (this.mode === 'ai') {
this.playerCharacterKey = 'elena';
this.opponentCharacterKey = 'balard';
} else { // pvp
const playerSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
const opponentSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Игрок 1 (слот 'player')
if (this.playerCount === 2 && opponentSocketInfo) { // Если есть второй игрок (слот 'opponent')
this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey;
// Убедимся, что персонажи разные
if (this.playerCharacterKey === this.opponentCharacterKey) {
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
opponentSocketInfo.chosenCharacterKey = this.opponentCharacterKey; // Обновляем и в информации о сокете
console.warn(`[Game ${this.id}] Corrected character selection in PvP. Opponent for slot ${GAME_CONFIG.OPPONENT_ID} is now ${this.opponentCharacterKey}`);
}
} else if (this.playerCount === 1) { // Только один игрок в PvP
this.opponentCharacterKey = null; // Оппонент еще не определен
} else {
console.error(`[Game ${this.id}] Unexpected playerCount (${this.playerCount}) or missing socketInfo during PvP character key assignment.`);
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; // Фоллбэк
}
}
console.log(`[Game ${this.id}] Finalizing characters - Player Slot: ${this.playerCharacterKey}, Opponent Slot: ${this.opponentCharacterKey || 'N/A'}`);
const playerBase = this._getCharacterBaseData(this.playerCharacterKey);
const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey);
let opponentBase = null;
let opponentAbilities = null;
if (this.opponentCharacterKey) {
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
}
const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities);
if (!playerBase || !playerAbilities || (!isReadyForFullGameState && !(this.mode === 'pvp' && this.playerCount === 1)) ) {
console.error(`[Game ${this.id}] CRITICAL ERROR: Failed to load necessary character data for initialization! Player: ${this.playerCharacterKey}, Opponent: ${this.opponentCharacterKey}, PlayerCount: ${this.playerCount}, Mode: ${this.mode}`);
this.logBuffer = [];
this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры.' });
this.gameState = null;
return;
}
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: [],
silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
abilityCooldowns: {}
},
isPlayerTurn: Math.random() < 0.5, isGameOver: false, turnNumber: 1, gameMode: this.mode
};
playerAbilities.forEach(ability => {
if (typeof ability.cooldown === 'number' && ability.cooldown > 0) this.gameState.player.abilityCooldowns[ability.id] = 0;
});
if (opponentAbilities) {
opponentAbilities.forEach(ability => {
let cd = 0;
if (ability.cooldown) cd = ability.cooldown;
else if (this.opponentCharacterKey === 'balard') {
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) cd = GAME_CONFIG[ability.internalCooldownFromConfig];
else if (typeof ability.internalCooldownValue === 'number') cd = ability.internalCooldownValue;
}
if (cd > 0) this.gameState.opponent.abilityCooldowns[ability.id] = 0;
});
}
this.restartVotes.clear();
const isFullGameReadyForLog = (this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2 && this.opponentCharacterKey);
const isRestart = this.logBuffer.length > 0 && isFullGameReadyForLog;
this.logBuffer = [];
if (isFullGameReadyForLog) {
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 ? this.gameState.player.name : (this.gameState.opponent?.name || 'Оппонент')}`);
}
startGame() {
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание...') {
if (this.mode === 'pvp' && this.playerCount === 1 && !this.opponentCharacterKey) {
console.log(`[Game ${this.id}] startGame: Waiting for opponent in PvP game.`);
} else if (!this.gameState) {
console.error(`[Game ${this.id}] Game cannot start: gameState is null.`);
} else {
console.warn(`[Game ${this.id}] Game not fully ready to start. OpponentKey: ${this.opponentCharacterKey}, OpponentName: ${this.gameState.opponent.name}, PlayerCount: ${this.playerCount}`);
}
return;
}
console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver: ${this.gameState.isGameOver}`);
const playerCharData = this._getCharacterData(this.playerCharacterKey);
const opponentCharData = this._getCharacterData(this.opponentCharacterKey);
if (!playerCharData || !opponentCharData) {
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: 'Критическая ошибка сервера при старте игры (данные персонажей).' });
return;
}
Object.values(this.players).forEach(pInfo => {
let dataForThisClient;
if (pInfo.id === GAME_CONFIG.PLAYER_ID) {
dataForThisClient = {
gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState,
playerBaseStats: playerCharData.baseStats, opponentBaseStats: opponentCharData.baseStats,
playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities,
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
};
} else {
dataForThisClient = {
gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState,
playerBaseStats: opponentCharData.baseStats, opponentBaseStats: playerCharData.baseStats,
playerAbilities: opponentCharData.abilities, opponentAbilities: playerCharData.abilities,
log: [], clientConfig: { ...GAME_CONFIG }
};
}
pInfo.socket.emit('gameStarted', dataForThisClient);
});
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 && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else {
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else {
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
handleVoteRestart(requestingSocketId) {
if (!this.gameState || !this.gameState.isGameOver) {
const playerSocket = this.players[requestingSocketId]?.socket || this.io.sockets.sockets.get(requestingSocketId);
if(playerSocket) playerSocket.emit('gameError', {message: "Нельзя рестартовать игру, которая не завершена."});
return;
}
if (!this.players[requestingSocketId]) return;
this.restartVotes.add(requestingSocketId);
const voterInfo = this.players[requestingSocketId];
const voterCharacterKey = voterInfo.id === GAME_CONFIG.PLAYER_ID ? this.gameState.player.characterKey : this.gameState.opponent.characterKey;
const voterCharacterData = this._getCharacterBaseData(voterCharacterKey);
this.addToLog(`Игрок ${voterCharacterData?.name || 'Неизвестный'} (${voterInfo.id}) голосует за рестарт.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate();
const requiredVotes = this.playerCount > 0 ? this.playerCount : 1;
if (this.restartVotes.size >= requiredVotes) {
this.initializeGame();
if (this.gameState) this.startGame();
else console.error(`[Game ${this.id}] Failed to restart: gameState is null after re-initialization.`);
} else if (this.mode === 'pvp') {
this.io.to(this.id).emit('waitingForRestartVote', {
voterCharacterName: voterCharacterData?.name || 'Неизвестный',
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 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) {
this.addToLog('Критическая ошибка сервера при обработке действия (данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate(); return;
}
const attackerBaseStats = attackerData.baseStats;
const defenderBaseStats = defenderData.baseStats;
const attackerAbilities = attackerData.abilities;
let actionValid = true;
if (actionData.actionType === 'attack') {
let taunt = "";
if (attackerState.characterKey === 'elena') {
taunt = serverGameLogic.getElenaTaunt('playerBasicAttack', {}, GAME_CONFIG, gameData, this.gameState);
}
const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH
: (attackerState.characterKey === 'almagest' ? GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK : null);
let attackBuffEffect = null;
if (attackBuffAbilityId) {
attackBuffEffect = attackerState.activeEffects.find(eff => eff.id === attackBuffAbilityId);
}
if (attackerState.characterKey === 'elena' && taunt && taunt !== "(Молчание)") {
this.addToLog(`${attackerState.name} атакует: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
} else {
this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
}
if (attackBuffEffect && !attackBuffEffect.justCast) {
this.addToLog(`✨ Эффект "${attackBuffEffect.name}" активен! Атака восстановит ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_EFFECT);
}
serverGameLogic.performAttack(
attackerState, defenderState, attackerBaseStats, defenderBaseStats,
this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData
);
if (attackBuffEffect) {
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} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
}
// Эффект НЕ удаляется здесь для многоразового действия
}
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerAbilities.find(ab => ab.id === actionData.abilityId);
if (!ability) { 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; }
if (actionValid && attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще не готова (КД: ${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (actionValid && attackerState.characterKey === 'balard') {
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && (attackerState.silenceCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.name}" еще не готова (спец. КД или общий КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && (attackerState.manaDrainCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.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; }
const isDebuffAbility = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
if (actionValid && isDebuffAbility) {
if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
}
if (actionValid) {
attackerState.currentResource -= ability.cost;
let baseCooldown = 0;
if (ability.cooldown) baseCooldown = ability.cooldown;
else if (attackerState.characterKey === 'balard') {
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { 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 && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
else { 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;
let logMessage = `${attackerState.name} колдует "${ability.name}" (-${ability.cost} ${attackerState.resourceName})`;
if (attackerState.characterKey === 'elena') {
const taunt = serverGameLogic.getElenaTaunt('playerActionCast', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
if (taunt && taunt !== "(Молчание)") logMessage += `: "${taunt}"`;
}
const 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 targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerState : defenderState;
const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderBaseStats : attackerBaseStats);
serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerBaseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
} else { actionValid = false; }
if (!actionValid) { this.broadcastLogUpdate(); return; }
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
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 endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey);
if (!endingTurnCharacterData) { console.error(`SwitchTurn Error: No char data for ${endingTurnActorState.characterKey}`); return; }
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 (endingTurnActorRole === GAME_CONFIG.OPPONENT_ID) {
const playerStateInGame = this.gameState.player;
if (playerStateInGame.disabledAbilities?.length > 0) {
const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey);
if (playerCharAbilities) serverGameLogic.processDisabledAbilities(playerStateInGame.disabledAbilities, playerCharAbilities, playerStateInGame.name, this.addToLog.bind(this));
}
}
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); 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();
if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else {
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else {
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
processAiTurn() {
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.opponentCharacterKey !== 'balard') {
if(!this.gameState || this.gameState.isGameOver) return;
return;
}
const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this));
const attackerState = this.gameState.opponent;
const defenderState = this.gameState.player;
const attackerData = this._getCharacterData('balard');
const defenderData = this._getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) { this.switchTurn(); return; }
let actionValid = true;
if (aiDecision.actionType === 'attack') {
this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
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;
if (attackerState.currentResource < ability.cost ||
(attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0)
) {
actionValid = false;
this.addToLog(`AI ${attackerState.name} не смог применить "${ability.name}" (ресурс/КД).`, GAME_CONFIG.LOG_TYPE_INFO);
}
if (actionValid) {
attackerState.currentResource -= ability.cost;
let baseCooldown = 0;
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { 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 && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
else { 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;
this.addToLog(`${attackerState.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT);
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL) ? attackerState : defenderState;
const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderData.baseStats : attackerData.baseStats);
serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerData.baseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
} 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 actionValid = false;
if (!actionValid) this.addToLog(`${attackerState.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
this.switchTurn();
}
checkGameOver() {
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true;
const playerState = this.gameState.player;
const opponentState = this.gameState.opponent;
if (!playerState || !opponentState) return false;
const playerDead = playerState.currentHp <= 0;
const opponentDead = opponentState.currentHp <= 0;
if (playerDead || opponentDead) {
this.gameState.isGameOver = true;
const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
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 ? "Игрок" : "Противник");
this.addToLog(`ПОБЕДА! ${winnerName} одолел(а) ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
if (winnerState?.characterKey === 'elena') {
const taunt = serverGameLogic.getElenaTaunt('opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState);
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
if (loserState?.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
else if (loserState?.characterKey === 'almagest') this.addToLog(`Елена одержала победу над темной волшебницей ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
}
this.io.to(this.id).emit('gameOver', { winnerId: winnerRole, reason: `${loserName} побежден(а)`, finalGameState: this.gameState, log: this.consumeLogBuffer() });
return true;
}
return false;
}
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) 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(`_getCharacterData: Unknown character key "${key}"`); return null; }}
_getCharacterBaseData(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerBaseStats; case 'balard': return gameData.opponentBaseStats; case 'almagest': return gameData.almagestBaseStats; default: console.error(`_getCharacterBaseData: Unknown character key "${key}"`); return null; }}
_getCharacterAbilities(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerAbilities; case 'balard': return gameData.opponentAbilities; case 'almagest': return gameData.almagestAbilities; default: console.error(`_getCharacterAbilities: Unknown character key "${key}"`); return null; }}
}
module.exports = GameInstance;