/** * 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 } };