// /server_modules/gameManager.js const { v4: uuidv4 } = require('uuid'); const GameInstance = require('./gameInstance'); const gameData = require('./data'); const GAME_CONFIG = require('./config'); class GameManager { constructor(io) { this.io = io; this.games = {}; // { gameId: GameInstance } this.userIdentifierToGameId = {}; // { userId|socketId: gameId } this.pendingPvPGames = []; // [gameId] } _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) { const oldPendingGameId = this.userIdentifierToGameId[identifier]; if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) { const gameToRemove = this.games[oldPendingGameId]; if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) { const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) { console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`); this._cleanupGame(oldPendingGameId, 'replaced_by_new_game'); } } else { if (this.userIdentifierToGameId[identifier] !== excludeGameId) { console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`); delete this.userIdentifierToGameId[identifier]; } } } } createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) { this._removePreviousPendingGames(socket.id, identifier); if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) { console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`); socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } const gameId = uuidv4(); const game = new GameInstance(gameId, this.io, mode, this); game.ownerIdentifier = identifier; this.games[gameId] = game; const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena'; if (game.addPlayer(socket, charKeyForInstance, identifier)) { this.userIdentifierToGameId[identifier] = gameId; console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`); const assignedPlayerId = game.players[socket.id]?.id; if (!assignedPlayerId) { this._cleanupGame(gameId, 'player_add_failed'); console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`); socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' }); return; } socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId }); if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) { console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`); const isInitialized = game.initializeGame(); if (isInitialized) { game.startGame(); } else { console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`); this._cleanupGame(gameId, 'initialization_failed'); } if (game.mode === 'pvp' && game.playerCount === 2) { const gameIndex = this.pendingPvPGames.indexOf(gameId); if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1); this.broadcastAvailablePvPGames(); } } else if (mode === 'pvp' && game.playerCount === 1) { if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } game.initializeGame(); // Частичная инициализация this.broadcastAvailablePvPGames(); } } else { this._cleanupGame(gameId, 'player_add_failed'); console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`); } } joinGame(socket, gameId, identifier) { const game = this.games[gameId]; if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; } if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) { console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`); socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return; } this._removePreviousPendingGames(socket.id, identifier, gameId); if (game.addPlayer(socket, null, identifier)) { this.userIdentifierToGameId[identifier] = gameId; console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`); if (game.mode === 'pvp' && game.playerCount === 2) { console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`); const isInitialized = game.initializeGame(); if (isInitialized) { game.startGame(); } else { console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`); this._cleanupGame(gameId, 'initialization_failed'); } const gameIndex = this.pendingPvPGames.indexOf(gameId); if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1); this.broadcastAvailablePvPGames(); } } else { console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`); } } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { this._removePreviousPendingGames(socket.id, identifier); if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) { console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на поиск.`); socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } let gameIdToJoin = null; const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena'; for (const id of this.pendingPvPGames) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) { const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) { gameIdToJoin = id; break; } if (!gameIdToJoin) gameIdToJoin = id; } } if (gameIdToJoin) { console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`); this.joinGame(socket, gameIdToJoin, identifier); } else { console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`); this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); socket.emit('noPendingGamesFound', { message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.', gameId: this.userIdentifierToGameId[identifier], yourPlayerId: GAME_CONFIG.PLAYER_ID }); } } handlePlayerAction(identifier, actionData) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game && game.players) { const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const currentSocketId = playerInfo?.socket?.id; if (playerInfo && currentSocketId) { const actualSocket = this.io.sockets.sockets.get(currentSocketId); if (actualSocket && actualSocket.connected) { game.processPlayerAction(currentSocketId, actionData); } else { console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`); } } else { console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`); delete this.userIdentifierToGameId[identifier]; const playerSocket = this.io.sockets.sockets.get(identifier) || playerInfo?.socket; if (playerSocket) { playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' }); } } } else { console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`); delete this.userIdentifierToGameId[identifier]; const playerSocket = this.io.sockets.sockets.get(identifier); if (playerSocket) { playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' }); } } } handleDisconnect(socketId, identifier) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game && game.players) { const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfo) { console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`); const disconnectedPlayerRole = playerInfo.id; const disconnectedCharacterKey = playerInfo.chosenCharacterKey; game.removePlayer(socketId); // Удаляем именно этот сокет if (game.playerCount === 0) { console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`); this._cleanupGame(gameId, 'player_count_zero_on_disconnect'); } else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) { // Если это PvP игра и остался 1 игрок, и игра НЕ была завершена дисконнектом этого игрока // (т.е. другой игрок еще в игре) // Тогда игра переходит в состояние ожидания. const remainingPlayerInfo = Object.values(game.players)[0]; // Единственный оставшийся игрок if (remainingPlayerInfo) { // Проверяем, что это не тот же игрок, что и отключившийся if (remainingPlayerInfo.identifier !== identifier) { game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey); // _cleanupGame будет вызван из endGameDueToDisconnect } else { // Отключился единственный оставшийся игрок в ожидающей игре. // _cleanupGame должен быть вызван. console.log(`[GameManager] Отключился единственный игрок ${identifier} из ожидающей PvP игры ${gameId}. Удаляем игру.`); this._cleanupGame(gameId, 'last_player_disconnected_from_pending'); } } else { // Оставшегося игрока нет, хотя playerCount > 0 - это ошибка, очищаем. console.error(`[GameManager] Ошибка: playerCount > 0 в игре ${gameId}, но не найден оставшийся игрок после дисконнекта ${identifier}. Очищаем.`); this._cleanupGame(gameId, 'error_no_remaining_player'); } } else if (game.gameState && !game.gameState.isGameOver) { // Если игра была активна (не ожидала) и еще не была завершена, // дисконнект одного из игроков завершает игру. game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey); // _cleanupGame будет вызван из endGameDueToDisconnect } else if (game.gameState?.isGameOver) { // Если игра уже была завершена до этого дисконнекта, просто удаляем ссылку на игру для отключившегося. console.log(`[GameManager] Игрок ${identifier} отключился из уже завершенной игры ${gameId}. Удаляем ссылку.`); delete this.userIdentifierToGameId[identifier]; // _cleanupGame уже был вызван при завершении игры. } else { // Другие случаи (например, AI игра, где игрок остался, или ошибка) console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`); delete this.userIdentifierToGameId[identifier]; } } else { console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`); delete this.userIdentifierToGameId[identifier]; } } else { console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`); delete this.userIdentifierToGameId[identifier]; } } _cleanupGame(gameId, reason = 'unknown_reason') { const game = this.games[gameId]; if (!game) { console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`); return false; } console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`); // Очищаем таймеры, если они были активны if (typeof game.clearTurnTimer === 'function') { game.clearTurnTimer(); } Object.values(game.players).forEach(playerInfo => { if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) { delete this.userIdentifierToGameId[playerInfo.identifier]; console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`); } }); const pendingIndex = this.pendingPvPGames.indexOf(gameId); if (pendingIndex > -1) { this.pendingPvPGames.splice(pendingIndex, 1); console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`); } delete this.games[gameId]; console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`); this.broadcastAvailablePvPGames(); return true; } getAvailablePvPGamesListForClient() { return this.pendingPvPGames .map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) { let firstPlayerUsername = 'Игрок'; let firstPlayerCharacterName = ''; const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (firstPlayerInfo) { if (firstPlayerInfo.socket?.userData?.username) { firstPlayerUsername = firstPlayerInfo.socket.userData.username; } else { firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0, 6)}`; } const charKey = firstPlayerInfo.chosenCharacterKey; if (charKey) { const charBaseStats = this._getCharacterBaseData(charKey); if (charBaseStats && charBaseStats.name) { firstPlayerCharacterName = charBaseStats.name; } else { firstPlayerCharacterName = charKey; } } } else { console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`); firstPlayerUsername = 'Неизвестный игрок'; } let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`; if (firstPlayerCharacterName) statusString += ` за ${firstPlayerCharacterName}`; statusString += `)`; return { id: gameId, status: statusString }; } if (game && !this.pendingPvPGames.includes(gameId)) { /* Game not pending */ } else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) { /* Game over or not initialized */ } else if (game && game.playerCount === 2) { /* Game full */ } else if (game && game.playerCount === 0) { console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`); delete this.games[gameId]; } return null; }) .filter(info => info !== null); } broadcastAvailablePvPGames() { const availableGames = this.getAvailablePvPGamesListForClient(); this.io.emit('availablePvPGamesList', availableGames); console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`); } getActiveGamesList() { return Object.values(this.games).map(game => { let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)'); let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)'); const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID); if (!playerInSlot1) playerSlotCharName = 'Пусто'; if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...'; if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)'; return { id: game.id.substring(0, 8), mode: game.mode, playerCount: game.playerCount, isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)', playerSlot: playerSlotCharName, opponentSlot: opponentSlotCharName, ownerIdentifier: game.ownerIdentifier || 'N/A', pending: this.pendingPvPGames.includes(game.id), turn: game.gameState ? `Ход ${game.gameState.turnNumber}, ${game.gameState.isPlayerTurn ? (playerInSlot1?.identifier || 'Player Slot') : (playerInSlot2?.identifier || 'Opponent Slot')}` : 'N/A' }; }); } handleRequestGameState(socket, identifier) { const gameId = this.userIdentifierToGameId[identifier]; let game = gameId ? this.games[gameId] : null; if (game && game.players) { const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfo) { if (game.gameState?.isGameOver) { console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`); delete this.userIdentifierToGameId[identifier]; socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' }); return; } console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`); const oldSocketId = playerInfo.socket?.id; if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) { console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`); delete game.players[oldSocketId]; if (game.playerSockets[playerInfo.id]?.id === oldSocketId) { delete game.playerSockets[playerInfo.id]; } } game.players[socket.id] = playerInfo; game.players[socket.id].socket = socket; game.playerSockets[playerInfo.id] = socket; socket.join(game.id); const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey); const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; let opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null; if (!opponentCharacterKeyForClient) { opponentCharacterKeyForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey; } const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient); if (playerCharDataForClient && opponentCharDataForClient && game.gameState) { const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2); const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...'; socket.emit('gameState', { gameId: game.id, yourPlayerId: playerInfo.id, gameState: game.gameState, playerBaseStats: playerCharDataForClient.baseStats, opponentBaseStats: opponentCharDataForClient.baseStats, playerAbilities: playerCharDataForClient.abilities, opponentAbilities: opponentCharDataForClient.abilities, log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } }); console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`); if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) { console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect. Initializing/Starting.`); const isInitialized = game.initializeGame(); if (isInitialized) { game.startGame(); } else { console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`); this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения.' }); this._cleanupGame(gameId, 'reconnect_initialization_failed'); } } else if (!isGameReadyForPlay) { console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`); socket.emit('waitingForOpponent'); } else if (game.gameState.isGameOver) { // Повторная проверка, т.к. startGame мог завершить игру console.log(`[GameManager] Reconnected to game ${gameId} which is now over (after re-init). Sending gameNotFound.`); delete this.userIdentifierToGameId[identifier]; socket.emit('gameNotFound', { message: 'Ваша игровая сессия завершилась во время переподключения.' }); } else { console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`); // Важно: если игра активна, нужно отправить и текущее состояние таймера. // Это можно сделать, вызвав game.startTurnTimer() (он отправит update), // но только если это ход этого игрока и игра не AI (или ход игрока в AI). // Или добавить отдельный метод в GameInstance для отправки текущего состояния таймера. if (typeof game.startTurnTimer === 'function') { // Проверяем, что метод существует // Перезапуск таймера здесь может быть некорректным, если ход не этого игрока // Лучше, чтобы gameInstance сам отправлял 'turnTimerUpdate' при gameState // Либо добавить специальный метод в gameInstance для отправки текущего значения таймера // Пока оставим так, startTurnTimer сам проверит, чей ход. game.startTurnTimer(); } } } else { console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`); socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' }); this._cleanupGame(gameId, 'reconnect_send_failed'); socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' }); } } else { console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`); delete this.userIdentifierToGameId[identifier]; socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' }); } } else { console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`); socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' }); } } _getCharacterData(key) { if (!key) { console.warn("GameManager::_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(`GameManager::_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 = GameManager;