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:
parent
1a60cf55a3
commit
8d9ef427be
37 changed files with 13302 additions and 26 deletions
572
src/controllers/api/combat.controller.js
Normal file
572
src/controllers/api/combat.controller.js
Normal 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)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue