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,324 @@
/**
* Combat Validation Schemas
* Joi validation schemas for combat-related endpoints
*/
const Joi = require('joi');
// Combat initiation validation schema
const initiateCombatSchema = Joi.object({
attacker_fleet_id: Joi.number().integer().positive().required()
.messages({
'number.base': 'Attacker fleet ID must be a number',
'number.integer': 'Attacker fleet ID must be an integer',
'number.positive': 'Attacker fleet ID must be positive',
'any.required': 'Attacker fleet ID is required'
}),
defender_fleet_id: Joi.number().integer().positive().allow(null)
.messages({
'number.base': 'Defender fleet ID must be a number',
'number.integer': 'Defender fleet ID must be an integer',
'number.positive': 'Defender fleet ID must be positive'
}),
defender_colony_id: Joi.number().integer().positive().allow(null)
.messages({
'number.base': 'Defender colony ID must be a number',
'number.integer': 'Defender colony ID must be an integer',
'number.positive': 'Defender colony ID must be positive'
}),
location: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required()
.messages({
'string.pattern.base': 'Location must be in format A3-91-X',
'any.required': 'Location is required'
}),
combat_type: Joi.string().valid('instant', 'turn_based', 'tactical', 'real_time').default('instant')
.messages({
'any.only': 'Combat type must be one of: instant, turn_based, tactical, real_time'
}),
tactical_settings: Joi.object({
formation: Joi.string().valid('standard', 'defensive', 'aggressive', 'flanking', 'escort').optional(),
priority_targets: Joi.array().items(Joi.string()).optional(),
engagement_range: Joi.string().valid('close', 'medium', 'long').optional(),
retreat_threshold: Joi.number().min(0).max(100).optional()
}).optional()
}).custom((value, helpers) => {
// Ensure exactly one defender is specified
const hasFleetDefender = value.defender_fleet_id !== null && value.defender_fleet_id !== undefined;
const hasColonyDefender = value.defender_colony_id !== null && value.defender_colony_id !== undefined;
if (hasFleetDefender && hasColonyDefender) {
return helpers.error('custom.bothDefenders');
}
if (!hasFleetDefender && !hasColonyDefender) {
return helpers.error('custom.noDefender');
}
return value;
}, 'defender validation').messages({
'custom.bothDefenders': 'Cannot specify both defender fleet and defender colony',
'custom.noDefender': 'Must specify either defender fleet or defender colony'
});
// Fleet position update validation schema
const updateFleetPositionSchema = Joi.object({
position_x: Joi.number().min(-1000).max(1000).default(0)
.messages({
'number.base': 'Position X must be a number',
'number.min': 'Position X must be at least -1000',
'number.max': 'Position X must be at most 1000'
}),
position_y: Joi.number().min(-1000).max(1000).default(0)
.messages({
'number.base': 'Position Y must be a number',
'number.min': 'Position Y must be at least -1000',
'number.max': 'Position Y must be at most 1000'
}),
position_z: Joi.number().min(-1000).max(1000).default(0)
.messages({
'number.base': 'Position Z must be a number',
'number.min': 'Position Z must be at least -1000',
'number.max': 'Position Z must be at most 1000'
}),
formation: Joi.string().valid('standard', 'defensive', 'aggressive', 'flanking', 'escort').default('standard')
.messages({
'any.only': 'Formation must be one of: standard, defensive, aggressive, flanking, escort'
}),
tactical_settings: Joi.object({
auto_engage: Joi.boolean().default(true),
engagement_range: Joi.string().valid('close', 'medium', 'long').default('medium'),
target_priority: Joi.string().valid('closest', 'weakest', 'strongest', 'random').default('closest'),
retreat_threshold: Joi.number().min(0).max(100).default(25),
formation_spacing: Joi.number().min(0.1).max(10.0).default(1.0)
}).default({})
});
// Combat history query parameters validation schema
const combatHistoryQuerySchema = Joi.object({
limit: Joi.number().integer().min(1).max(100).default(20)
.messages({
'number.base': 'Limit must be a number',
'number.integer': 'Limit must be an integer',
'number.min': 'Limit must be at least 1',
'number.max': 'Limit cannot exceed 100'
}),
offset: Joi.number().integer().min(0).default(0)
.messages({
'number.base': 'Offset must be a number',
'number.integer': 'Offset must be an integer',
'number.min': 'Offset must be at least 0'
}),
outcome: Joi.string().valid('attacker_victory', 'defender_victory', 'draw').optional()
.messages({
'any.only': 'Outcome must be one of: attacker_victory, defender_victory, draw'
}),
battle_type: Joi.string().valid('fleet_vs_fleet', 'fleet_vs_colony', 'siege').optional()
.messages({
'any.only': 'Battle type must be one of: fleet_vs_fleet, fleet_vs_colony, siege'
}),
start_date: Joi.date().iso().optional()
.messages({
'date.format': 'Start date must be in ISO format'
}),
end_date: Joi.date().iso().optional()
.messages({
'date.format': 'End date must be in ISO format'
})
}).custom((value, helpers) => {
// Ensure end_date is after start_date if both are provided
if (value.start_date && value.end_date && value.end_date <= value.start_date) {
return helpers.error('custom.invalidDateRange');
}
return value;
}, 'date validation').messages({
'custom.invalidDateRange': 'End date must be after start date'
});
// Combat queue query parameters validation schema
const combatQueueQuerySchema = Joi.object({
status: Joi.string().valid('pending', 'processing', 'completed', 'failed').optional()
.messages({
'any.only': 'Status must be one of: pending, processing, completed, failed'
}),
limit: Joi.number().integer().min(1).max(100).default(50)
.messages({
'number.base': 'Limit must be a number',
'number.integer': 'Limit must be an integer',
'number.min': 'Limit must be at least 1',
'number.max': 'Limit cannot exceed 100'
}),
priority_min: Joi.number().integer().min(1).max(1000).optional()
.messages({
'number.base': 'Priority minimum must be a number',
'number.integer': 'Priority minimum must be an integer',
'number.min': 'Priority minimum must be at least 1',
'number.max': 'Priority minimum cannot exceed 1000'
}),
priority_max: Joi.number().integer().min(1).max(1000).optional()
.messages({
'number.base': 'Priority maximum must be a number',
'number.integer': 'Priority maximum must be an integer',
'number.min': 'Priority maximum must be at least 1',
'number.max': 'Priority maximum cannot exceed 1000'
})
}).custom((value, helpers) => {
// Ensure priority_max is greater than priority_min if both are provided
if (value.priority_min && value.priority_max && value.priority_max <= value.priority_min) {
return helpers.error('custom.invalidPriorityRange');
}
return value;
}, 'priority validation').messages({
'custom.invalidPriorityRange': 'Priority maximum must be greater than priority minimum'
});
// Parameter validation schemas
const battleIdParamSchema = Joi.object({
battleId: Joi.number().integer().positive().required()
.messages({
'number.base': 'Battle ID must be a number',
'number.integer': 'Battle ID must be an integer',
'number.positive': 'Battle ID must be positive',
'any.required': 'Battle ID is required'
})
});
const fleetIdParamSchema = Joi.object({
fleetId: Joi.number().integer().positive().required()
.messages({
'number.base': 'Fleet ID must be a number',
'number.integer': 'Fleet ID must be an integer',
'number.positive': 'Fleet ID must be positive',
'any.required': 'Fleet ID is required'
})
});
const encounterIdParamSchema = Joi.object({
encounterId: Joi.number().integer().positive().required()
.messages({
'number.base': 'Encounter ID must be a number',
'number.integer': 'Encounter ID must be an integer',
'number.positive': 'Encounter ID must be positive',
'any.required': 'Encounter ID is required'
})
});
// Combat configuration validation schema (admin only)
const combatConfigurationSchema = Joi.object({
config_name: Joi.string().min(3).max(100).required()
.messages({
'string.min': 'Configuration name must be at least 3 characters',
'string.max': 'Configuration name cannot exceed 100 characters',
'any.required': 'Configuration name is required'
}),
combat_type: Joi.string().valid('instant', 'turn_based', 'tactical', 'real_time').required()
.messages({
'any.only': 'Combat type must be one of: instant, turn_based, tactical, real_time',
'any.required': 'Combat type is required'
}),
config_data: Joi.object({
auto_resolve: Joi.boolean().default(true),
preparation_time: Joi.number().integer().min(0).max(300).default(30),
max_rounds: Joi.number().integer().min(1).max(100).default(20),
round_duration: Joi.number().integer().min(1).max(60).default(5),
damage_variance: Joi.number().min(0).max(1).default(0.1),
experience_gain: Joi.number().min(0).max(10).default(1.0),
casualty_rate_min: Joi.number().min(0).max(1).default(0.1),
casualty_rate_max: Joi.number().min(0).max(1).default(0.8),
loot_multiplier: Joi.number().min(0).max(10).default(1.0),
spectator_limit: Joi.number().integer().min(0).max(1000).default(100),
priority: Joi.number().integer().min(1).max(1000).default(100)
}).required()
.custom((value, helpers) => {
// Ensure casualty_rate_max >= casualty_rate_min
if (value.casualty_rate_max < value.casualty_rate_min) {
return helpers.error('custom.invalidCasualtyRange');
}
return value;
}, 'casualty rate validation')
.messages({
'custom.invalidCasualtyRange': 'Maximum casualty rate must be greater than or equal to minimum casualty rate'
}),
description: Joi.string().max(500).optional()
.messages({
'string.max': 'Description cannot exceed 500 characters'
}),
is_active: Joi.boolean().default(true)
});
// Export validation functions
const validateInitiateCombat = (data) => {
return initiateCombatSchema.validate(data, { abortEarly: false });
};
const validateUpdateFleetPosition = (data) => {
return updateFleetPositionSchema.validate(data, { abortEarly: false });
};
const validateCombatHistoryQuery = (data) => {
return combatHistoryQuerySchema.validate(data, { abortEarly: false });
};
const validateCombatQueueQuery = (data) => {
return combatQueueQuerySchema.validate(data, { abortEarly: false });
};
const validateBattleIdParam = (data) => {
return battleIdParamSchema.validate(data, { abortEarly: false });
};
const validateFleetIdParam = (data) => {
return fleetIdParamSchema.validate(data, { abortEarly: false });
};
const validateEncounterIdParam = (data) => {
return encounterIdParamSchema.validate(data, { abortEarly: false });
};
const validateCombatConfiguration = (data) => {
return combatConfigurationSchema.validate(data, { abortEarly: false });
};
module.exports = {
// Validation functions
validateInitiateCombat,
validateUpdateFleetPosition,
validateCombatHistoryQuery,
validateCombatQueueQuery,
validateBattleIdParam,
validateFleetIdParam,
validateEncounterIdParam,
validateCombatConfiguration,
// Raw schemas for middleware use
schemas: {
initiateCombat: initiateCombatSchema,
updateFleetPosition: updateFleetPositionSchema,
combatHistoryQuery: combatHistoryQuerySchema,
combatQueueQuery: combatQueueQuerySchema,
battleIdParam: battleIdParamSchema,
fleetIdParam: fleetIdParamSchema,
encounterIdParam: encounterIdParamSchema,
combatConfiguration: combatConfigurationSchema
}
};