- 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>
324 lines
No EOL
13 KiB
JavaScript
324 lines
No EOL
13 KiB
JavaScript
/**
|
|
* 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
|
|
}
|
|
}; |