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

@ -40,7 +40,8 @@ router.get('/', (req, res) => {
players: '/api/admin/players',
system: '/api/admin/system',
events: '/api/admin/events',
analytics: '/api/admin/analytics'
analytics: '/api/admin/analytics',
combat: '/api/admin/combat'
},
note: 'Administrative access required for all endpoints'
});
@ -336,6 +337,12 @@ systemRoutes.get('/health',
// Mount system routes
router.use('/system', systemRoutes);
/**
* Combat Management Routes
* /api/admin/combat/*
*/
router.use('/combat', require('./admin/combat'));
/**
* Events Management Routes (placeholder)
* /api/admin/events/*

345
src/routes/admin/combat.js Normal file
View file

@ -0,0 +1,345 @@
/**
* Admin Combat Routes
* Administrative endpoints for combat system management
*/
const express = require('express');
const router = express.Router();
// Import controllers
const {
getCombatStatistics,
getCombatQueue,
forceResolveCombat,
cancelBattle,
getCombatConfigurations,
saveCombatConfiguration,
deleteCombatConfiguration
} = require('../../controllers/admin/combat.controller');
// Import middleware
const { authenticateAdmin } = require('../../middleware/admin.middleware');
const {
validateCombatQueueQuery,
validateParams,
logCombatAction
} = require('../../middleware/combat.middleware');
const { validateCombatConfiguration } = require('../../validators/combat.validators');
// Apply admin authentication to all routes
router.use(authenticateAdmin);
/**
* @route GET /api/admin/combat/statistics
* @desc Get comprehensive combat system statistics
* @access Admin
*/
router.get('/statistics',
logCombatAction('admin_get_combat_statistics'),
getCombatStatistics
);
/**
* @route GET /api/admin/combat/queue
* @desc Get combat queue with filtering options
* @access Admin
*/
router.get('/queue',
logCombatAction('admin_get_combat_queue'),
validateCombatQueueQuery,
getCombatQueue
);
/**
* @route POST /api/admin/combat/resolve/:battleId
* @desc Force resolve a specific battle
* @access Admin
*/
router.post('/resolve/:battleId',
logCombatAction('admin_force_resolve_combat'),
validateParams('battleId'),
forceResolveCombat
);
/**
* @route POST /api/admin/combat/cancel/:battleId
* @desc Cancel a battle
* @access Admin
*/
router.post('/cancel/:battleId',
logCombatAction('admin_cancel_battle'),
validateParams('battleId'),
(req, res, next) => {
// Validate cancel reason in request body
const { reason } = req.body;
if (!reason || typeof reason !== 'string' || reason.trim().length < 5) {
return res.status(400).json({
error: 'Cancel reason is required and must be at least 5 characters',
code: 'INVALID_CANCEL_REASON'
});
}
next();
},
cancelBattle
);
/**
* @route GET /api/admin/combat/configurations
* @desc Get all combat configurations
* @access Admin
*/
router.get('/configurations',
logCombatAction('admin_get_combat_configurations'),
getCombatConfigurations
);
/**
* @route POST /api/admin/combat/configurations
* @desc Create new combat configuration
* @access Admin
*/
router.post('/configurations',
logCombatAction('admin_create_combat_configuration'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details
});
}
req.body = value;
next();
},
saveCombatConfiguration
);
/**
* @route PUT /api/admin/combat/configurations/:configId
* @desc Update existing combat configuration
* @access Admin
*/
router.put('/configurations/:configId',
logCombatAction('admin_update_combat_configuration'),
validateParams('configId'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details
});
}
req.body = value;
next();
},
saveCombatConfiguration
);
/**
* @route DELETE /api/admin/combat/configurations/:configId
* @desc Delete combat configuration
* @access Admin
*/
router.delete('/configurations/:configId',
logCombatAction('admin_delete_combat_configuration'),
validateParams('configId'),
deleteCombatConfiguration
);
/**
* @route GET /api/admin/combat/battles
* @desc Get all battles with filtering and pagination
* @access Admin
*/
router.get('/battles',
logCombatAction('admin_get_battles'),
async (req, res, next) => {
try {
const {
status,
battle_type,
location,
limit = 50,
offset = 0,
start_date,
end_date
} = req.query;
const db = require('../../database/connection');
const logger = require('../../utils/logger');
let query = db('battles')
.select([
'battles.*',
'combat_configurations.config_name',
'combat_configurations.combat_type'
])
.leftJoin('combat_configurations', 'battles.combat_configuration_id', 'combat_configurations.id')
.orderBy('battles.started_at', 'desc')
.limit(parseInt(limit))
.offset(parseInt(offset));
if (status) {
query = query.where('battles.status', status);
}
if (battle_type) {
query = query.where('battles.battle_type', battle_type);
}
if (location) {
query = query.where('battles.location', location);
}
if (start_date) {
query = query.where('battles.started_at', '>=', new Date(start_date));
}
if (end_date) {
query = query.where('battles.started_at', '<=', new Date(end_date));
}
const battles = await query;
// Get total count for pagination
let countQuery = db('battles').count('* as total');
if (status) countQuery = countQuery.where('status', status);
if (battle_type) countQuery = countQuery.where('battle_type', battle_type);
if (location) countQuery = countQuery.where('location', location);
if (start_date) countQuery = countQuery.where('started_at', '>=', new Date(start_date));
if (end_date) countQuery = countQuery.where('started_at', '<=', new Date(end_date));
const [{ total }] = await countQuery;
// Parse participants JSON for each battle
const battlesWithParsedParticipants = battles.map(battle => ({
...battle,
participants: JSON.parse(battle.participants),
battle_data: battle.battle_data ? JSON.parse(battle.battle_data) : null,
result: battle.result ? JSON.parse(battle.result) : null
}));
logger.info('Admin battles retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
count: battles.length,
total: parseInt(total)
});
res.json({
success: true,
data: {
battles: battlesWithParsedParticipants,
pagination: {
total: parseInt(total),
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: (parseInt(offset) + parseInt(limit)) < parseInt(total)
}
}
});
} catch (error) {
next(error);
}
}
);
/**
* @route GET /api/admin/combat/encounters/:encounterId
* @desc Get detailed combat encounter for admin review
* @access Admin
*/
router.get('/encounters/:encounterId',
logCombatAction('admin_get_combat_encounter'),
validateParams('encounterId'),
async (req, res, next) => {
try {
const encounterId = parseInt(req.params.encounterId);
const db = require('../../database/connection');
const logger = require('../../utils/logger');
// Get encounter with all related data
const encounter = await db('combat_encounters')
.select([
'combat_encounters.*',
'battles.battle_type',
'battles.participants',
'battles.started_at as battle_started',
'battles.completed_at as battle_completed',
'attacker_fleet.name as attacker_fleet_name',
'attacker_player.username as attacker_username',
'defender_fleet.name as defender_fleet_name',
'defender_player.username as defender_username',
'defender_colony.name as defender_colony_name',
'colony_player.username as colony_owner_username'
])
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('players as attacker_player', 'attacker_fleet.player_id', 'attacker_player.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('players as defender_player', 'defender_fleet.player_id', 'defender_player.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.leftJoin('players as colony_player', 'defender_colony.player_id', 'colony_player.id')
.where('combat_encounters.id', encounterId)
.first();
if (!encounter) {
return res.status(404).json({
error: 'Combat encounter not found',
code: 'ENCOUNTER_NOT_FOUND'
});
}
// Get combat logs
const combatLogs = await db('combat_logs')
.where('encounter_id', encounterId)
.orderBy('round_number')
.orderBy('timestamp');
const detailedEncounter = {
...encounter,
participants: JSON.parse(encounter.participants),
initial_forces: JSON.parse(encounter.initial_forces),
final_forces: JSON.parse(encounter.final_forces),
casualties: JSON.parse(encounter.casualties),
combat_log: JSON.parse(encounter.combat_log),
loot_awarded: JSON.parse(encounter.loot_awarded),
detailed_logs: combatLogs.map(log => ({
...log,
event_data: JSON.parse(log.event_data)
}))
};
logger.info('Admin combat encounter retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
encounterId
});
res.json({
success: true,
data: detailedEncounter
});
} catch (error) {
next(error);
}
}
);
module.exports = router;

View file

@ -0,0 +1,586 @@
/**
* Admin System Management Routes
* Provides administrative controls for game tick system, configuration, and monitoring
*/
const express = require('express');
const router = express.Router();
const logger = require('../../utils/logger');
const {
gameTickService,
getGameTickStatus,
triggerManualTick
} = require('../../services/game-tick.service');
const db = require('../../database/connection');
const { v4: uuidv4 } = require('uuid');
/**
* Get game tick system status and metrics
* GET /admin/system/tick/status
*/
router.get('/tick/status', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin requesting game tick status', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
});
const status = getGameTickStatus();
// Get recent tick logs
const recentLogs = await db('game_tick_log')
.select('*')
.orderBy('tick_number', 'desc')
.limit(10);
// Get performance statistics
const performanceStats = await db('game_tick_log')
.select(
db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms'),
db.raw('COUNT(*) as total_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'),
db.raw('MAX(tick_number) as latest_tick')
)
.where('started_at', '>=', db.raw("NOW() - INTERVAL '24 hours'"))
.first();
// Get user group statistics
const userGroupStats = await db('game_tick_log')
.select(
'user_group',
db.raw('COUNT(*) as tick_count'),
db.raw('AVG(processed_players) as avg_players'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failures')
)
.where('started_at', '>=', db.raw("NOW() - INTERVAL '24 hours'"))
.groupBy('user_group')
.orderBy('user_group');
res.json({
success: true,
data: {
service: status,
performance: performanceStats,
userGroups: userGroupStats,
recentLogs: recentLogs.map(log => ({
id: log.id,
tickNumber: log.tick_number,
userGroup: log.user_group,
status: log.status,
processedPlayers: log.processed_players,
duration: log.performance_metrics?.duration_ms,
startedAt: log.started_at,
completedAt: log.completed_at,
errorMessage: log.error_message
}))
},
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to get game tick status', {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: 'Failed to retrieve game tick status',
correlationId
});
}
});
/**
* Trigger manual game tick
* POST /admin/system/tick/trigger
*/
router.post('/tick/trigger', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin triggering manual game tick', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
});
const result = await triggerManualTick(correlationId);
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
entity_id: 0,
action: 'manual_tick_triggered',
actor_type: 'admin',
actor_id: req.user?.id,
changes: {
correlation_id: correlationId,
triggered_by: req.user?.username
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Manual game tick triggered successfully',
data: result,
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to trigger manual game tick', {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: error.message || 'Failed to trigger manual game tick',
correlationId
});
}
});
/**
* Update game tick configuration
* PUT /admin/system/tick/config
*/
router.put('/tick/config', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const {
tick_interval_ms,
user_groups_count,
max_retry_attempts,
bonus_tick_threshold,
retry_delay_ms
} = req.body;
logger.info('Admin updating game tick configuration', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username,
newConfig: req.body
});
// Validate configuration values
const validationErrors = [];
if (tick_interval_ms && (tick_interval_ms < 10000 || tick_interval_ms > 3600000)) {
validationErrors.push('tick_interval_ms must be between 10000 and 3600000 (10 seconds to 1 hour)');
}
if (user_groups_count && (user_groups_count < 1 || user_groups_count > 50)) {
validationErrors.push('user_groups_count must be between 1 and 50');
}
if (max_retry_attempts && (max_retry_attempts < 1 || max_retry_attempts > 10)) {
validationErrors.push('max_retry_attempts must be between 1 and 10');
}
if (validationErrors.length > 0) {
return res.status(400).json({
success: false,
error: 'Configuration validation failed',
details: validationErrors,
correlationId
});
}
// Get current configuration
const currentConfig = await db('game_tick_config')
.where('is_active', true)
.first();
if (!currentConfig) {
return res.status(404).json({
success: false,
error: 'No active game tick configuration found',
correlationId
});
}
// Update configuration
const updatedConfig = await db('game_tick_config')
.where('id', currentConfig.id)
.update({
tick_interval_ms: tick_interval_ms || currentConfig.tick_interval_ms,
user_groups_count: user_groups_count || currentConfig.user_groups_count,
max_retry_attempts: max_retry_attempts || currentConfig.max_retry_attempts,
bonus_tick_threshold: bonus_tick_threshold || currentConfig.bonus_tick_threshold,
retry_delay_ms: retry_delay_ms || currentConfig.retry_delay_ms,
updated_at: new Date()
})
.returning('*');
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick_config',
entity_id: currentConfig.id,
action: 'configuration_updated',
actor_type: 'admin',
actor_id: req.user?.id,
changes: {
before: currentConfig,
after: updatedConfig[0],
updated_by: req.user?.username
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
});
// Reload configuration in the service
await gameTickService.loadConfig();
res.json({
success: true,
message: 'Game tick configuration updated successfully',
data: {
previousConfig: currentConfig,
newConfig: updatedConfig[0]
},
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to update game tick configuration', {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: 'Failed to update game tick configuration',
correlationId
});
}
});
/**
* Get game tick logs with filtering
* GET /admin/system/tick/logs
*/
router.get('/tick/logs', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const {
page = 1,
limit = 50,
status,
userGroup,
tickNumber,
startDate,
endDate
} = req.query;
const pageNum = parseInt(page);
const limitNum = Math.min(parseInt(limit), 100); // Max 100 records per page
const offset = (pageNum - 1) * limitNum;
let query = db('game_tick_log').select('*');
// Apply filters
if (status) {
query = query.where('status', status);
}
if (userGroup !== undefined) {
query = query.where('user_group', parseInt(userGroup));
}
if (tickNumber) {
query = query.where('tick_number', parseInt(tickNumber));
}
if (startDate) {
query = query.where('started_at', '>=', new Date(startDate));
}
if (endDate) {
query = query.where('started_at', '<=', new Date(endDate));
}
// Get total count for pagination
const countQuery = query.clone().clearSelect().count('* as total');
const [{ total }] = await countQuery;
// Get paginated results
const logs = await query
.orderBy('tick_number', 'desc')
.orderBy('user_group', 'asc')
.limit(limitNum)
.offset(offset);
res.json({
success: true,
data: {
logs: logs.map(log => ({
id: log.id,
tickNumber: log.tick_number,
userGroup: log.user_group,
status: log.status,
processedPlayers: log.processed_players,
retryCount: log.retry_count,
errorMessage: log.error_message,
performanceMetrics: log.performance_metrics,
startedAt: log.started_at,
completedAt: log.completed_at
})),
pagination: {
page: pageNum,
limit: limitNum,
total: parseInt(total),
pages: Math.ceil(total / limitNum)
}
},
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to get game tick logs', {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: 'Failed to retrieve game tick logs',
correlationId
});
}
});
/**
* Get system performance metrics
* GET /admin/system/performance
*/
router.get('/performance', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const { timeRange = '24h' } = req.query;
let interval;
switch (timeRange) {
case '1h':
interval = "1 hour";
break;
case '24h':
interval = "24 hours";
break;
case '7d':
interval = "7 days";
break;
case '30d':
interval = "30 days";
break;
default:
interval = "24 hours";
}
// Get tick performance metrics
const tickMetrics = await db('game_tick_log')
.select(
db.raw('DATE_TRUNC(\'hour\', started_at) as hour'),
db.raw('COUNT(*) as total_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'),
db.raw('AVG(processed_players) as avg_players_processed'),
db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms')
)
.where('started_at', '>=', db.raw(`NOW() - INTERVAL '${interval}'`))
.groupBy(db.raw('DATE_TRUNC(\'hour\', started_at)'))
.orderBy('hour');
// Get database performance metrics
const dbMetrics = await db.raw(`
SELECT
schemaname,
tablename,
n_tup_ins as inserts,
n_tup_upd as updates,
n_tup_del as deletes,
seq_scan as sequential_scans,
idx_scan as index_scans
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC
LIMIT 10
`);
// Get active player count
const playerStats = await db('players')
.select(
db.raw('COUNT(*) FILTER (WHERE is_active = true) as active_players'),
db.raw('COUNT(*) FILTER (WHERE last_login >= NOW() - INTERVAL \'24 hours\') as recent_players'),
db.raw('COUNT(*) as total_players')
)
.first();
res.json({
success: true,
data: {
timeRange,
tickMetrics: tickMetrics.map(metric => ({
hour: metric.hour,
totalTicks: parseInt(metric.total_ticks),
successfulTicks: parseInt(metric.successful_ticks),
failedTicks: parseInt(metric.failed_ticks),
successRate: metric.total_ticks > 0 ?
((metric.successful_ticks / metric.total_ticks) * 100).toFixed(2) : 0,
avgPlayersProcessed: parseFloat(metric.avg_players_processed || 0).toFixed(1),
avgDurationMs: parseFloat(metric.avg_duration_ms || 0).toFixed(2)
})),
databaseMetrics: dbMetrics.rows,
playerStats
},
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to get system performance metrics', {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: 'Failed to retrieve performance metrics',
correlationId
});
}
});
/**
* Stop game tick service
* POST /admin/system/tick/stop
*/
router.post('/tick/stop', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.warn('Admin stopping game tick service', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
});
gameTickService.stop();
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
entity_id: 0,
action: 'service_stopped',
actor_type: 'admin',
actor_id: req.user?.id,
changes: {
correlation_id: correlationId,
stopped_by: req.user?.username,
timestamp: new Date().toISOString()
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Game tick service stopped successfully',
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to stop game tick service', {
correlationId,
adminId: req.user?.id,
error: error.message
});
res.status(500).json({
success: false,
error: 'Failed to stop game tick service',
correlationId
});
}
});
/**
* Start game tick service
* POST /admin/system/tick/start
*/
router.post('/tick/start', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin starting game tick service', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
});
await gameTickService.initialize();
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
entity_id: 0,
action: 'service_started',
actor_type: 'admin',
actor_id: req.user?.id,
changes: {
correlation_id: correlationId,
started_by: req.user?.username,
timestamp: new Date().toISOString()
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Game tick service started successfully',
data: gameTickService.getStatus(),
timestamp: new Date().toISOString(),
correlationId
});
} catch (error) {
logger.error('Failed to start game tick service', {
correlationId,
adminId: req.user?.id,
error: error.message
});
res.status(500).json({
success: false,
error: error.message || 'Failed to start game tick service',
correlationId
});
}
});
module.exports = router;

View file

@ -39,7 +39,8 @@ router.get('/', (req, res) => {
colonies: '/api/colonies',
fleets: '/api/fleets',
research: '/api/research',
galaxy: '/api/galaxy'
galaxy: '/api/galaxy',
combat: '/api/combat'
}
}
});
@ -162,6 +163,12 @@ playerRoutes.put('/notifications/read',
// Mount player routes
router.use('/player', playerRoutes);
/**
* Combat Routes
* /api/combat/*
*/
router.use('/combat', require('./api/combat'));
/**
* Game Feature Routes
* These will be expanded with actual game functionality

130
src/routes/api/combat.js Normal file
View file

@ -0,0 +1,130 @@
/**
* Combat API Routes
* Defines all combat-related endpoints for players
*/
const express = require('express');
const router = express.Router();
// Import controllers
const {
initiateCombat,
getActiveCombats,
getCombatHistory,
getCombatEncounter,
getCombatStatistics,
updateFleetPosition,
getCombatTypes,
forceResolveCombat
} = require('../../controllers/api/combat.controller');
// Import middleware
const { authenticatePlayer } = require('../../middleware/auth.middleware');
const {
validateCombatInitiation,
validateFleetPositionUpdate,
validateCombatHistoryQuery,
validateParams,
checkFleetOwnership,
checkBattleAccess,
checkCombatCooldown,
checkFleetAvailability,
combatRateLimit,
logCombatAction
} = require('../../middleware/combat.middleware');
// Apply authentication to all combat routes
router.use(authenticatePlayer);
/**
* @route POST /api/combat/initiate
* @desc Initiate combat between fleets or fleet vs colony
* @access Private
*/
router.post('/initiate',
logCombatAction('initiate_combat'),
combatRateLimit(5, 15), // Max 5 combat initiations per 15 minutes
checkCombatCooldown,
validateCombatInitiation,
checkFleetAvailability,
initiateCombat
);
/**
* @route GET /api/combat/active
* @desc Get active combats for the current player
* @access Private
*/
router.get('/active',
logCombatAction('get_active_combats'),
getActiveCombats
);
/**
* @route GET /api/combat/history
* @desc Get combat history for the current player
* @access Private
*/
router.get('/history',
logCombatAction('get_combat_history'),
validateCombatHistoryQuery,
getCombatHistory
);
/**
* @route GET /api/combat/encounter/:encounterId
* @desc Get detailed combat encounter information
* @access Private
*/
router.get('/encounter/:encounterId',
logCombatAction('get_combat_encounter'),
validateParams('encounterId'),
getCombatEncounter
);
/**
* @route GET /api/combat/statistics
* @desc Get combat statistics for the current player
* @access Private
*/
router.get('/statistics',
logCombatAction('get_combat_statistics'),
getCombatStatistics
);
/**
* @route PUT /api/combat/position/:fleetId
* @desc Update fleet positioning for tactical combat
* @access Private
*/
router.put('/position/:fleetId',
logCombatAction('update_fleet_position'),
validateParams('fleetId'),
checkFleetOwnership,
validateFleetPositionUpdate,
updateFleetPosition
);
/**
* @route GET /api/combat/types
* @desc Get available combat types and configurations
* @access Private
*/
router.get('/types',
logCombatAction('get_combat_types'),
getCombatTypes
);
/**
* @route POST /api/combat/resolve/:battleId
* @desc Force resolve a combat (emergency use only)
* @access Private (requires special permission)
*/
router.post('/resolve/:battleId',
logCombatAction('force_resolve_combat'),
validateParams('battleId'),
checkBattleAccess,
forceResolveCombat
);
module.exports = router;

View file

@ -35,7 +35,10 @@ router.get('/', (req, res) => {
websocket: '/debug/websocket',
system: '/debug/system',
logs: '/debug/logs',
player: '/debug/player/:playerId'
player: '/debug/player/:playerId',
colonies: '/debug/colonies',
resources: '/debug/resources',
gameEvents: '/debug/game-events'
}
});
});
@ -311,4 +314,243 @@ router.get('/test/:scenario', (req, res) => {
}
});
/**
* Colony Debug Information
*/
router.get('/colonies', async (req, res) => {
try {
const { playerId, limit = 10 } = req.query;
let query = db('colonies')
.select([
'colonies.*',
'planet_types.name as planet_type_name',
'galaxy_sectors.name as sector_name',
'players.username'
])
.leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id')
.leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id')
.leftJoin('players', 'colonies.player_id', 'players.id')
.orderBy('colonies.founded_at', 'desc')
.limit(parseInt(limit));
if (playerId) {
query = query.where('colonies.player_id', parseInt(playerId));
}
const colonies = await query;
// Get building counts for each colony
const coloniesWithBuildings = await Promise.all(colonies.map(async (colony) => {
const buildingCount = await db('colony_buildings')
.where('colony_id', colony.id)
.count('* as count')
.first();
const resourceProduction = await db('colony_resource_production')
.select([
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored'
])
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colony_resource_production.colony_id', colony.id)
.where('colony_resource_production.production_rate', '>', 0);
return {
...colony,
buildingCount: parseInt(buildingCount.count) || 0,
resourceProduction
};
}));
res.json({
colonies: coloniesWithBuildings,
totalCount: coloniesWithBuildings.length,
filters: { playerId, limit },
correlationId: req.correlationId
});
} catch (error) {
logger.error('Colony debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* Resource Debug Information
*/
router.get('/resources', async (req, res) => {
try {
const { playerId } = req.query;
// Get resource types
const resourceTypes = await db('resource_types')
.where('is_active', true)
.orderBy('category')
.orderBy('name');
let resourceSummary = {};
if (playerId) {
// Get specific player resources
const playerResources = await db('player_resources')
.select([
'player_resources.*',
'resource_types.name as resource_name',
'resource_types.category'
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.where('player_resources.player_id', parseInt(playerId));
resourceSummary.playerResources = playerResources;
// Get player's colony resource production
const colonyProduction = await db('colony_resource_production')
.select([
'colonies.name as colony_name',
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored'
])
.join('colonies', 'colony_resource_production.colony_id', 'colonies.id')
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colonies.player_id', parseInt(playerId))
.where('colony_resource_production.production_rate', '>', 0);
resourceSummary.colonyProduction = colonyProduction;
} else {
// Get global resource statistics
const totalResources = await db('player_resources')
.select([
'resource_types.name as resource_name',
db.raw('SUM(player_resources.amount) as total_amount'),
db.raw('COUNT(player_resources.id) as player_count'),
db.raw('AVG(player_resources.amount) as average_amount')
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.groupBy('resource_types.id', 'resource_types.name')
.orderBy('resource_types.name');
resourceSummary.globalStats = totalResources;
}
res.json({
resourceTypes,
...resourceSummary,
filters: { playerId },
correlationId: req.correlationId
});
} catch (error) {
logger.error('Resource debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* Game Events Debug Information
*/
router.get('/game-events', (req, res) => {
try {
const serviceLocator = require('../services/ServiceLocator');
const gameEventService = serviceLocator.get('gameEventService');
if (!gameEventService) {
return res.json({
status: 'not_available',
message: 'Game event service not initialized',
correlationId: req.correlationId
});
}
const connectedPlayers = gameEventService.getConnectedPlayerCount();
// Get room information
const io = gameEventService.io;
const rooms = Array.from(io.sockets.adapter.rooms.entries()).map(([roomName, socketSet]) => ({
name: roomName,
socketCount: socketSet.size,
type: roomName.includes(':') ? roomName.split(':')[0] : 'unknown'
}));
res.json({
status: 'active',
connectedPlayers,
rooms: {
total: rooms.length,
breakdown: rooms
},
eventTypes: [
'colony_created',
'building_constructed',
'resources_updated',
'resource_production',
'colony_status_update',
'error',
'notification',
'player_status_change',
'system_announcement'
],
correlationId: req.correlationId
});
} catch (error) {
logger.error('Game events debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* Add resources to a player (for testing)
*/
router.post('/add-resources', async (req, res) => {
try {
const { playerId, resources } = req.body;
if (!playerId || !resources) {
return res.status(400).json({
error: 'playerId and resources are required',
correlationId: req.correlationId
});
}
const serviceLocator = require('../services/ServiceLocator');
const ResourceService = require('../services/resource/ResourceService');
const gameEventService = serviceLocator.get('gameEventService');
const resourceService = new ResourceService(gameEventService);
const updatedResources = await resourceService.addPlayerResources(
playerId,
resources,
req.correlationId
);
res.json({
success: true,
message: 'Resources added successfully',
playerId,
addedResources: resources,
updatedResources,
correlationId: req.correlationId
});
} catch (error) {
logger.error('Add resources debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
}
});
module.exports = router;

View file

@ -0,0 +1,53 @@
/**
* Player Colony Routes
* Handles all colony-related endpoints for players
*/
const express = require('express');
const router = express.Router();
const {
createColony,
getPlayerColonies,
getColonyDetails,
constructBuilding,
getBuildingTypes,
getPlanetTypes,
getGalaxySectors
} = require('../../controllers/player/colony.controller');
const { validateRequest } = require('../../middleware/validation.middleware');
const {
createColonySchema,
constructBuildingSchema,
colonyIdParamSchema
} = require('../../validators/colony.validators');
// Colony CRUD operations
router.post('/',
validateRequest(createColonySchema),
createColony
);
router.get('/',
getPlayerColonies
);
router.get('/:colonyId',
validateRequest(colonyIdParamSchema, 'params'),
getColonyDetails
);
// Building operations
router.post('/:colonyId/buildings',
validateRequest(colonyIdParamSchema, 'params'),
validateRequest(constructBuildingSchema),
constructBuilding
);
// Reference data endpoints
router.get('/ref/building-types', getBuildingTypes);
router.get('/ref/planet-types', getPlanetTypes);
router.get('/ref/galaxy-sectors', getGalaxySectors);
module.exports = router;

View file

@ -4,7 +4,7 @@
const express = require('express');
const { authenticateToken, optionalAuth } = require('../../middleware/auth');
const { asyncHandler } = require('../../middleware/error-handler');
const { asyncHandler } = require('../../middleware/error.middleware');
const router = express.Router();
@ -12,6 +12,7 @@ const router = express.Router();
const authRoutes = require('./auth');
const profileRoutes = require('./profile');
const coloniesRoutes = require('./colonies');
const resourcesRoutes = require('./resources');
const fleetsRoutes = require('./fleets');
const researchRoutes = require('./research');
const galaxyRoutes = require('./galaxy');
@ -25,6 +26,7 @@ router.use('/galaxy', optionalAuth('player'), galaxyRoutes);
// Protected routes (authentication required)
router.use('/profile', authenticateToken('player'), profileRoutes);
router.use('/colonies', authenticateToken('player'), coloniesRoutes);
router.use('/resources', authenticateToken('player'), resourcesRoutes);
router.use('/fleets', authenticateToken('player'), fleetsRoutes);
router.use('/research', authenticateToken('player'), researchRoutes);
router.use('/events', authenticateToken('player'), eventsRoutes);

View file

@ -0,0 +1,54 @@
/**
* Player Resource Routes
* Handles all resource-related endpoints for players
*/
const express = require('express');
const router = express.Router();
const {
getPlayerResources,
getPlayerResourceSummary,
getResourceProduction,
addResources,
transferResources,
getResourceTypes
} = require('../../controllers/player/resource.controller');
const { validateRequest } = require('../../middleware/validation.middleware');
const {
transferResourcesSchema,
addResourcesSchema,
resourceQuerySchema
} = require('../../validators/resource.validators');
// Resource information endpoints
router.get('/',
validateRequest(resourceQuerySchema, 'query'),
getPlayerResources
);
router.get('/summary',
getPlayerResourceSummary
);
router.get('/production',
getResourceProduction
);
// Resource manipulation endpoints
router.post('/transfer',
validateRequest(transferResourcesSchema),
transferResources
);
// Development/testing endpoints
router.post('/add',
validateRequest(addResourcesSchema),
addResources
);
// Reference data endpoints
router.get('/types', getResourceTypes);
module.exports = router;