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,479 @@
/**
* Test Helpers
* Utility functions for setting up test data
*/
const db = require('../../database/connection');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
/**
* Create a test user with authentication token
* @param {string} email - User email
* @param {string} username - Username
* @param {string} password - Password (optional, defaults to 'testpassword')
* @returns {Promise<Object>} User and token
*/
async function createTestUser(email, username, password = 'testpassword') {
try {
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const [user] = await db('players').insert({
email,
username,
password_hash: hashedPassword,
email_verified: true,
user_group: Math.floor(Math.random() * 10),
is_active: true,
created_at: new Date(),
updated_at: new Date()
}).returning('*');
// Generate JWT token
const token = jwt.sign(
{ id: user.id, username: user.username },
process.env.JWT_SECRET || 'test-secret',
{ expiresIn: '24h' }
);
// Initialize player resources
const resourceTypes = await db('resource_types').where('is_active', true);
for (const resourceType of resourceTypes) {
await db('player_resources').insert({
player_id: user.id,
resource_type_id: resourceType.id,
amount: 10000, // Generous amount for testing
storage_capacity: 50000,
last_updated: new Date()
});
}
// Initialize combat statistics
await db('combat_statistics').insert({
player_id: user.id,
battles_initiated: 0,
battles_won: 0,
battles_lost: 0,
ships_lost: 0,
ships_destroyed: 0,
total_damage_dealt: 0,
total_damage_received: 0,
total_experience_gained: 0,
resources_looted: JSON.stringify({}),
created_at: new Date(),
updated_at: new Date()
});
return { user, token };
} catch (error) {
console.error('Failed to create test user:', error);
throw error;
}
}
/**
* Create a test fleet with ships
* @param {number} playerId - Owner player ID
* @param {string} name - Fleet name
* @param {string} location - Fleet location
* @returns {Promise<Object>} Created fleet
*/
async function createTestFleet(playerId, name, location) {
try {
// Get or create a basic ship design
let shipDesign = await db('ship_designs')
.where('name', 'Test Fighter')
.where('is_public', true)
.first();
if (!shipDesign) {
[shipDesign] = await db('ship_designs').insert({
name: 'Test Fighter',
ship_class: 'fighter',
hull_type: 'light',
components: JSON.stringify({
weapons: ['basic_laser'],
shields: ['basic_shield'],
engines: ['basic_engine']
}),
stats: JSON.stringify({
hp: 100,
attack: 15,
defense: 10,
speed: 5
}),
cost: JSON.stringify({
scrap: 100,
energy: 50
}),
build_time: 30,
is_public: true,
is_active: true,
hull_points: 100,
shield_points: 25,
armor_points: 10,
attack_power: 15,
attack_speed: 1.0,
movement_speed: 5,
cargo_capacity: 0,
special_abilities: JSON.stringify([]),
damage_resistances: JSON.stringify({}),
created_at: new Date(),
updated_at: new Date()
}).returning('*');
}
// Create fleet
const [fleet] = await db('fleets').insert({
player_id: playerId,
name,
current_location: location,
destination: null,
fleet_status: 'idle',
combat_rating: 150,
total_ship_count: 10,
fleet_composition: JSON.stringify({
'Test Fighter': 10
}),
combat_victories: 0,
combat_defeats: 0,
movement_started: null,
arrival_time: null,
last_updated: new Date(),
created_at: new Date()
}).returning('*');
// Add ships to fleet
await db('fleet_ships').insert({
fleet_id: fleet.id,
ship_design_id: shipDesign.id,
quantity: 10,
health_percentage: 100,
experience: 0,
created_at: new Date()
});
// Create initial combat experience record
await db('ship_combat_experience').insert({
fleet_id: fleet.id,
ship_design_id: shipDesign.id,
battles_survived: 0,
enemies_destroyed: 0,
damage_dealt: 0,
experience_points: 0,
veterancy_level: 1,
combat_bonuses: JSON.stringify({}),
created_at: new Date(),
updated_at: new Date()
});
return fleet;
} catch (error) {
console.error('Failed to create test fleet:', error);
throw error;
}
}
/**
* Create a test colony with basic buildings
* @param {number} playerId - Owner player ID
* @param {string} name - Colony name
* @param {string} coordinates - Colony coordinates
* @param {number} planetTypeId - Planet type ID (optional)
* @returns {Promise<Object>} Created colony
*/
async function createTestColony(playerId, name, coordinates, planetTypeId = null) {
try {
// Get planet type
if (!planetTypeId) {
const planetType = await db('planet_types')
.where('is_active', true)
.first();
planetTypeId = planetType?.id || 1;
}
// Get sector
const sectorCoordinates = coordinates.split('-').slice(0, 2).join('-');
let sector = await db('galaxy_sectors')
.where('coordinates', sectorCoordinates)
.first();
if (!sector) {
[sector] = await db('galaxy_sectors').insert({
name: `Test Sector ${sectorCoordinates}`,
coordinates: sectorCoordinates,
description: 'Test sector for integration tests',
danger_level: 3,
special_rules: JSON.stringify({}),
created_at: new Date()
}).returning('*');
}
// Create colony
const [colony] = await db('colonies').insert({
player_id: playerId,
name,
coordinates,
sector_id: sector.id,
planet_type_id: planetTypeId,
population: 1000,
max_population: 10000,
morale: 100,
loyalty: 100,
defense_rating: 50,
shield_strength: 25,
under_siege: false,
successful_defenses: 0,
times_captured: 0,
founded_at: new Date(),
last_updated: new Date()
}).returning('*');
// Add basic buildings
const commandCenter = await db('building_types')
.where('name', 'Command Center')
.first();
if (commandCenter) {
await db('colony_buildings').insert({
colony_id: colony.id,
building_type_id: commandCenter.id,
level: 1,
health_percentage: 100,
is_under_construction: false,
created_at: new Date(),
updated_at: new Date()
});
}
// Add defense grid for combat testing
const defenseGrid = await db('building_types')
.where('name', 'Defense Grid')
.first();
if (defenseGrid) {
await db('colony_buildings').insert({
colony_id: colony.id,
building_type_id: defenseGrid.id,
level: 2,
health_percentage: 100,
is_under_construction: false,
created_at: new Date(),
updated_at: new Date()
});
}
// Initialize colony resource production
const resourceTypes = await db('resource_types').where('is_active', true);
for (const resourceType of resourceTypes) {
await db('colony_resource_production').insert({
colony_id: colony.id,
resource_type_id: resourceType.id,
production_rate: 10,
consumption_rate: 5,
current_stored: 1000,
storage_capacity: 10000,
last_calculated: new Date()
});
}
return colony;
} catch (error) {
console.error('Failed to create test colony:', error);
throw error;
}
}
/**
* Create test combat configuration
* @param {string} name - Configuration name
* @param {string} type - Combat type
* @param {Object} config - Configuration data
* @returns {Promise<Object>} Created configuration
*/
async function createTestCombatConfig(name, type, config = {}) {
try {
const [combatConfig] = await db('combat_configurations').insert({
config_name: name,
combat_type: type,
config_data: JSON.stringify({
auto_resolve: true,
preparation_time: 1,
max_rounds: 5,
round_duration: 2,
damage_variance: 0.1,
experience_gain: 1.0,
casualty_rate_min: 0.1,
casualty_rate_max: 0.7,
loot_multiplier: 1.0,
spectator_limit: 100,
priority: 100,
...config
}),
description: `Test ${type} combat configuration`,
is_active: true,
created_at: new Date(),
updated_at: new Date()
}).returning('*');
return combatConfig;
} catch (error) {
console.error('Failed to create test combat config:', error);
throw error;
}
}
/**
* Create a completed combat encounter for testing history
* @param {number} attackerFleetId - Attacker fleet ID
* @param {number} defenderFleetId - Defender fleet ID (optional)
* @param {number} defenderColonyId - Defender colony ID (optional)
* @param {string} outcome - Combat outcome
* @returns {Promise<Object>} Created encounter
*/
async function createTestCombatEncounter(attackerFleetId, defenderFleetId = null, defenderColonyId = null, outcome = 'attacker_victory') {
try {
// Create battle
const [battle] = await db('battles').insert({
battle_type: defenderColonyId ? 'fleet_vs_colony' : 'fleet_vs_fleet',
location: 'A3-91-X',
combat_type_id: 1,
participants: JSON.stringify({
attacker_fleet_id: attackerFleetId,
defender_fleet_id: defenderFleetId,
defender_colony_id: defenderColonyId
}),
status: 'completed',
battle_data: JSON.stringify({}),
result: JSON.stringify({ outcome }),
started_at: new Date(Date.now() - 300000), // 5 minutes ago
completed_at: new Date(),
created_at: new Date()
}).returning('*');
// Create encounter
const [encounter] = await db('combat_encounters').insert({
battle_id: battle.id,
attacker_fleet_id: attackerFleetId,
defender_fleet_id: defenderFleetId,
defender_colony_id: defenderColonyId,
encounter_type: battle.battle_type,
location: battle.location,
initial_forces: JSON.stringify({
attacker: { ships: 10 },
defender: { ships: 8 }
}),
final_forces: JSON.stringify({
attacker: { ships: outcome === 'attacker_victory' ? 7 : 2 },
defender: { ships: outcome === 'defender_victory' ? 6 : 0 }
}),
casualties: JSON.stringify({
attacker: { ships: {}, total_ships: outcome === 'attacker_victory' ? 3 : 8 },
defender: { ships: {}, total_ships: outcome === 'defender_victory' ? 2 : 8 }
}),
combat_log: JSON.stringify([
{ round: 1, event: 'combat_start', description: 'Combat initiated' },
{ round: 1, event: 'combat_resolution', description: `${outcome.replace('_', ' ')}` }
]),
experience_gained: 100,
loot_awarded: JSON.stringify(outcome === 'attacker_victory' ? { scrap: 500, energy: 250 } : {}),
outcome,
duration_seconds: 90,
started_at: battle.started_at,
completed_at: battle.completed_at,
created_at: new Date()
}).returning('*');
return { battle, encounter };
} catch (error) {
console.error('Failed to create test combat encounter:', error);
throw error;
}
}
/**
* Wait for a condition to be met
* @param {Function} condition - Function that returns true when condition is met
* @param {number} timeout - Timeout in milliseconds
* @param {number} interval - Check interval in milliseconds
* @returns {Promise<void>}
*/
async function waitForCondition(condition, timeout = 5000, interval = 100) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (await condition()) {
return;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error(`Condition not met within ${timeout}ms`);
}
/**
* Clean up all test data
*/
async function cleanupTestData() {
try {
// Delete in order to respect foreign key constraints
await db('combat_logs').del();
await db('combat_encounters').del();
await db('combat_queue').del();
await db('battles').del();
await db('ship_combat_experience').del();
await db('fleet_ships').del();
await db('fleet_positions').del();
await db('fleets').del();
await db('colony_resource_production').del();
await db('colony_buildings').del();
await db('colonies').del();
await db('player_resources').del();
await db('combat_statistics').del();
await db('players').where('email', 'like', '%@test.com').del();
await db('combat_configurations').where('config_name', 'like', 'test_%').del();
await db('ship_designs').where('name', 'like', 'Test %').del();
await db('galaxy_sectors').where('name', 'like', 'Test Sector%').del();
} catch (error) {
console.error('Failed to cleanup test data:', error);
}
}
/**
* Reset combat-related data between tests
*/
async function resetCombatData() {
try {
await db('combat_logs').del();
await db('combat_encounters').del();
await db('combat_queue').del();
await db('battles').del();
// Reset fleet statuses
await db('fleets').update({
fleet_status: 'idle',
last_combat: null
});
// Reset colony siege status
await db('colonies').update({
under_siege: false,
last_attacked: null
});
} catch (error) {
console.error('Failed to reset combat data:', error);
}
}
module.exports = {
createTestUser,
createTestFleet,
createTestColony,
createTestCombatConfig,
createTestCombatEncounter,
waitForCondition,
cleanupTestData,
resetCombatData
};