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>
This commit is contained in:
MegaProxy 2025-08-02 18:36:06 +00:00
parent 8d9ef427be
commit d41d1e8125
130 changed files with 33588 additions and 14817 deletions

View file

@ -10,563 +10,563 @@ const logger = require('../../utils/logger');
const { ValidationError, ConflictError, NotFoundError } = require('../../middleware/error.middleware');
class CombatController {
constructor() {
this.combatPluginManager = null;
this.gameEventService = null;
this.combatService = null;
}
constructor() {
this.combatPluginManager = null;
this.gameEventService = null;
this.combatService = null;
}
/**
/**
* Initialize controller with dependencies
* @param {Object} dependencies - Service dependencies
*/
async initialize(dependencies = {}) {
this.gameEventService = dependencies.gameEventService || new GameEventService();
this.combatPluginManager = dependencies.combatPluginManager || new CombatPluginManager();
this.combatService = dependencies.combatService || new CombatService(this.gameEventService, this.combatPluginManager);
async initialize(dependencies = {}) {
this.gameEventService = dependencies.gameEventService || new GameEventService();
this.combatPluginManager = dependencies.combatPluginManager || new CombatPluginManager();
this.combatService = dependencies.combatService || new CombatService(this.gameEventService, this.combatPluginManager);
// Initialize plugin manager
await this.combatPluginManager.initialize('controller-init');
}
// Initialize plugin manager
await this.combatPluginManager.initialize('controller-init');
}
/**
/**
* Initiate combat between fleets or fleet vs colony
* POST /api/combat/initiate
*/
async initiateCombat(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const combatData = req.body;
async initiateCombat(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const combatData = req.body;
logger.info('Combat initiation request', {
correlationId,
playerId,
combatData
});
logger.info('Combat initiation request', {
correlationId,
playerId,
combatData,
});
// Validate required fields
if (!combatData.attacker_fleet_id) {
return res.status(400).json({
error: 'Attacker fleet ID is required',
code: 'MISSING_ATTACKER_FLEET'
});
}
// Validate required fields
if (!combatData.attacker_fleet_id) {
return res.status(400).json({
error: 'Attacker fleet ID is required',
code: 'MISSING_ATTACKER_FLEET',
});
}
if (!combatData.location) {
return res.status(400).json({
error: 'Combat location is required',
code: 'MISSING_LOCATION'
});
}
if (!combatData.location) {
return res.status(400).json({
error: 'Combat location is required',
code: 'MISSING_LOCATION',
});
}
if (!combatData.defender_fleet_id && !combatData.defender_colony_id) {
return res.status(400).json({
error: 'Either defender fleet or colony must be specified',
code: 'MISSING_DEFENDER'
});
}
if (!combatData.defender_fleet_id && !combatData.defender_colony_id) {
return res.status(400).json({
error: 'Either defender fleet or colony must be specified',
code: 'MISSING_DEFENDER',
});
}
// Initialize services if not already done
if (!this.combatService) {
await this.initialize();
}
// Initialize services if not already done
if (!this.combatService) {
await this.initialize();
}
// Initiate combat
const result = await this.combatService.initiateCombat(combatData, playerId, correlationId);
// Initiate combat
const result = await this.combatService.initiateCombat(combatData, playerId, correlationId);
logger.info('Combat initiated successfully', {
correlationId,
playerId,
battleId: result.battleId
});
logger.info('Combat initiated successfully', {
correlationId,
playerId,
battleId: result.battleId,
});
res.status(201).json({
success: true,
data: result,
message: 'Combat initiated successfully'
});
res.status(201).json({
success: true,
data: result,
message: 'Combat initiated successfully',
});
} catch (error) {
logger.error('Combat initiation failed', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Combat initiation failed', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack,
});
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
code: 'VALIDATION_ERROR'
});
}
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
code: 'VALIDATION_ERROR',
});
}
if (error instanceof ConflictError) {
return res.status(409).json({
error: error.message,
code: 'CONFLICT_ERROR'
});
}
if (error instanceof ConflictError) {
return res.status(409).json({
error: error.message,
code: 'CONFLICT_ERROR',
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND'
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND',
});
}
next(error);
}
next(error);
}
}
/**
/**
* Get active combats for the current player
* GET /api/combat/active
*/
async getActiveCombats(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
async getActiveCombats(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
logger.info('Active combats request', {
correlationId,
playerId
});
logger.info('Active combats request', {
correlationId,
playerId,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const activeCombats = await this.combatService.getActiveCombats(playerId, correlationId);
const activeCombats = await this.combatService.getActiveCombats(playerId, correlationId);
logger.info('Active combats retrieved', {
correlationId,
playerId,
count: activeCombats.length
});
logger.info('Active combats retrieved', {
correlationId,
playerId,
count: activeCombats.length,
});
res.json({
success: true,
data: {
combats: activeCombats,
count: activeCombats.length
}
});
res.json({
success: true,
data: {
combats: activeCombats,
count: activeCombats.length,
},
});
} catch (error) {
logger.error('Failed to get active combats', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to get active combats', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack,
});
next(error);
}
next(error);
}
}
/**
/**
* Get combat history for the current player
* GET /api/combat/history
*/
async getCombatHistory(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
// Parse query parameters
const options = {
limit: parseInt(req.query.limit) || 20,
offset: parseInt(req.query.offset) || 0,
outcome: req.query.outcome || null
};
async getCombatHistory(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
// Validate parameters
if (options.limit > 100) {
return res.status(400).json({
error: 'Limit cannot exceed 100',
code: 'INVALID_LIMIT'
});
}
// Parse query parameters
const options = {
limit: parseInt(req.query.limit) || 20,
offset: parseInt(req.query.offset) || 0,
outcome: req.query.outcome || null,
};
if (options.outcome && !['attacker_victory', 'defender_victory', 'draw'].includes(options.outcome)) {
return res.status(400).json({
error: 'Invalid outcome filter',
code: 'INVALID_OUTCOME'
});
}
// Validate parameters
if (options.limit > 100) {
return res.status(400).json({
error: 'Limit cannot exceed 100',
code: 'INVALID_LIMIT',
});
}
logger.info('Combat history request', {
correlationId,
playerId,
options
});
if (options.outcome && !['attacker_victory', 'defender_victory', 'draw'].includes(options.outcome)) {
return res.status(400).json({
error: 'Invalid outcome filter',
code: 'INVALID_OUTCOME',
});
}
if (!this.combatService) {
await this.initialize();
}
logger.info('Combat history request', {
correlationId,
playerId,
options,
});
const history = await this.combatService.getCombatHistory(playerId, options, correlationId);
if (!this.combatService) {
await this.initialize();
}
logger.info('Combat history retrieved', {
correlationId,
playerId,
count: history.combats.length,
total: history.pagination.total
});
const history = await this.combatService.getCombatHistory(playerId, options, correlationId);
res.json({
success: true,
data: history
});
logger.info('Combat history retrieved', {
correlationId,
playerId,
count: history.combats.length,
total: history.pagination.total,
});
} catch (error) {
logger.error('Failed to get combat history', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack
});
res.json({
success: true,
data: history,
});
next(error);
}
} catch (error) {
logger.error('Failed to get combat history', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack,
});
next(error);
}
}
/**
/**
* Get detailed combat encounter information
* GET /api/combat/encounter/:encounterId
*/
async getCombatEncounter(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const encounterId = parseInt(req.params.encounterId);
async getCombatEncounter(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const encounterId = parseInt(req.params.encounterId);
if (!encounterId || isNaN(encounterId)) {
return res.status(400).json({
error: 'Valid encounter ID is required',
code: 'INVALID_ENCOUNTER_ID'
});
}
if (!encounterId || isNaN(encounterId)) {
return res.status(400).json({
error: 'Valid encounter ID is required',
code: 'INVALID_ENCOUNTER_ID',
});
}
logger.info('Combat encounter request', {
correlationId,
playerId,
encounterId
});
logger.info('Combat encounter request', {
correlationId,
playerId,
encounterId,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const encounter = await this.combatService.getCombatEncounter(encounterId, playerId, correlationId);
const encounter = await this.combatService.getCombatEncounter(encounterId, playerId, correlationId);
if (!encounter) {
return res.status(404).json({
error: 'Combat encounter not found or access denied',
code: 'ENCOUNTER_NOT_FOUND'
});
}
if (!encounter) {
return res.status(404).json({
error: 'Combat encounter not found or access denied',
code: 'ENCOUNTER_NOT_FOUND',
});
}
logger.info('Combat encounter retrieved', {
correlationId,
playerId,
encounterId
});
logger.info('Combat encounter retrieved', {
correlationId,
playerId,
encounterId,
});
res.json({
success: true,
data: encounter
});
res.json({
success: true,
data: encounter,
});
} catch (error) {
logger.error('Failed to get combat encounter', {
correlationId: req.correlationId,
playerId: req.user?.id,
encounterId: req.params.encounterId,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to get combat encounter', {
correlationId: req.correlationId,
playerId: req.user?.id,
encounterId: req.params.encounterId,
error: error.message,
stack: error.stack,
});
next(error);
}
next(error);
}
}
/**
/**
* Get combat statistics for the current player
* GET /api/combat/statistics
*/
async getCombatStatistics(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
async getCombatStatistics(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
logger.info('Combat statistics request', {
correlationId,
playerId
});
logger.info('Combat statistics request', {
correlationId,
playerId,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const statistics = await this.combatService.getCombatStatistics(playerId, correlationId);
const statistics = await this.combatService.getCombatStatistics(playerId, correlationId);
logger.info('Combat statistics retrieved', {
correlationId,
playerId
});
logger.info('Combat statistics retrieved', {
correlationId,
playerId,
});
res.json({
success: true,
data: statistics
});
res.json({
success: true,
data: statistics,
});
} catch (error) {
logger.error('Failed to get combat statistics', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to get combat statistics', {
correlationId: req.correlationId,
playerId: req.user?.id,
error: error.message,
stack: error.stack,
});
next(error);
}
next(error);
}
}
/**
/**
* Update fleet positioning for tactical combat
* PUT /api/combat/position/:fleetId
*/
async updateFleetPosition(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const fleetId = parseInt(req.params.fleetId);
const positionData = req.body;
async updateFleetPosition(req, res, next) {
try {
const correlationId = req.correlationId;
const playerId = req.user.id;
const fleetId = parseInt(req.params.fleetId);
const positionData = req.body;
if (!fleetId || isNaN(fleetId)) {
return res.status(400).json({
error: 'Valid fleet ID is required',
code: 'INVALID_FLEET_ID'
});
}
if (!fleetId || isNaN(fleetId)) {
return res.status(400).json({
error: 'Valid fleet ID is required',
code: 'INVALID_FLEET_ID',
});
}
logger.info('Fleet position update request', {
correlationId,
playerId,
fleetId,
positionData
});
logger.info('Fleet position update request', {
correlationId,
playerId,
fleetId,
positionData,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const result = await this.combatService.updateFleetPosition(fleetId, positionData, playerId, correlationId);
const result = await this.combatService.updateFleetPosition(fleetId, positionData, playerId, correlationId);
logger.info('Fleet position updated', {
correlationId,
playerId,
fleetId
});
logger.info('Fleet position updated', {
correlationId,
playerId,
fleetId,
});
res.json({
success: true,
data: result,
message: 'Fleet position updated successfully'
});
res.json({
success: true,
data: result,
message: 'Fleet position updated successfully',
});
} catch (error) {
logger.error('Failed to update fleet position', {
correlationId: req.correlationId,
playerId: req.user?.id,
fleetId: req.params.fleetId,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to update fleet position', {
correlationId: req.correlationId,
playerId: req.user?.id,
fleetId: req.params.fleetId,
error: error.message,
stack: error.stack,
});
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
code: 'VALIDATION_ERROR'
});
}
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
code: 'VALIDATION_ERROR',
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND'
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND',
});
}
next(error);
}
next(error);
}
}
/**
/**
* Get available combat types and configurations
* GET /api/combat/types
*/
async getCombatTypes(req, res, next) {
try {
const correlationId = req.correlationId;
async getCombatTypes(req, res, next) {
try {
const correlationId = req.correlationId;
logger.info('Combat types request', { correlationId });
logger.info('Combat types request', { correlationId });
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const combatTypes = await this.combatService.getAvailableCombatTypes(correlationId);
const combatTypes = await this.combatService.getAvailableCombatTypes(correlationId);
logger.info('Combat types retrieved', {
correlationId,
count: combatTypes.length
});
logger.info('Combat types retrieved', {
correlationId,
count: combatTypes.length,
});
res.json({
success: true,
data: combatTypes
});
res.json({
success: true,
data: combatTypes,
});
} catch (error) {
logger.error('Failed to get combat types', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to get combat types', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
});
next(error);
}
next(error);
}
}
/**
/**
* Force resolve a combat (admin only)
* POST /api/combat/resolve/:battleId
*/
async forceResolveCombat(req, res, next) {
try {
const correlationId = req.correlationId;
const battleId = parseInt(req.params.battleId);
async forceResolveCombat(req, res, next) {
try {
const correlationId = req.correlationId;
const battleId = parseInt(req.params.battleId);
if (!battleId || isNaN(battleId)) {
return res.status(400).json({
error: 'Valid battle ID is required',
code: 'INVALID_BATTLE_ID'
});
}
if (!battleId || isNaN(battleId)) {
return res.status(400).json({
error: 'Valid battle ID is required',
code: 'INVALID_BATTLE_ID',
});
}
logger.info('Force resolve combat request', {
correlationId,
battleId,
adminUser: req.user?.id
});
logger.info('Force resolve combat request', {
correlationId,
battleId,
adminUser: req.user?.id,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const result = await this.combatService.processCombat(battleId, correlationId);
const result = await this.combatService.processCombat(battleId, correlationId);
logger.info('Combat force resolved', {
correlationId,
battleId,
outcome: result.outcome
});
logger.info('Combat force resolved', {
correlationId,
battleId,
outcome: result.outcome,
});
res.json({
success: true,
data: result,
message: 'Combat resolved successfully'
});
res.json({
success: true,
data: result,
message: 'Combat resolved successfully',
});
} catch (error) {
logger.error('Failed to force resolve combat', {
correlationId: req.correlationId,
battleId: req.params.battleId,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to force resolve combat', {
correlationId: req.correlationId,
battleId: req.params.battleId,
error: error.message,
stack: error.stack,
});
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND'
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: error.message,
code: 'NOT_FOUND',
});
}
if (error instanceof ConflictError) {
return res.status(409).json({
error: error.message,
code: 'CONFLICT_ERROR'
});
}
if (error instanceof ConflictError) {
return res.status(409).json({
error: error.message,
code: 'CONFLICT_ERROR',
});
}
next(error);
}
next(error);
}
}
/**
/**
* Get combat queue status (admin only)
* GET /api/combat/queue
*/
async getCombatQueue(req, res, next) {
try {
const correlationId = req.correlationId;
const status = req.query.status || null;
const limit = parseInt(req.query.limit) || 50;
async getCombatQueue(req, res, next) {
try {
const correlationId = req.correlationId;
const status = req.query.status || null;
const limit = parseInt(req.query.limit) || 50;
logger.info('Combat queue request', {
correlationId,
status,
limit,
adminUser: req.user?.id
});
logger.info('Combat queue request', {
correlationId,
status,
limit,
adminUser: req.user?.id,
});
if (!this.combatService) {
await this.initialize();
}
if (!this.combatService) {
await this.initialize();
}
const queue = await this.combatService.getCombatQueue({ status, limit }, correlationId);
const queue = await this.combatService.getCombatQueue({ status, limit }, correlationId);
logger.info('Combat queue retrieved', {
correlationId,
count: queue.length
});
logger.info('Combat queue retrieved', {
correlationId,
count: queue.length,
});
res.json({
success: true,
data: queue
});
res.json({
success: true,
data: queue,
});
} catch (error) {
logger.error('Failed to get combat queue', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
} catch (error) {
logger.error('Failed to get combat queue', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
});
next(error);
}
next(error);
}
}
}
// Export singleton instance
const combatController = new CombatController();
module.exports = {
CombatController,
// Export bound methods for route usage
initiateCombat: combatController.initiateCombat.bind(combatController),
getActiveCombats: combatController.getActiveCombats.bind(combatController),
getCombatHistory: combatController.getCombatHistory.bind(combatController),
getCombatEncounter: combatController.getCombatEncounter.bind(combatController),
getCombatStatistics: combatController.getCombatStatistics.bind(combatController),
updateFleetPosition: combatController.updateFleetPosition.bind(combatController),
getCombatTypes: combatController.getCombatTypes.bind(combatController),
forceResolveCombat: combatController.forceResolveCombat.bind(combatController),
getCombatQueue: combatController.getCombatQueue.bind(combatController)
};
CombatController,
// Export bound methods for route usage
initiateCombat: combatController.initiateCombat.bind(combatController),
getActiveCombats: combatController.getActiveCombats.bind(combatController),
getCombatHistory: combatController.getCombatHistory.bind(combatController),
getCombatEncounter: combatController.getCombatEncounter.bind(combatController),
getCombatStatistics: combatController.getCombatStatistics.bind(combatController),
updateFleetPosition: combatController.updateFleetPosition.bind(combatController),
getCombatTypes: combatController.getCombatTypes.bind(combatController),
forceResolveCombat: combatController.forceResolveCombat.bind(combatController),
getCombatQueue: combatController.getCombatQueue.bind(combatController),
};