bc/server_modules/gameInstance.js

788 lines
54 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 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;