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
479
src/tests/helpers/test-helpers.js
Normal file
479
src/tests/helpers/test-helpers.js
Normal 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue