/** * 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} 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} 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} 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} 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} */ 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} 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} 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} 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} 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} 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} 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} 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} 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} */ 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} */ 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} 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} 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} 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} 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} 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;