Initial commit: Shattered Void MMO foundation
- Complete PostgreSQL database schema with 21+ tables - Express.js server with dual authentication (player/admin) - WebSocket support for real-time features - Comprehensive middleware (auth, validation, logging, security) - Game systems: colonies, resources, fleets, research, factions - Plugin-based combat architecture - Admin panel foundation - Production-ready logging and error handling - Docker support and CI/CD ready - Complete project structure following CLAUDE.md patterns 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
1a60cf55a3
69 changed files with 24471 additions and 0 deletions
685
CLAUDE.md
Normal file
685
CLAUDE.md
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
# Shattered Void - MMO Strategy Game Development Guide
|
||||
|
||||
## Project Overview
|
||||
Shattered Void is a cross-platform text-based MMO strategy game set in a post-collapse galaxy. Players rebuild civilizations from ruins through colony management, fleet construction, exploration, and participation in dynamic galaxy-wide events.
|
||||
|
||||
## Development Stack
|
||||
- **Backend**: Node.js with Express framework
|
||||
- **Database**: PostgreSQL (primary) + Redis (caching/sessions)
|
||||
- **Frontend**: React with responsive design
|
||||
- **Real-time**: WebSocket connections
|
||||
- **Authentication**: JWT-based with email verification
|
||||
- **Deployment**: Docker containers with CI/CD pipeline
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### JavaScript/Node.js Standards
|
||||
```javascript
|
||||
// Use strict ESLint configuration
|
||||
// Prefer async/await over promises
|
||||
// Use descriptive variable names
|
||||
// Include comprehensive error handling
|
||||
// Add JSDoc comments for all functions
|
||||
|
||||
/**
|
||||
* Calculate resource production for a colony building
|
||||
* @param {Object} building - Building instance with type and level
|
||||
* @param {Object} modifiers - Colony and player modifiers
|
||||
* @returns {number} Production rate per hour
|
||||
*/
|
||||
async function calculateBuildingProduction(building, modifiers) {
|
||||
try {
|
||||
const baseProduction = await getBuildingBaseProduction(building.type, building.level);
|
||||
const totalModifier = 1 + (modifiers.colony || 0) + (modifiers.player || 0);
|
||||
return Math.floor(baseProduction * totalModifier);
|
||||
} catch (error) {
|
||||
logger.error(`Production calculation failed for building ${building.id}:`, error);
|
||||
throw new Error(`Failed to calculate production: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Best Practices
|
||||
```sql
|
||||
-- Always use transactions for multi-table operations
|
||||
-- Include proper indexes for performance
|
||||
-- Use foreign key constraints for data integrity
|
||||
-- Add audit columns (created_at, updated_at, created_by)
|
||||
-- Use descriptive column names and comments
|
||||
|
||||
CREATE TABLE colonies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
coordinates VARCHAR(20) NOT NULL, -- Format: "A3-91-X"
|
||||
planet_type_id INTEGER REFERENCES planet_types(id),
|
||||
population INTEGER DEFAULT 0 CHECK (population >= 0),
|
||||
morale INTEGER DEFAULT 100 CHECK (morale BETWEEN 0 AND 100),
|
||||
founded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(coordinates),
|
||||
INDEX idx_colonies_player_id (player_id),
|
||||
INDEX idx_colonies_coordinates (coordinates)
|
||||
);
|
||||
```
|
||||
|
||||
## Debugging Guidelines
|
||||
|
||||
### Comprehensive Logging Strategy
|
||||
```javascript
|
||||
// Use structured logging with context
|
||||
const winston = require('winston');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' }),
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add request correlation IDs for tracing
|
||||
app.use((req, res, next) => {
|
||||
req.correlationId = require('uuid').v4();
|
||||
logger.info('Request started', {
|
||||
correlationId: req.correlationId,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip
|
||||
});
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
```javascript
|
||||
// Service layer error handling
|
||||
class ColonyService {
|
||||
async createColony(playerId, colonyData) {
|
||||
const correlationId = require('async_hooks').executionAsyncResource?.correlationId;
|
||||
|
||||
try {
|
||||
logger.info('Creating colony', {
|
||||
correlationId,
|
||||
playerId,
|
||||
coordinates: colonyData.coordinates
|
||||
});
|
||||
|
||||
// Validate input data
|
||||
if (!colonyData.coordinates || !this.isValidCoordinates(colonyData.coordinates)) {
|
||||
throw new ValidationError('Invalid coordinates format');
|
||||
}
|
||||
|
||||
// Check if coordinates are already taken
|
||||
const existingColony = await this.getColonyByCoordinates(colonyData.coordinates);
|
||||
if (existingColony) {
|
||||
throw new ConflictError('Coordinates already occupied');
|
||||
}
|
||||
|
||||
// Database transaction for atomic operation
|
||||
const colony = await db.transaction(async (trx) => {
|
||||
const [newColony] = await trx('colonies')
|
||||
.insert({
|
||||
player_id: playerId,
|
||||
name: colonyData.name,
|
||||
coordinates: colonyData.coordinates,
|
||||
planet_type_id: colonyData.planet_type_id
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
// Create initial buildings
|
||||
await this.createInitialBuildings(newColony.id, trx);
|
||||
|
||||
logger.info('Colony created successfully', {
|
||||
correlationId,
|
||||
colonyId: newColony.id,
|
||||
playerId
|
||||
});
|
||||
|
||||
return newColony;
|
||||
});
|
||||
|
||||
return colony;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Colony creation failed', {
|
||||
correlationId,
|
||||
playerId,
|
||||
coordinates: colonyData.coordinates,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
// Re-throw with context
|
||||
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ServiceError('Failed to create colony', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom error classes for better error handling
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ConflictError';
|
||||
this.statusCode = 409;
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceError extends Error {
|
||||
constructor(message, originalError) {
|
||||
super(message);
|
||||
this.name = 'ServiceError';
|
||||
this.statusCode = 500;
|
||||
this.originalError = originalError;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Development Debugging Tools
|
||||
```javascript
|
||||
// Debug middleware for development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use('/debug', require('./middleware/debug'));
|
||||
}
|
||||
|
||||
// Debug endpoint for inspecting game state
|
||||
app.get('/debug/player/:playerId/state', async (req, res) => {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
try {
|
||||
const playerId = req.params.playerId;
|
||||
const playerState = {
|
||||
player: await Player.findById(playerId),
|
||||
colonies: await Colony.findByPlayerId(playerId),
|
||||
fleets: await Fleet.findByPlayerId(playerId),
|
||||
research: await Research.findByPlayerId(playerId),
|
||||
resources: await Resource.findByPlayerId(playerId)
|
||||
};
|
||||
|
||||
res.json({
|
||||
debug: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
playerState
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Debug endpoint error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Performance monitoring
|
||||
const performanceTracker = {
|
||||
trackOperation: (operationName) => {
|
||||
const start = process.hrtime.bigint();
|
||||
return {
|
||||
end: () => {
|
||||
const duration = Number(process.hrtime.bigint() - start) / 1000000; // Convert to ms
|
||||
logger.info('Operation completed', {
|
||||
operation: operationName,
|
||||
duration: `${duration.toFixed(2)}ms`
|
||||
});
|
||||
return duration;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Usage in service methods
|
||||
async function processGameTick() {
|
||||
const tracker = performanceTracker.trackOperation('game_tick');
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
processResourceProduction(),
|
||||
processFleetMovements(),
|
||||
processResearchCompletion(),
|
||||
processBuildingConstruction()
|
||||
]);
|
||||
} finally {
|
||||
tracker.end();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing with Jest
|
||||
```javascript
|
||||
// colony.service.test.js
|
||||
const ColonyService = require('../services/colony.service');
|
||||
const db = require('../database/connection');
|
||||
|
||||
describe('ColonyService', () => {
|
||||
let colonyService;
|
||||
|
||||
beforeEach(() => {
|
||||
colonyService = new ColonyService();
|
||||
// Mock database calls
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createColony', () => {
|
||||
it('should create colony with valid data', async () => {
|
||||
const playerId = 1;
|
||||
const colonyData = {
|
||||
name: 'Test Colony',
|
||||
coordinates: 'A3-91-X',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
// Mock database responses
|
||||
db.transaction = jest.fn().mockImplementation(async (callback) => {
|
||||
const mockTrx = {
|
||||
'colonies': {
|
||||
insert: jest.fn().mockReturnValue({
|
||||
returning: jest.fn().mockResolvedValue([{
|
||||
id: 123,
|
||||
player_id: playerId,
|
||||
...colonyData
|
||||
}])
|
||||
})
|
||||
}
|
||||
};
|
||||
return callback(mockTrx);
|
||||
});
|
||||
|
||||
colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue(null);
|
||||
colonyService.createInitialBuildings = jest.fn().mockResolvedValue();
|
||||
|
||||
const result = await colonyService.createColony(playerId, colonyData);
|
||||
|
||||
expect(result).toHaveProperty('id', 123);
|
||||
expect(result).toHaveProperty('name', 'Test Colony');
|
||||
expect(colonyService.createInitialBuildings).toHaveBeenCalledWith(123, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should throw ConflictError for occupied coordinates', async () => {
|
||||
const playerId = 1;
|
||||
const colonyData = {
|
||||
name: 'Test Colony',
|
||||
coordinates: 'A3-91-X',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue({
|
||||
id: 456,
|
||||
coordinates: 'A3-91-X'
|
||||
});
|
||||
|
||||
await expect(colonyService.createColony(playerId, colonyData))
|
||||
.rejects
|
||||
.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid coordinates', async () => {
|
||||
const playerId = 1;
|
||||
const colonyData = {
|
||||
name: 'Test Colony',
|
||||
coordinates: 'INVALID',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
colonyService.isValidCoordinates = jest.fn().mockReturnValue(false);
|
||||
|
||||
await expect(colonyService.createColony(playerId, colonyData))
|
||||
.rejects
|
||||
.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
```javascript
|
||||
// integration/colony.integration.test.js
|
||||
const request = require('supertest');
|
||||
const app = require('../app');
|
||||
const db = require('../database/connection');
|
||||
|
||||
describe('Colony Integration Tests', () => {
|
||||
let authToken;
|
||||
let testPlayer;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set up test database
|
||||
await db.migrate.latest();
|
||||
await db.seed.run();
|
||||
|
||||
// Create test player and get auth token
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
email: 'test@example.com',
|
||||
password: 'testpassword123',
|
||||
username: 'testplayer'
|
||||
});
|
||||
|
||||
authToken = response.body.token;
|
||||
testPlayer = response.body.player;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up colonies before each test
|
||||
await db('colonies').where('player_id', testPlayer.id).del();
|
||||
});
|
||||
|
||||
it('should create colony via API endpoint', async () => {
|
||||
const colonyData = {
|
||||
name: 'Integration Test Colony',
|
||||
coordinates: 'Z9-99-Z',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/colonies')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(colonyData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.name).toBe(colonyData.name);
|
||||
expect(response.body.coordinates).toBe(colonyData.coordinates);
|
||||
|
||||
// Verify in database
|
||||
const dbColony = await db('colonies')
|
||||
.where('id', response.body.id)
|
||||
.first();
|
||||
|
||||
expect(dbColony).toHaveProperty('player_id', testPlayer.id);
|
||||
});
|
||||
|
||||
it('should return 409 for duplicate coordinates', async () => {
|
||||
const colonyData = {
|
||||
name: 'First Colony',
|
||||
coordinates: 'Z9-99-Y',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
// Create first colony
|
||||
await request(app)
|
||||
.post('/api/colonies')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(colonyData)
|
||||
.expect(201);
|
||||
|
||||
// Try to create second colony with same coordinates
|
||||
const duplicateData = {
|
||||
name: 'Duplicate Colony',
|
||||
coordinates: 'Z9-99-Y',
|
||||
planet_type_id: 1
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/colonies')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(duplicateData)
|
||||
.expect(409);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toContain('occupied');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Essential Scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect=0.0.0.0:9229 src/server.js",
|
||||
"start": "node src/server.js",
|
||||
"test": "jest --verbose --coverage",
|
||||
"test:watch": "jest --watch --verbose",
|
||||
"test:integration": "jest --testPathPattern=integration --runInBand",
|
||||
"lint": "eslint src/ --ext .js --fix",
|
||||
"lint:check": "eslint src/ --ext .js",
|
||||
"typecheck": "jsdoc -t node_modules/tsd-jsdoc/dist -r src/ -d temp/ && rm -rf temp/",
|
||||
"db:migrate": "knex migrate:latest",
|
||||
"db:rollback": "knex migrate:rollback",
|
||||
"db:seed": "knex seed:run",
|
||||
"db:reset": "knex migrate:rollback:all && knex migrate:latest && knex seed:run",
|
||||
"docker:build": "docker build -t shattered-void .",
|
||||
"docker:run": "docker-compose up -d",
|
||||
"logs": "tail -f logs/combined.log",
|
||||
"logs:error": "tail -f logs/error.log"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
```bash
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=shattered_void_dev
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=password
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-super-secret-key-change-in-production
|
||||
JWT_EXPIRE=24h
|
||||
|
||||
# Game Configuration
|
||||
GAME_TICK_INTERVAL=60000
|
||||
MAX_COLONIES_PER_PLAYER=10
|
||||
STARTING_RESOURCES_SCRAP=1000
|
||||
STARTING_RESOURCES_ENERGY=500
|
||||
|
||||
# Debug Features
|
||||
ENABLE_DEBUG_ENDPOINTS=true
|
||||
ENABLE_SQL_LOGGING=true
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Database Query Optimization
|
||||
```javascript
|
||||
// Monitor slow queries
|
||||
const slowQueryThreshold = 100; // ms
|
||||
|
||||
db.on('query', (queryData) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
queryData.response = queryData.response || {};
|
||||
const originalCallback = queryData.response.callback;
|
||||
|
||||
queryData.response.callback = function(...args) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (duration > slowQueryThreshold) {
|
||||
logger.warn('Slow query detected', {
|
||||
query: queryData.sql,
|
||||
bindings: queryData.bindings,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
}
|
||||
|
||||
if (originalCallback) {
|
||||
originalCallback.apply(this, args);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Add query performance metrics
|
||||
const queryMetrics = new Map();
|
||||
|
||||
function trackQuery(queryName, query) {
|
||||
return async (...args) => {
|
||||
const start = process.hrtime.bigint();
|
||||
try {
|
||||
const result = await query(...args);
|
||||
const duration = Number(process.hrtime.bigint() - start) / 1000000;
|
||||
|
||||
// Update metrics
|
||||
const metrics = queryMetrics.get(queryName) || { count: 0, totalTime: 0, maxTime: 0 };
|
||||
metrics.count++;
|
||||
metrics.totalTime += duration;
|
||||
metrics.maxTime = Math.max(metrics.maxTime, duration);
|
||||
queryMetrics.set(queryName, metrics);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Query failed: ${queryName}`, { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Memory and Resource Monitoring
|
||||
```javascript
|
||||
// Memory usage tracking
|
||||
setInterval(() => {
|
||||
const memUsage = process.memoryUsage();
|
||||
logger.info('Memory usage', {
|
||||
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
|
||||
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
|
||||
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
|
||||
external: `${Math.round(memUsage.external / 1024 / 1024)}MB`
|
||||
});
|
||||
}, 60000); // Log every minute
|
||||
|
||||
// Database connection monitoring
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const dbStats = await db.raw('SELECT COUNT(*) as active_connections FROM pg_stat_activity WHERE state = ?', ['active']);
|
||||
logger.info('Database connections', {
|
||||
active: dbStats.rows[0].active_connections
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get DB connection stats:', error);
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
```javascript
|
||||
const Joi = require('joi');
|
||||
|
||||
// Validation schemas
|
||||
const schemas = {
|
||||
createColony: Joi.object({
|
||||
name: Joi.string().min(3).max(50).required(),
|
||||
coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(),
|
||||
planet_type_id: Joi.number().integer().min(1).required()
|
||||
}),
|
||||
|
||||
createFleet: Joi.object({
|
||||
name: Joi.string().min(3).max(50).required(),
|
||||
ships: Joi.array().items(
|
||||
Joi.object({
|
||||
design_id: Joi.number().integer().min(1).required(),
|
||||
quantity: Joi.number().integer().min(1).max(100).required()
|
||||
})
|
||||
).min(1).required()
|
||||
})
|
||||
};
|
||||
|
||||
// Validation middleware
|
||||
function validateRequest(schema) {
|
||||
return (req, res, next) => {
|
||||
const { error, value } = schema.validate(req.body);
|
||||
if (error) {
|
||||
logger.warn('Validation failed', {
|
||||
correlationId: req.correlationId,
|
||||
error: error.details[0].message,
|
||||
body: req.body
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: error.details[0].message
|
||||
});
|
||||
}
|
||||
req.body = value; // Use sanitized data
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
app.use('/api/', apiLimiter);
|
||||
```
|
||||
|
||||
## File Structure
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Route handlers
|
||||
├── services/ # Business logic
|
||||
├── models/ # Database models
|
||||
├── middleware/ # Express middleware
|
||||
├── database/
|
||||
│ ├── migrations/ # Database schema changes
|
||||
│ ├── seeds/ # Test data
|
||||
│ └── connection.js # Database configuration
|
||||
├── utils/ # Helper functions
|
||||
├── validators/ # Input validation schemas
|
||||
├── tests/
|
||||
│ ├── unit/ # Unit tests
|
||||
│ ├── integration/ # Integration tests
|
||||
│ └── fixtures/ # Test data
|
||||
├── config/ # Configuration files
|
||||
└── server.js # Application entry point
|
||||
|
||||
logs/ # Log files
|
||||
docker/ # Docker configuration
|
||||
docs/ # Documentation
|
||||
scripts/ # Deployment and utility scripts
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
- Use feature branches: `feature/colony-management`
|
||||
- Commit messages: `feat: add colony creation API endpoint`
|
||||
- Always run `npm run lint && npm test` before committing
|
||||
- Use conventional commits for automated changelog generation
|
||||
|
||||
This development guide ensures high code quality, comprehensive debugging capabilities, and maintainable architecture for the Shattered Void MMO project.
|
||||
Loading…
Add table
Add a link
Reference in a new issue