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:
parent
1a60cf55a3
commit
8d9ef427be
37 changed files with 13302 additions and 26 deletions
324
src/validators/combat.validators.js
Normal file
324
src/validators/combat.validators.js
Normal 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
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue