/** * Combat API Controller * Handles all combat-related HTTP requests including combat initiation, status, and history */ const CombatService = require('../../services/combat/CombatService'); const { CombatPluginManager } = require('../../services/combat/CombatPluginManager'); const GameEventService = require('../../services/websocket/GameEventService'); const logger = require('../../utils/logger'); const { ValidationError, ConflictError, NotFoundError } = require('../../middleware/error.middleware'); class CombatController { constructor() { this.combatPluginManager = null; this.gameEventService = null; this.combatService = null; } /** * Initialize controller with dependencies * @param {Object} dependencies - Service dependencies */ async initialize(dependencies = {}) { this.gameEventService = dependencies.gameEventService || new GameEventService(); this.combatPluginManager = dependencies.combatPluginManager || new CombatPluginManager(); this.combatService = dependencies.combatService || new CombatService(this.gameEventService, this.combatPluginManager); // Initialize plugin manager await this.combatPluginManager.initialize('controller-init'); } /** * Initiate combat between fleets or fleet vs colony * POST /api/combat/initiate */ async initiateCombat(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; const combatData = req.body; logger.info('Combat initiation request', { correlationId, playerId, combatData, }); // Validate required fields if (!combatData.attacker_fleet_id) { return res.status(400).json({ error: 'Attacker fleet ID is required', code: 'MISSING_ATTACKER_FLEET', }); } if (!combatData.location) { return res.status(400).json({ error: 'Combat location is required', code: 'MISSING_LOCATION', }); } if (!combatData.defender_fleet_id && !combatData.defender_colony_id) { return res.status(400).json({ error: 'Either defender fleet or colony must be specified', code: 'MISSING_DEFENDER', }); } // Initialize services if not already done if (!this.combatService) { await this.initialize(); } // Initiate combat const result = await this.combatService.initiateCombat(combatData, playerId, correlationId); logger.info('Combat initiated successfully', { correlationId, playerId, battleId: result.battleId, }); res.status(201).json({ success: true, data: result, message: 'Combat initiated successfully', }); } catch (error) { logger.error('Combat initiation failed', { correlationId: req.correlationId, playerId: req.user?.id, error: error.message, stack: error.stack, }); if (error instanceof ValidationError) { return res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR', }); } if (error instanceof ConflictError) { return res.status(409).json({ error: error.message, code: 'CONFLICT_ERROR', }); } if (error instanceof NotFoundError) { return res.status(404).json({ error: error.message, code: 'NOT_FOUND', }); } next(error); } } /** * Get active combats for the current player * GET /api/combat/active */ async getActiveCombats(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; logger.info('Active combats request', { correlationId, playerId, }); if (!this.combatService) { await this.initialize(); } const activeCombats = await this.combatService.getActiveCombats(playerId, correlationId); logger.info('Active combats retrieved', { correlationId, playerId, count: activeCombats.length, }); res.json({ success: true, data: { combats: activeCombats, count: activeCombats.length, }, }); } catch (error) { logger.error('Failed to get active combats', { correlationId: req.correlationId, playerId: req.user?.id, error: error.message, stack: error.stack, }); next(error); } } /** * Get combat history for the current player * GET /api/combat/history */ async getCombatHistory(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; // Parse query parameters const options = { limit: parseInt(req.query.limit) || 20, offset: parseInt(req.query.offset) || 0, outcome: req.query.outcome || null, }; // Validate parameters if (options.limit > 100) { return res.status(400).json({ error: 'Limit cannot exceed 100', code: 'INVALID_LIMIT', }); } if (options.outcome && !['attacker_victory', 'defender_victory', 'draw'].includes(options.outcome)) { return res.status(400).json({ error: 'Invalid outcome filter', code: 'INVALID_OUTCOME', }); } logger.info('Combat history request', { correlationId, playerId, options, }); if (!this.combatService) { await this.initialize(); } const history = await this.combatService.getCombatHistory(playerId, options, correlationId); logger.info('Combat history retrieved', { correlationId, playerId, count: history.combats.length, total: history.pagination.total, }); res.json({ success: true, data: history, }); } catch (error) { logger.error('Failed to get combat history', { correlationId: req.correlationId, playerId: req.user?.id, error: error.message, stack: error.stack, }); next(error); } } /** * Get detailed combat encounter information * GET /api/combat/encounter/:encounterId */ async getCombatEncounter(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; const encounterId = parseInt(req.params.encounterId); if (!encounterId || isNaN(encounterId)) { return res.status(400).json({ error: 'Valid encounter ID is required', code: 'INVALID_ENCOUNTER_ID', }); } logger.info('Combat encounter request', { correlationId, playerId, encounterId, }); if (!this.combatService) { await this.initialize(); } const encounter = await this.combatService.getCombatEncounter(encounterId, playerId, correlationId); if (!encounter) { return res.status(404).json({ error: 'Combat encounter not found or access denied', code: 'ENCOUNTER_NOT_FOUND', }); } logger.info('Combat encounter retrieved', { correlationId, playerId, encounterId, }); res.json({ success: true, data: encounter, }); } catch (error) { logger.error('Failed to get combat encounter', { correlationId: req.correlationId, playerId: req.user?.id, encounterId: req.params.encounterId, error: error.message, stack: error.stack, }); next(error); } } /** * Get combat statistics for the current player * GET /api/combat/statistics */ async getCombatStatistics(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; logger.info('Combat statistics request', { correlationId, playerId, }); if (!this.combatService) { await this.initialize(); } const statistics = await this.combatService.getCombatStatistics(playerId, correlationId); logger.info('Combat statistics retrieved', { correlationId, playerId, }); res.json({ success: true, data: statistics, }); } catch (error) { logger.error('Failed to get combat statistics', { correlationId: req.correlationId, playerId: req.user?.id, error: error.message, stack: error.stack, }); next(error); } } /** * Update fleet positioning for tactical combat * PUT /api/combat/position/:fleetId */ async updateFleetPosition(req, res, next) { try { const correlationId = req.correlationId; const playerId = req.user.id; const fleetId = parseInt(req.params.fleetId); const positionData = req.body; if (!fleetId || isNaN(fleetId)) { return res.status(400).json({ error: 'Valid fleet ID is required', code: 'INVALID_FLEET_ID', }); } logger.info('Fleet position update request', { correlationId, playerId, fleetId, positionData, }); if (!this.combatService) { await this.initialize(); } const result = await this.combatService.updateFleetPosition(fleetId, positionData, playerId, correlationId); logger.info('Fleet position updated', { correlationId, playerId, fleetId, }); res.json({ success: true, data: result, message: 'Fleet position updated successfully', }); } catch (error) { logger.error('Failed to update fleet position', { correlationId: req.correlationId, playerId: req.user?.id, fleetId: req.params.fleetId, error: error.message, stack: error.stack, }); if (error instanceof ValidationError) { return res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR', }); } if (error instanceof NotFoundError) { return res.status(404).json({ error: error.message, code: 'NOT_FOUND', }); } next(error); } } /** * Get available combat types and configurations * GET /api/combat/types */ async getCombatTypes(req, res, next) { try { const correlationId = req.correlationId; logger.info('Combat types request', { correlationId }); if (!this.combatService) { await this.initialize(); } const combatTypes = await this.combatService.getAvailableCombatTypes(correlationId); logger.info('Combat types retrieved', { correlationId, count: combatTypes.length, }); res.json({ success: true, data: combatTypes, }); } catch (error) { logger.error('Failed to get combat types', { correlationId: req.correlationId, error: error.message, stack: error.stack, }); next(error); } } /** * Force resolve a combat (admin only) * POST /api/combat/resolve/:battleId */ async forceResolveCombat(req, res, next) { try { const correlationId = req.correlationId; const battleId = parseInt(req.params.battleId); if (!battleId || isNaN(battleId)) { return res.status(400).json({ error: 'Valid battle ID is required', code: 'INVALID_BATTLE_ID', }); } logger.info('Force resolve combat request', { correlationId, battleId, adminUser: req.user?.id, }); if (!this.combatService) { await this.initialize(); } const result = await this.combatService.processCombat(battleId, correlationId); logger.info('Combat force resolved', { correlationId, battleId, outcome: result.outcome, }); res.json({ success: true, data: result, message: 'Combat resolved successfully', }); } catch (error) { logger.error('Failed to force resolve combat', { correlationId: req.correlationId, battleId: req.params.battleId, error: error.message, stack: error.stack, }); if (error instanceof NotFoundError) { return res.status(404).json({ error: error.message, code: 'NOT_FOUND', }); } if (error instanceof ConflictError) { return res.status(409).json({ error: error.message, code: 'CONFLICT_ERROR', }); } next(error); } } /** * Get combat queue status (admin only) * GET /api/combat/queue */ async getCombatQueue(req, res, next) { try { const correlationId = req.correlationId; const status = req.query.status || null; const limit = parseInt(req.query.limit) || 50; logger.info('Combat queue request', { correlationId, status, limit, adminUser: req.user?.id, }); if (!this.combatService) { await this.initialize(); } const queue = await this.combatService.getCombatQueue({ status, limit }, correlationId); logger.info('Combat queue retrieved', { correlationId, count: queue.length, }); res.json({ success: true, data: queue, }); } catch (error) { logger.error('Failed to get combat queue', { correlationId: req.correlationId, error: error.message, stack: error.stack, }); next(error); } } } // Export singleton instance const combatController = new CombatController(); module.exports = { CombatController, // Export bound methods for route usage initiateCombat: combatController.initiateCombat.bind(combatController), getActiveCombats: combatController.getActiveCombats.bind(combatController), getCombatHistory: combatController.getCombatHistory.bind(combatController), getCombatEncounter: combatController.getCombatEncounter.bind(combatController), getCombatStatistics: combatController.getCombatStatistics.bind(combatController), updateFleetPosition: combatController.updateFleetPosition.bind(combatController), getCombatTypes: combatController.getCombatTypes.bind(combatController), forceResolveCombat: combatController.forceResolveCombat.bind(combatController), getCombatQueue: combatController.getCombatQueue.bind(combatController), };