Shatteredvoid/src/services/combat/CombatService.js
MegaProxy d41d1e8125 feat: implement complete Phase 2 frontend foundation with React 18
Major milestone: Frontend implementation complete for Shattered Void MMO

FRONTEND IMPLEMENTATION:
- React 18 + TypeScript + Vite development environment
- Tailwind CSS with custom dark theme for sci-fi aesthetic
- Zustand state management with authentication persistence
- Socket.io WebSocket client with auto-reconnection
- Protected routing with authentication guards
- Responsive design with mobile-first approach

AUTHENTICATION SYSTEM:
- Login/register forms with comprehensive validation
- JWT token management with localStorage persistence
- Password strength validation and user feedback
- Protected routes and authentication guards

CORE GAME INTERFACE:
- Colony management dashboard with real-time updates
- Resource display with live production tracking
- WebSocket integration for real-time game events
- Navigation with connection status indicator
- Toast notifications for user feedback

BACKEND ENHANCEMENTS:
- Complete Research System with technology tree (23 technologies)
- Fleet Management System with ship designs and movement
- Enhanced Authentication with email verification and password reset
- Complete game tick integration for all systems
- Advanced WebSocket events for real-time updates

ARCHITECTURE FEATURES:
- Type-safe TypeScript throughout
- Component-based architecture with reusable UI elements
- API client with request/response interceptors
- Error handling and loading states
- Performance optimized builds with code splitting

Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met)
Ready for: Colony management, fleet operations, research interface
Next: Enhanced gameplay features and admin interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 18:36:06 +00:00

1446 lines
48 KiB
JavaScript

/**
* Combat Service
* Handles all combat-related business logic including fleet battles, colony sieges, and combat resolution
*/
const db = require('../../database/connection');
const logger = require('../../utils/logger');
const { ValidationError, ConflictError, NotFoundError, ServiceError } = require('../../middleware/error.middleware');
class CombatService {
constructor(gameEventService = null, combatPluginManager = null) {
this.gameEventService = gameEventService;
this.combatPluginManager = combatPluginManager;
this.activeCombats = new Map(); // Track ongoing combats
}
/**
* Initiate combat between fleets or fleet vs colony
* @param {Object} combatData - Combat initiation data
* @param {number} combatData.attacker_fleet_id - Attacking fleet ID
* @param {number|null} combatData.defender_fleet_id - Defending fleet ID (null for colony)
* @param {number|null} combatData.defender_colony_id - Defending colony ID (null for fleet)
* @param {string} combatData.location - Combat location coordinates
* @param {string} combatData.combat_type - Type of combat ('instant', 'turn_based', 'real_time')
* @param {number} attackerPlayerId - Attacking player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat initiation result
*/
async initiateCombat(combatData, attackerPlayerId, correlationId) {
try {
const { attacker_fleet_id, defender_fleet_id, defender_colony_id, location, combat_type = 'instant' } = combatData;
logger.info('Combat initiation started', {
correlationId,
attackerPlayerId,
attacker_fleet_id,
defender_fleet_id,
defender_colony_id,
location,
combat_type,
});
// Validate combat data
await this.validateCombatInitiation(combatData, attackerPlayerId, correlationId);
// Check if any participants are already in combat
const conflictCheck = await this.checkCombatConflicts(attacker_fleet_id, defender_fleet_id, defender_colony_id);
if (conflictCheck.hasConflict) {
throw new ConflictError(`Combat participant already engaged: ${conflictCheck.reason}`);
}
// Get combat configuration
const combatConfig = await this.getCombatConfiguration(combat_type);
if (!combatConfig) {
throw new ValidationError('Invalid combat type specified');
}
// Database transaction for atomic combat creation
const combat = await db.transaction(async (trx) => {
// Create battle record
const [battle] = await trx('battles').insert({
battle_type: defender_colony_id ? 'fleet_vs_colony' : 'fleet_vs_fleet',
location,
combat_type_id: combatConfig.id,
participants: JSON.stringify({
attacker_fleet_id,
defender_fleet_id,
defender_colony_id,
attacker_player_id: attackerPlayerId,
}),
status: 'preparing',
battle_data: JSON.stringify({
combat_phase: 'preparation',
preparation_time: combatConfig.config_data.preparation_time || 30,
}),
combat_configuration_id: combatConfig.id,
tactical_settings: JSON.stringify({}),
spectator_count: 0,
estimated_duration: combatConfig.config_data.estimated_duration || 60,
started_at: new Date(),
created_at: new Date(),
}).returning('*');
// Update fleet statuses to 'in_combat'
await trx('fleets')
.whereIn('id', [attacker_fleet_id, defender_fleet_id].filter(Boolean))
.update({
fleet_status: 'in_combat',
last_updated: new Date(),
});
// Update colony status if defending colony
if (defender_colony_id) {
await trx('colonies')
.where('id', defender_colony_id)
.update({
under_siege: true,
last_updated: new Date(),
});
}
// Add to combat queue for processing
await trx('combat_queue').insert({
battle_id: battle.id,
queue_status: 'pending',
priority: combatConfig.config_data.priority || 100,
scheduled_at: new Date(),
processing_metadata: JSON.stringify({
combat_type,
auto_resolve: combatConfig.config_data.auto_resolve || true,
}),
});
logger.info('Combat initiated successfully', {
correlationId,
battleId: battle.id,
attackerPlayerId,
combatType: combat_type,
});
return battle;
});
// Add to active combats tracking
this.activeCombats.set(combat.id, {
battleId: combat.id,
status: 'preparing',
participants: JSON.parse(combat.participants),
startedAt: combat.started_at,
});
// Emit WebSocket event for combat initiation
if (this.gameEventService) {
await this.gameEventService.emitCombatInitiated(combat, correlationId);
}
// Auto-resolve combat if configured
if (combatConfig.config_data.auto_resolve) {
setTimeout(() => {
this.processCombat(combat.id, correlationId).catch(error => {
logger.error('Auto-resolve combat failed', {
correlationId,
battleId: combat.id,
error: error.message,
});
});
}, (combatConfig.config_data.preparation_time || 5) * 1000);
}
return {
battleId: combat.id,
status: combat.status,
estimatedDuration: combat.estimated_duration,
preparationTime: combatConfig.config_data.preparation_time || 30,
};
} catch (error) {
logger.error('Combat initiation failed', {
correlationId,
attackerPlayerId,
combatData,
error: error.message,
stack: error.stack,
});
if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) {
throw error;
}
throw new ServiceError('Failed to initiate combat', error);
}
}
/**
* Process combat resolution using configured plugin
* @param {number} battleId - Battle ID to process
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat result
*/
async processCombat(battleId, correlationId) {
try {
logger.info('Processing combat', {
correlationId,
battleId,
});
// Get battle data
const battle = await this.getBattleById(battleId);
if (!battle) {
throw new NotFoundError('Battle not found');
}
if (battle.status !== 'preparing' && battle.status !== 'active') {
throw new ConflictError('Battle is not in a processable state');
}
// Get combat forces
const combatForces = await this.getCombatForces(battle, correlationId);
// Get combat configuration
const combatConfig = await this.getCombatConfiguration(null, battle.combat_configuration_id);
// Database transaction for combat resolution
const result = await db.transaction(async (trx) => {
// Update battle status to active
await trx('battles')
.where('id', battleId)
.update({
status: 'active',
battle_data: JSON.stringify({
combat_phase: 'resolution',
processing_started: new Date(),
}),
});
// Resolve combat using plugin system
const combatResult = await this.resolveCombat(battle, combatForces, combatConfig, trx, correlationId);
// Create combat encounter record
const [encounter] = await trx('combat_encounters').insert({
battle_id: battleId,
attacker_fleet_id: combatForces.attacker.fleet?.id || null,
defender_fleet_id: combatForces.defender.fleet?.id || null,
defender_colony_id: combatForces.defender.colony?.id || null,
encounter_type: battle.battle_type,
location: battle.location,
initial_forces: JSON.stringify(combatForces.initial),
final_forces: JSON.stringify(combatResult.final_forces),
casualties: JSON.stringify(combatResult.casualties),
combat_log: JSON.stringify(combatResult.combat_log),
experience_gained: combatResult.experience_gained || 0,
loot_awarded: JSON.stringify(combatResult.loot || {}),
outcome: combatResult.outcome,
duration_seconds: combatResult.duration || 60,
started_at: battle.started_at,
completed_at: new Date(),
}).returning('*');
// Update battle with final result
await trx('battles')
.where('id', battleId)
.update({
status: 'completed',
result: JSON.stringify(combatResult),
completed_at: new Date(),
});
// Apply combat results to fleets and colonies
await this.applyCombatResults(combatResult, combatForces, trx, correlationId);
// Update combat statistics
await this.updateCombatStatistics(combatResult, combatForces, trx, correlationId);
// Update combat queue
await trx('combat_queue')
.where('battle_id', battleId)
.update({
queue_status: 'completed',
completed_at: new Date(),
});
logger.info('Combat processed successfully', {
correlationId,
battleId,
encounterId: encounter.id,
outcome: combatResult.outcome,
duration: combatResult.duration,
});
return {
battleId,
encounterId: encounter.id,
outcome: combatResult.outcome,
casualties: combatResult.casualties,
experience: combatResult.experience_gained,
loot: combatResult.loot,
duration: combatResult.duration,
};
});
// Remove from active combats
this.activeCombats.delete(battleId);
// Emit WebSocket event for combat completion
if (this.gameEventService) {
await this.gameEventService.emitCombatCompleted(result, correlationId);
}
return result;
} catch (error) {
logger.error('Combat processing failed', {
correlationId,
battleId,
error: error.message,
stack: error.stack,
});
// Update combat queue with error
await db('combat_queue')
.where('battle_id', battleId)
.update({
queue_status: 'failed',
error_message: error.message,
completed_at: new Date(),
})
.catch(dbError => {
logger.error('Failed to update combat queue error', {
correlationId,
battleId,
dbError: dbError.message,
});
});
if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) {
throw error;
}
throw new ServiceError('Failed to process combat', error);
}
}
/**
* Get combat history for a player
* @param {number} playerId - Player ID
* @param {Object} options - Query options
* @param {number} options.limit - Result limit
* @param {number} options.offset - Result offset
* @param {string} options.outcome - Filter by outcome
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat history
*/
async getCombatHistory(playerId, options = {}, correlationId) {
try {
const { limit = 50, offset = 0, outcome } = options;
logger.info('Fetching combat history', {
correlationId,
playerId,
limit,
offset,
outcome,
});
let query = db('combat_encounters')
.select([
'combat_encounters.*',
'battles.battle_type',
'battles.location',
'attacker_fleet.name as attacker_fleet_name',
'defender_fleet.name as defender_fleet_name',
'defender_colony.name as defender_colony_name',
])
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.where(function () {
this.where('attacker_fleet.player_id', playerId)
.orWhere('defender_fleet.player_id', playerId)
.orWhere('defender_colony.player_id', playerId);
})
.orderBy('combat_encounters.completed_at', 'desc')
.limit(limit)
.offset(offset);
if (outcome) {
query = query.where('combat_encounters.outcome', outcome);
}
const combats = await query;
// Get total count for pagination
let countQuery = db('combat_encounters')
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.where(function () {
this.where('attacker_fleet.player_id', playerId)
.orWhere('defender_fleet.player_id', playerId)
.orWhere('defender_colony.player_id', playerId);
})
.count('* as total');
if (outcome) {
countQuery = countQuery.where('combat_encounters.outcome', outcome);
}
const [{ total }] = await countQuery;
logger.info('Combat history retrieved', {
correlationId,
playerId,
combatCount: combats.length,
totalCombats: parseInt(total),
});
return {
combats,
pagination: {
total: parseInt(total),
limit,
offset,
hasMore: (offset + limit) < parseInt(total),
},
};
} catch (error) {
logger.error('Failed to fetch combat history', {
correlationId,
playerId,
options,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve combat history', error);
}
}
/**
* Get active combats for a player
* @param {number} playerId - Player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Array>} Active combats
*/
async getActiveCombats(playerId, correlationId) {
try {
logger.info('Fetching active combats', {
correlationId,
playerId,
});
const activeCombats = await db('battles')
.select([
'battles.*',
'attacker_fleet.name as attacker_fleet_name',
'defender_fleet.name as defender_fleet_name',
'defender_colony.name as defender_colony_name',
])
.leftJoin('fleets as attacker_fleet',
db.raw('JSON_EXTRACT(battles.participants, \'$.attacker_fleet_id\')'),
'attacker_fleet.id')
.leftJoin('fleets as defender_fleet',
db.raw('JSON_EXTRACT(battles.participants, \'$.defender_fleet_id\')'),
'defender_fleet.id')
.leftJoin('colonies as defender_colony',
db.raw('JSON_EXTRACT(battles.participants, \'$.defender_colony_id\')'),
'defender_colony.id')
.where(function () {
this.where('attacker_fleet.player_id', playerId)
.orWhere('defender_fleet.player_id', playerId)
.orWhere('defender_colony.player_id', playerId);
})
.whereIn('battles.status', ['preparing', 'active'])
.orderBy('battles.started_at', 'desc');
logger.info('Active combats retrieved', {
correlationId,
playerId,
activeCount: activeCombats.length,
});
return activeCombats;
} catch (error) {
logger.error('Failed to fetch active combats', {
correlationId,
playerId,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve active combats', error);
}
}
// Helper methods
/**
* Validate combat initiation data
* @param {Object} combatData - Combat data to validate
* @param {number} attackerPlayerId - Attacking player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<void>}
*/
async validateCombatInitiation(combatData, attackerPlayerId, correlationId) {
const { attacker_fleet_id, defender_fleet_id, defender_colony_id, location } = combatData;
// Validate attacker fleet
const attackerFleet = await db('fleets')
.where('id', attacker_fleet_id)
.where('player_id', attackerPlayerId)
.where('fleet_status', 'idle')
.first();
if (!attackerFleet) {
throw new ValidationError('Invalid attacker fleet or fleet not available for combat');
}
// Validate location matches fleet location
if (attackerFleet.current_location !== location) {
throw new ValidationError('Fleet must be at the specified location to initiate combat');
}
// Validate defender (either fleet or colony)
if (!defender_fleet_id && !defender_colony_id) {
throw new ValidationError('Must specify either defender fleet or defender colony');
}
if (defender_fleet_id && defender_colony_id) {
throw new ValidationError('Cannot specify both defender fleet and defender colony');
}
if (defender_fleet_id) {
const defenderFleet = await db('fleets')
.where('id', defender_fleet_id)
.where('current_location', location)
.first();
if (!defenderFleet) {
throw new ValidationError('Defender fleet not found at specified location');
}
// Check if attacking own fleet
if (defenderFleet.player_id === attackerPlayerId) {
throw new ValidationError('Cannot attack your own fleet');
}
}
if (defender_colony_id) {
const defenderColony = await db('colonies')
.where('id', defender_colony_id)
.where('coordinates', location)
.first();
if (!defenderColony) {
throw new ValidationError('Defender colony not found at specified location');
}
// Check if attacking own colony
if (defenderColony.player_id === attackerPlayerId) {
throw new ValidationError('Cannot attack your own colony');
}
}
}
/**
* Check for combat conflicts with existing battles
* @param {number} attackerFleetId - Attacker fleet ID
* @param {number|null} defenderFleetId - Defender fleet ID
* @param {number|null} defenderColonyId - Defender colony ID
* @returns {Promise<Object>} Conflict check result
*/
async checkCombatConflicts(attackerFleetId, defenderFleetId, defenderColonyId) {
// Check if any fleet is already in combat
const fleetsInCombat = await db('fleets')
.whereIn('id', [attackerFleetId, defenderFleetId].filter(Boolean))
.where('fleet_status', 'in_combat');
if (fleetsInCombat.length > 0) {
return {
hasConflict: true,
reason: `Fleet ${fleetsInCombat[0].id} is already in combat`,
};
}
// Check if colony is under siege
if (defenderColonyId) {
const colonyUnderSiege = await db('colonies')
.where('id', defenderColonyId)
.where('under_siege', true)
.first();
if (colonyUnderSiege) {
return {
hasConflict: true,
reason: `Colony ${defenderColonyId} is already under siege`,
};
}
}
return { hasConflict: false };
}
/**
* Get combat configuration by type or ID
* @param {string|null} combatType - Combat type name
* @param {number|null} configId - Configuration ID
* @returns {Promise<Object|null>} Combat configuration
*/
async getCombatConfiguration(combatType = null, configId = null) {
try {
let query = db('combat_configurations').where('is_active', true);
if (configId) {
query = query.where('id', configId);
} else if (combatType) {
query = query.where('combat_type', combatType);
} else {
// Default to instant combat
query = query.where('combat_type', 'instant');
}
return await query.first();
} catch (error) {
logger.error('Failed to get combat configuration', { error: error.message });
return null;
}
}
/**
* Get battle by ID with full details
* @param {number} battleId - Battle ID
* @returns {Promise<Object|null>} Battle data
*/
async getBattleById(battleId) {
try {
return await db('battles')
.where('id', battleId)
.first();
} catch (error) {
logger.error('Failed to get battle', { battleId, error: error.message });
return null;
}
}
/**
* Get combat forces for battle resolution
* @param {Object} battle - Battle data
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat forces
*/
async getCombatForces(battle, correlationId) {
const participants = JSON.parse(battle.participants);
const forces = {
attacker: {},
defender: {},
initial: {},
};
// Get attacker fleet
if (participants.attacker_fleet_id) {
forces.attacker.fleet = await this.getFleetCombatData(participants.attacker_fleet_id);
}
// Get defender fleet or colony
if (participants.defender_fleet_id) {
forces.defender.fleet = await this.getFleetCombatData(participants.defender_fleet_id);
} else if (participants.defender_colony_id) {
forces.defender.colony = await this.getColonyCombatData(participants.defender_colony_id);
}
// Save initial forces snapshot
forces.initial = JSON.parse(JSON.stringify({ attacker: forces.attacker, defender: forces.defender }));
return forces;
}
/**
* Get fleet combat data including ships and stats
* @param {number} fleetId - Fleet ID
* @returns {Promise<Object>} Fleet combat data
*/
async getFleetCombatData(fleetId) {
const fleet = await db('fleets')
.where('id', fleetId)
.first();
if (!fleet) return null;
const ships = await db('fleet_ships')
.select([
'fleet_ships.*',
'ship_designs.name as design_name',
'ship_designs.ship_class',
'ship_designs.hull_points',
'ship_designs.shield_points',
'ship_designs.armor_points',
'ship_designs.attack_power',
'ship_designs.attack_speed',
'ship_designs.movement_speed',
'ship_designs.special_abilities',
])
.join('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id')
.where('fleet_ships.fleet_id', fleetId);
// Get combat experience for ships
const experience = await db('ship_combat_experience')
.where('fleet_id', fleetId);
const experienceMap = {};
experience.forEach(exp => {
experienceMap[exp.ship_design_id] = exp;
});
// Calculate fleet combat rating
let totalCombatRating = 0;
const shipDetails = ships.map(ship => {
const exp = experienceMap[ship.ship_design_id] || {};
const veterancyBonus = (exp.veterancy_level || 1) * 0.1;
const effectiveAttack = ship.attack_power * (1 + veterancyBonus);
const effectiveHp = (ship.hull_points + ship.shield_points + ship.armor_points) * (1 + veterancyBonus);
const shipRating = (effectiveAttack * ship.attack_speed + effectiveHp) * ship.quantity;
totalCombatRating += shipRating;
return {
...ship,
experience: exp,
effective_attack: effectiveAttack,
effective_hp: effectiveHp,
combat_rating: shipRating,
};
});
return {
...fleet,
ships: shipDetails,
total_combat_rating: totalCombatRating,
};
}
/**
* Get colony combat data including defenses
* @param {number} colonyId - Colony ID
* @returns {Promise<Object>} Colony combat data
*/
async getColonyCombatData(colonyId) {
const colony = await db('colonies')
.where('id', colonyId)
.first();
if (!colony) return null;
// Get defensive buildings
const defenseBuildings = await db('colony_buildings')
.select([
'colony_buildings.*',
'building_types.name as building_name',
'building_types.special_effects',
])
.join('building_types', 'colony_buildings.building_type_id', 'building_types.id')
.where('colony_buildings.colony_id', colonyId)
.where('building_types.category', 'military');
// Calculate total defense rating
let totalDefenseRating = colony.defense_rating || 0;
defenseBuildings.forEach(building => {
const effects = building.special_effects || {};
const defenseBonus = effects.defense_rating || 0;
totalDefenseRating += defenseBonus * building.level * (building.health_percentage / 100);
});
return {
...colony,
defense_buildings: defenseBuildings,
total_defense_rating: totalDefenseRating,
effective_hp: totalDefenseRating * 10 + (colony.shield_strength || 0),
};
}
/**
* Resolve combat using plugin system
* @param {Object} battle - Battle data
* @param {Object} forces - Combat forces
* @param {Object} config - Combat configuration
* @param {Object} trx - Database transaction
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat result
*/
async resolveCombat(battle, forces, config, trx, correlationId) {
if (this.combatPluginManager) {
return await this.combatPluginManager.resolveCombat(battle, forces, config, correlationId);
}
// Fallback instant combat resolver
return await this.instantCombatResolver(battle, forces, config, correlationId);
}
/**
* Instant combat resolver (fallback implementation)
* @param {Object} battle - Battle data
* @param {Object} forces - Combat forces
* @param {Object} config - Combat configuration
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat result
*/
async instantCombatResolver(battle, forces, config, correlationId) {
const attackerRating = forces.attacker.fleet?.total_combat_rating || 0;
const defenderRating = forces.defender.fleet?.total_combat_rating || forces.defender.colony?.total_defense_rating || 0;
const totalRating = attackerRating + defenderRating;
const attackerWinChance = totalRating > 0 ? attackerRating / totalRating : 0.5;
// Add some randomness
const randomFactor = 0.1; // 10% randomness
const roll = Math.random();
const adjustedChance = attackerWinChance + (Math.random() - 0.5) * randomFactor;
const attackerWins = roll < adjustedChance;
const outcome = attackerWins ? 'attacker_victory' : 'defender_victory';
// Calculate casualties
const casualties = this.calculateCasualties(forces, attackerWins, correlationId);
// Calculate experience gain
const experienceGained = Math.floor((attackerRating + defenderRating) / 100);
// Generate combat log
const combatLog = [
{
round: 1,
event: 'combat_start',
description: 'Combat initiated',
attacker_strength: attackerRating,
defender_strength: defenderRating,
},
{
round: 1,
event: 'combat_resolution',
description: `${outcome.replace('_', ' ')}`,
winner: attackerWins ? 'attacker' : 'defender',
},
];
return {
outcome,
casualties,
experience_gained: experienceGained,
combat_log: combatLog,
duration: Math.floor(Math.random() * 120) + 30, // 30-150 seconds
final_forces: this.calculateFinalForces(forces, casualties),
loot: this.calculateLoot(forces, attackerWins, correlationId),
};
}
/**
* Calculate combat casualties
* @param {Object} forces - Combat forces
* @param {boolean} attackerWins - Whether attacker won
* @param {string} correlationId - Request correlation ID
* @returns {Object} Casualty breakdown
*/
calculateCasualties(forces, attackerWins, correlationId) {
const casualties = {
attacker: { ships: {}, total_ships: 0 },
defender: { ships: {}, total_ships: 0, buildings: {} },
};
// Calculate ship losses (winner loses 10-30%, loser loses 40-80%)
const attackerLossRate = attackerWins ? (0.1 + Math.random() * 0.2) : (0.4 + Math.random() * 0.4);
const defenderLossRate = attackerWins ? (0.4 + Math.random() * 0.4) : (0.1 + Math.random() * 0.2);
// Attacker casualties
if (forces.attacker.fleet && forces.attacker.fleet.ships) {
forces.attacker.fleet.ships.forEach(ship => {
const losses = Math.floor(ship.quantity * attackerLossRate);
if (losses > 0) {
casualties.attacker.ships[ship.design_name] = losses;
casualties.attacker.total_ships += losses;
}
});
}
// Defender casualties
if (forces.defender.fleet && forces.defender.fleet.ships) {
forces.defender.fleet.ships.forEach(ship => {
const losses = Math.floor(ship.quantity * defenderLossRate);
if (losses > 0) {
casualties.defender.ships[ship.design_name] = losses;
casualties.defender.total_ships += losses;
}
});
}
// Colony building damage
if (forces.defender.colony && attackerWins) {
const buildingDamageRate = 0.1 + Math.random() * 0.3; // 10-40% damage
if (forces.defender.colony.defense_buildings) {
forces.defender.colony.defense_buildings.forEach(building => {
const damage = Math.floor(building.health_percentage * buildingDamageRate);
if (damage > 0) {
casualties.defender.buildings[building.building_name] = damage;
}
});
}
}
return casualties;
}
/**
* Calculate final forces after combat
* @param {Object} forces - Initial forces
* @param {Object} casualties - Combat casualties
* @returns {Object} Final forces
*/
calculateFinalForces(forces, casualties) {
const finalForces = JSON.parse(JSON.stringify(forces));
// Apply attacker casualties
if (finalForces.attacker.fleet && finalForces.attacker.fleet.ships) {
finalForces.attacker.fleet.ships.forEach(ship => {
const losses = casualties.attacker.ships[ship.design_name] || 0;
ship.quantity = Math.max(0, ship.quantity - losses);
});
}
// Apply defender casualties
if (finalForces.defender.fleet && finalForces.defender.fleet.ships) {
finalForces.defender.fleet.ships.forEach(ship => {
const losses = casualties.defender.ships[ship.design_name] || 0;
ship.quantity = Math.max(0, ship.quantity - losses);
});
}
// Apply building damage
if (finalForces.defender.colony && finalForces.defender.colony.defense_buildings) {
finalForces.defender.colony.defense_buildings.forEach(building => {
const damage = casualties.defender.buildings[building.building_name] || 0;
building.health_percentage = Math.max(0, building.health_percentage - damage);
});
}
return finalForces;
}
/**
* Calculate loot from combat
* @param {Object} forces - Combat forces
* @param {boolean} attackerWins - Whether attacker won
* @param {string} correlationId - Request correlation ID
* @returns {Object} Loot awarded
*/
calculateLoot(forces, attackerWins, correlationId) {
if (!attackerWins) return {};
const loot = {};
// Base loot from combat
const baseLoot = Math.floor(Math.random() * 1000) + 100;
loot.scrap = baseLoot;
loot.energy = Math.floor(baseLoot * 0.5);
// Additional loot from colony raids
if (forces.defender.colony) {
loot.data_cores = Math.floor(Math.random() * 10) + 1;
if (Math.random() < 0.1) { // 10% chance for rare elements
loot.rare_elements = Math.floor(Math.random() * 5) + 1;
}
}
return loot;
}
/**
* Apply combat results to fleets and colonies
* @param {Object} result - Combat result
* @param {Object} forces - Combat forces
* @param {Object} trx - Database transaction
* @param {string} correlationId - Request correlation ID
* @returns {Promise<void>}
*/
async applyCombatResults(result, forces, trx, correlationId) {
// Update fleet ship quantities
for (const side of ['attacker', 'defender']) {
const fleet = forces[side].fleet;
if (fleet && result.casualties[side].ships) {
for (const ship of fleet.ships) {
const losses = result.casualties[side].ships[ship.design_name] || 0;
if (losses > 0) {
const newQuantity = Math.max(0, ship.quantity - losses);
await trx('fleet_ships')
.where('id', ship.id)
.update({
quantity: newQuantity,
health_percentage: newQuantity > 0 ?
Math.max(20, ship.health_percentage - Math.floor(Math.random() * 30)) :
0,
});
}
}
// Update fleet status and statistics
const isDestroyed = result.final_forces[side].fleet.ships.every(ship => ship.quantity === 0);
const newStatus = isDestroyed ? 'destroyed' : 'idle';
await trx('fleets')
.where('id', fleet.id)
.update({
fleet_status: newStatus,
last_combat: new Date(),
last_updated: new Date(),
combat_victories: side === result.outcome.split('_')[0] ?
db.raw('combat_victories + 1') : db.raw('combat_victories'),
combat_defeats: side !== result.outcome.split('_')[0] ?
db.raw('combat_defeats + 1') : db.raw('combat_defeats'),
});
}
}
// Update colony if involved
if (forces.defender.colony) {
const colony = forces.defender.colony;
const buildingDamage = result.casualties.defender.buildings || {};
// Apply building damage
for (const building of colony.defense_buildings) {
const damage = buildingDamage[building.building_name] || 0;
if (damage > 0) {
await trx('colony_buildings')
.where('id', building.id)
.update({
health_percentage: Math.max(0, building.health_percentage - damage),
});
}
}
// Update colony status
await trx('colonies')
.where('id', colony.id)
.update({
under_siege: false,
last_attacked: new Date(),
successful_defenses: result.outcome === 'defender_victory' ?
db.raw('successful_defenses + 1') : db.raw('successful_defenses'),
times_captured: result.outcome === 'attacker_victory' ?
db.raw('times_captured + 1') : db.raw('times_captured'),
});
}
// Award loot to winner
if (result.loot && Object.keys(result.loot).length > 0) {
const winnerId = result.outcome === 'attacker_victory' ?
forces.attacker.fleet.player_id :
(forces.defender.fleet ? forces.defender.fleet.player_id : forces.defender.colony.player_id);
for (const [resourceName, amount] of Object.entries(result.loot)) {
await trx('player_resources')
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.where('player_resources.player_id', winnerId)
.where('resource_types.name', resourceName)
.increment('player_resources.amount', amount)
.update('player_resources.last_updated', new Date());
}
}
}
/**
* Update combat statistics for participants
* @param {Object} result - Combat result
* @param {Object} forces - Combat forces
* @param {Object} trx - Database transaction
* @param {string} correlationId - Request correlation ID
* @returns {Promise<void>}
*/
async updateCombatStatistics(result, forces, trx, correlationId) {
const participants = [];
// Collect all participants
if (forces.attacker.fleet) {
participants.push({
playerId: forces.attacker.fleet.player_id,
side: 'attacker',
isWinner: result.outcome === 'attacker_victory',
});
}
if (forces.defender.fleet) {
participants.push({
playerId: forces.defender.fleet.player_id,
side: 'defender',
isWinner: result.outcome === 'defender_victory',
});
} else if (forces.defender.colony) {
participants.push({
playerId: forces.defender.colony.player_id,
side: 'defender',
isWinner: result.outcome === 'defender_victory',
});
}
// Update statistics for each participant
for (const participant of participants) {
const stats = {
battles_initiated: participant.side === 'attacker' ? 1 : 0,
battles_won: participant.isWinner ? 1 : 0,
battles_lost: participant.isWinner ? 0 : 1,
ships_lost: result.casualties[participant.side].total_ships || 0,
ships_destroyed: result.casualties[participant.side === 'attacker' ? 'defender' : 'attacker'].total_ships || 0,
total_experience_gained: participant.isWinner ? result.experience_gained : 0,
last_battle: new Date(),
};
await trx('combat_statistics')
.where('player_id', participant.playerId)
.update({
battles_initiated: db.raw(`battles_initiated + ${stats.battles_initiated}`),
battles_won: db.raw(`battles_won + ${stats.battles_won}`),
battles_lost: db.raw(`battles_lost + ${stats.battles_lost}`),
ships_lost: db.raw(`ships_lost + ${stats.ships_lost}`),
ships_destroyed: db.raw(`ships_destroyed + ${stats.ships_destroyed}`),
total_experience_gained: db.raw(`total_experience_gained + ${stats.total_experience_gained}`),
last_battle: stats.last_battle,
updated_at: new Date(),
});
// Insert if no existing record
const existingStats = await trx('combat_statistics')
.where('player_id', participant.playerId)
.first();
if (!existingStats) {
await trx('combat_statistics').insert({
player_id: participant.playerId,
...stats,
created_at: new Date(),
updated_at: new Date(),
});
}
}
}
/**
* Get combat encounter details
* @param {number} encounterId - Encounter ID
* @param {number} playerId - Player ID for access control
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object|null>} Combat encounter details
*/
async getCombatEncounter(encounterId, playerId, correlationId) {
try {
logger.info('Fetching combat encounter', {
correlationId,
encounterId,
playerId,
});
const encounter = await db('combat_encounters')
.select([
'combat_encounters.*',
'battles.battle_type',
'battles.location',
'attacker_fleet.name as attacker_fleet_name',
'attacker_fleet.player_id as attacker_player_id',
'defender_fleet.name as defender_fleet_name',
'defender_fleet.player_id as defender_player_id',
'defender_colony.name as defender_colony_name',
'defender_colony.player_id as defender_colony_player_id',
])
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.where('combat_encounters.id', encounterId)
.first();
if (!encounter) {
return null;
}
// Check if player has access to this encounter
const hasAccess = encounter.attacker_player_id === playerId ||
encounter.defender_player_id === playerId ||
encounter.defender_colony_player_id === playerId;
if (!hasAccess) {
return null;
}
// Get combat logs
const combatLogs = await db('combat_logs')
.where('encounter_id', encounterId)
.orderBy('round_number')
.orderBy('timestamp');
logger.info('Combat encounter retrieved', {
correlationId,
encounterId,
playerId,
logCount: combatLogs.length,
});
return {
...encounter,
combat_logs: combatLogs,
};
} catch (error) {
logger.error('Failed to fetch combat encounter', {
correlationId,
encounterId,
playerId,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve combat encounter', error);
}
}
/**
* Get combat statistics for a player
* @param {number} playerId - Player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Combat statistics
*/
async getCombatStatistics(playerId, correlationId) {
try {
logger.info('Fetching combat statistics', {
correlationId,
playerId,
});
let statistics = await db('combat_statistics')
.where('player_id', playerId)
.first();
// Create default statistics if none exist
if (!statistics) {
statistics = {
player_id: playerId,
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: {},
last_battle: null,
};
}
// Calculate derived statistics
const totalBattles = statistics.battles_won + statistics.battles_lost;
const winRate = totalBattles > 0 ? (statistics.battles_won / totalBattles * 100).toFixed(1) : 0;
const killDeathRatio = statistics.ships_lost > 0 ?
(statistics.ships_destroyed / statistics.ships_lost).toFixed(2) :
statistics.ships_destroyed;
logger.info('Combat statistics retrieved', {
correlationId,
playerId,
totalBattles,
winRate,
});
return {
...statistics,
derived_stats: {
total_battles: totalBattles,
win_rate_percentage: parseFloat(winRate),
kill_death_ratio: parseFloat(killDeathRatio),
average_experience_per_battle: totalBattles > 0 ?
(statistics.total_experience_gained / totalBattles).toFixed(1) : 0,
},
};
} catch (error) {
logger.error('Failed to fetch combat statistics', {
correlationId,
playerId,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve combat statistics', error);
}
}
/**
* Update fleet position for tactical combat
* @param {number} fleetId - Fleet ID
* @param {Object} positionData - Position and formation data
* @param {number} playerId - Player ID for authorization
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Updated position data
*/
async updateFleetPosition(fleetId, positionData, playerId, correlationId) {
try {
const { position_x, position_y, position_z, formation, tactical_settings } = positionData;
logger.info('Updating fleet position', {
correlationId,
fleetId,
playerId,
formation,
});
// Verify fleet ownership
const fleet = await db('fleets')
.where('id', fleetId)
.where('player_id', playerId)
.first();
if (!fleet) {
throw new NotFoundError('Fleet not found or access denied');
}
// Validate formation type
const validFormations = ['standard', 'defensive', 'aggressive', 'flanking', 'escort'];
if (formation && !validFormations.includes(formation)) {
throw new ValidationError('Invalid formation type');
}
// Update fleet position
const result = await db.transaction(async (trx) => {
// Insert or update fleet position
const existingPosition = await trx('fleet_positions')
.where('fleet_id', fleetId)
.first();
const positionUpdateData = {
fleet_id: fleetId,
location: fleet.current_location,
position_x: position_x || 0,
position_y: position_y || 0,
position_z: position_z || 0,
formation: formation || 'standard',
tactical_settings: JSON.stringify(tactical_settings || {}),
last_updated: new Date(),
};
if (existingPosition) {
await trx('fleet_positions')
.where('fleet_id', fleetId)
.update(positionUpdateData);
} else {
await trx('fleet_positions').insert(positionUpdateData);
}
return positionUpdateData;
});
logger.info('Fleet position updated', {
correlationId,
fleetId,
formation: result.formation,
});
return result;
} catch (error) {
logger.error('Failed to update fleet position', {
correlationId,
fleetId,
playerId,
error: error.message,
stack: error.stack,
});
if (error instanceof ValidationError || error instanceof NotFoundError) {
throw error;
}
throw new ServiceError('Failed to update fleet position', error);
}
}
/**
* Get available combat types and configurations
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Array>} Available combat types
*/
async getAvailableCombatTypes(correlationId) {
try {
logger.info('Fetching available combat types', { correlationId });
const combatTypes = await db('combat_configurations')
.where('is_active', true)
.orderBy('combat_type')
.orderBy('config_name');
logger.info('Combat types retrieved', {
correlationId,
count: combatTypes.length,
});
return combatTypes;
} catch (error) {
logger.error('Failed to fetch combat types', {
correlationId,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve combat types', error);
}
}
/**
* Get combat queue status
* @param {Object} options - Query options
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Array>} Combat queue entries
*/
async getCombatQueue(options = {}, correlationId) {
try {
const { status, limit = 50 } = options;
logger.info('Fetching combat queue', {
correlationId,
status,
limit,
});
let query = db('combat_queue')
.select([
'combat_queue.*',
'battles.battle_type',
'battles.location',
'battles.status as battle_status',
])
.join('battles', 'combat_queue.battle_id', 'battles.id')
.orderBy('combat_queue.priority', 'desc')
.orderBy('combat_queue.scheduled_at', 'asc')
.limit(limit);
if (status) {
query = query.where('combat_queue.queue_status', status);
}
const queue = await query;
logger.info('Combat queue retrieved', {
correlationId,
count: queue.length,
});
return queue;
} catch (error) {
logger.error('Failed to fetch combat queue', {
correlationId,
error: error.message,
stack: error.stack,
});
throw new ServiceError('Failed to retrieve combat queue', error);
}
}
}
module.exports = CombatService;