feat: implement comprehensive combat system with plugin architecture

- Complete combat system with instant, turn-based, and tactical combat
- Plugin-based architecture with CombatPluginManager for extensibility
- Real-time combat events via WebSocket
- Fleet vs fleet and fleet vs colony combat support
- Comprehensive combat statistics and history tracking
- Admin panel for combat management and configuration
- Database migrations for combat tables and fleet system
- Complete test suite for combat functionality
- Combat middleware for validation and logging
- Service locator pattern for dependency management

Combat system features:
• Multiple combat resolution types with plugin support
• Real-time combat events and spectator support
• Detailed combat logs and casualty calculations
• Experience gain and veterancy system for ships
• Fleet positioning and tactical formations
• Combat configurations and modifiers
• Queue system for battle processing
• Comprehensive admin controls and monitoring

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MegaProxy 2025-08-02 14:02:04 +00:00
parent 1a60cf55a3
commit 8d9ef427be
37 changed files with 13302 additions and 26 deletions

View file

@ -0,0 +1,572 @@
/**
* 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)
};