661 lines
34 KiB
JavaScript
661 lines
34 KiB
JavaScript
// /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; |