Major milestone: Frontend implementation complete for Shattered Void MMO FRONTEND IMPLEMENTATION: - React 18 + TypeScript + Vite development environment - Tailwind CSS with custom dark theme for sci-fi aesthetic - Zustand state management with authentication persistence - Socket.io WebSocket client with auto-reconnection - Protected routing with authentication guards - Responsive design with mobile-first approach AUTHENTICATION SYSTEM: - Login/register forms with comprehensive validation - JWT token management with localStorage persistence - Password strength validation and user feedback - Protected routes and authentication guards CORE GAME INTERFACE: - Colony management dashboard with real-time updates - Resource display with live production tracking - WebSocket integration for real-time game events - Navigation with connection status indicator - Toast notifications for user feedback BACKEND ENHANCEMENTS: - Complete Research System with technology tree (23 technologies) - Fleet Management System with ship designs and movement - Enhanced Authentication with email verification and password reset - Complete game tick integration for all systems - Advanced WebSocket events for real-time updates ARCHITECTURE FEATURES: - Type-safe TypeScript throughout - Component-based architecture with reusable UI elements - API client with request/response interceptors - Error handling and loading states - Performance optimized builds with code splitting Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met) Ready for: Colony management, fleet operations, research interface Next: Enhanced gameplay features and admin interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
572 lines
14 KiB
JavaScript
572 lines
14 KiB
JavaScript
/**
|
|
* 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),
|
|
};
|