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>
1446 lines
48 KiB
JavaScript
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;
|