initial github push
18
API/.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your_jwt_secret_key_here
|
||||
|
||||
# Redis Configuration (optional)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Client URL
|
||||
CLIENT_URL=http://localhost:3000
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
19
API/config/database.js
Normal file
@ -0,0 +1,19 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline';
|
||||
|
||||
const conn = await mongoose.connect(mongoUri, {
|
||||
// Remove deprecated options for newer MongoDB versions
|
||||
});
|
||||
|
||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||
} catch (error) {
|
||||
logger.error('Database connection error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
131
API/config/production.js
Normal file
@ -0,0 +1,131 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const productionConfig = {
|
||||
// Server settings
|
||||
port: process.env.PORT || 3001,
|
||||
|
||||
// Database settings
|
||||
database: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
}
|
||||
},
|
||||
|
||||
// JWT settings
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET,
|
||||
expiresIn: '24h',
|
||||
refreshExpiresIn: '7d'
|
||||
},
|
||||
|
||||
// Redis settings (for sessions and caching)
|
||||
redis: {
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
options: {
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3,
|
||||
}
|
||||
},
|
||||
|
||||
// CORS settings
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
},
|
||||
|
||||
// Socket.IO settings
|
||||
socketio: {
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
maxHttpBufferSize: 1e8, // 100 MB
|
||||
},
|
||||
|
||||
// Logging settings
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: process.env.NODE_ENV === 'production' ? 'json' : 'simple',
|
||||
file: {
|
||||
enabled: true,
|
||||
filename: 'logs/app.log',
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 5,
|
||||
}
|
||||
},
|
||||
|
||||
// Security settings
|
||||
security: {
|
||||
helmet: {
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true
|
||||
}
|
||||
},
|
||||
compression: {
|
||||
level: 6,
|
||||
threshold: 1024,
|
||||
}
|
||||
},
|
||||
|
||||
// Game settings
|
||||
game: {
|
||||
maxPlayersPerServer: 50,
|
||||
serverCleanupInterval: 300000, // 5 minutes
|
||||
inactivePlayerTimeout: 1800000, // 30 minutes
|
||||
autoSaveInterval: 60000, // 1 minute
|
||||
}
|
||||
};
|
||||
|
||||
// Validate required environment variables
|
||||
const validateConfig = () => {
|
||||
const required = ['JWT_SECRET'];
|
||||
const missing = required.filter(key => !process.env[key]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
logger.error(`Missing required environment variables: ${missing.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const prodRequired = ['MONGODB_URI', 'CLIENT_URL'];
|
||||
const prodMissing = prodRequired.filter(key => !process.env[key]);
|
||||
|
||||
if (prodMissing.length > 0) {
|
||||
logger.error(`Missing required production environment variables: ${prodMissing.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...productionConfig,
|
||||
validateConfig
|
||||
};
|
||||
134
API/middleware/errorHandler.js
Normal file
@ -0,0 +1,134 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Custom error classes
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(message) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationError extends AppError {
|
||||
constructor(message = 'Authentication failed') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationError extends AppError {
|
||||
constructor(message = 'Access denied') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message = 'Resource not found') {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Resource conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message = 'Database operation failed') {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
logger.error({
|
||||
error: err,
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
userId: req.userId
|
||||
}
|
||||
});
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message).join(', ');
|
||||
error = new ValidationError(message);
|
||||
}
|
||||
|
||||
// Mongoose duplicate key error
|
||||
if (err.code === 11000) {
|
||||
const field = Object.keys(err.keyValue)[0];
|
||||
const value = err.keyValue[field];
|
||||
error = new ConflictError(`${field} '${value}' already exists`);
|
||||
}
|
||||
|
||||
// Mongoose cast error
|
||||
if (err.name === 'CastError') {
|
||||
error = new ValidationError(`Invalid ${err.path}: ${err.value}`);
|
||||
}
|
||||
|
||||
// JWT errors
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
error = new AuthenticationError('Invalid token');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
error = new AuthenticationError('Token expired');
|
||||
}
|
||||
|
||||
// Default error
|
||||
if (!error.isOperational) {
|
||||
error = new AppError('Something went wrong', 500);
|
||||
}
|
||||
|
||||
res.status(error.statusCode || 500).json({
|
||||
status: error.status || 'error',
|
||||
message: error.message,
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
stack: error.stack,
|
||||
error: err
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// 404 handler
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
errorHandler,
|
||||
catchAsync,
|
||||
notFound
|
||||
};
|
||||
134
API/models/GameServer.js
Normal file
@ -0,0 +1,134 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const gameServerSchema = new mongoose.Schema({
|
||||
serverId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['public', 'private'],
|
||||
default: 'public'
|
||||
},
|
||||
region: {
|
||||
type: String,
|
||||
default: 'us-east'
|
||||
},
|
||||
maxPlayers: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 20
|
||||
},
|
||||
currentPlayers: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
owner: {
|
||||
userId: { type: String, required: true },
|
||||
username: { type: String, required: true }
|
||||
},
|
||||
settings: {
|
||||
password: { type: String, default: null },
|
||||
description: { type: String, default: '' },
|
||||
tags: [{ type: String }]
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['waiting', 'active', 'full', 'offline'],
|
||||
default: 'waiting'
|
||||
},
|
||||
gameServerUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastActivity: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance (only for non-unique fields)
|
||||
gameServerSchema.index({ type: 1 });
|
||||
gameServerSchema.index({ region: 1 });
|
||||
gameServerSchema.index({ status: 1 });
|
||||
gameServerSchema.index({ 'owner.userId': 1 });
|
||||
|
||||
// Methods
|
||||
gameServerSchema.methods.addPlayer = function() {
|
||||
if (this.currentPlayers < this.maxPlayers) {
|
||||
this.currentPlayers += 1;
|
||||
this.lastActivity = new Date();
|
||||
|
||||
if (this.currentPlayers >= this.maxPlayers) {
|
||||
this.status = 'full';
|
||||
} else if (this.currentPlayers > 0) {
|
||||
this.status = 'active';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.removePlayer = function() {
|
||||
if (this.currentPlayers > 0) {
|
||||
this.currentPlayers -= 1;
|
||||
this.lastActivity = new Date();
|
||||
|
||||
if (this.currentPlayers === 0) {
|
||||
this.status = 'waiting';
|
||||
} else if (this.currentPlayers < this.maxPlayers) {
|
||||
this.status = 'active';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.isFull = function() {
|
||||
return this.currentPlayers >= this.maxPlayers;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.canJoin = function() {
|
||||
return this.status !== 'offline' && !this.isFull();
|
||||
};
|
||||
|
||||
// Static methods
|
||||
gameServerSchema.statics.findAvailableServers = function(filters = {}) {
|
||||
const query = { status: { $ne: 'offline' } };
|
||||
|
||||
if (filters.type) {
|
||||
query.type = filters.type;
|
||||
}
|
||||
|
||||
if (filters.region) {
|
||||
query.region = filters.region;
|
||||
}
|
||||
|
||||
return this.find(query).sort({ lastActivity: -1 });
|
||||
};
|
||||
|
||||
gameServerSchema.statics.cleanupOldServers = function(maxAge = 24 * 60 * 60 * 1000) { // 24 hours
|
||||
const cutoffTime = new Date(Date.now() - maxAge);
|
||||
return this.deleteMany({
|
||||
$or: [
|
||||
{ lastActivity: { $lt: cutoffTime }, currentPlayers: 0 },
|
||||
{ status: 'offline', lastActivity: { $lt: cutoffTime } }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('GameServer', gameServerSchema);
|
||||
306
API/models/Inventory.js
Normal file
@ -0,0 +1,306 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const inventorySchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'Player'
|
||||
},
|
||||
|
||||
// Inventory settings
|
||||
maxSlots: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
|
||||
// Items array
|
||||
items: [{
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['weapon', 'armor', 'material', 'consumable']
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
|
||||
default: 'common'
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1
|
||||
},
|
||||
|
||||
// Item stats (for weapons/armor)
|
||||
stats: {
|
||||
attack: { type: Number, default: 0 },
|
||||
defense: { type: Number, default: 0 },
|
||||
speed: { type: Number, default: 0 },
|
||||
criticalChance: { type: Number, default: 0 },
|
||||
criticalDamage: { type: Number, default: 1.5 },
|
||||
damage: { type: Number, default: 0 },
|
||||
fireRate: { type: Number, default: 0 },
|
||||
range: { type: Number, default: 0 },
|
||||
energy: { type: Number, default: 0 },
|
||||
health: { type: Number, default: 0 },
|
||||
maxHealth: { type: Number, default: 0 },
|
||||
durability: { type: Number, default: 0 },
|
||||
weight: { type: Number, default: 0 },
|
||||
energyShield: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Item properties
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Equipment properties
|
||||
equipable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
slot: {
|
||||
type: String,
|
||||
enum: ['weapon', 'armor', 'engine', 'shield', 'special'],
|
||||
default: null
|
||||
},
|
||||
isEquipped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Consumable properties
|
||||
consumable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
effect: {
|
||||
health: { type: Number, default: 0 },
|
||||
energy: { type: Number, default: 0 },
|
||||
attack: { type: Number, default: 0 },
|
||||
defense: { type: Number, default: 0 },
|
||||
speed: { type: Number, default: 0 },
|
||||
duration: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Stackable items
|
||||
stackable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
acquiredAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
default: null
|
||||
}
|
||||
}],
|
||||
|
||||
// Equipped items
|
||||
equippedItems: {
|
||||
weapon: { type: String, default: null },
|
||||
armor: { type: String, default: null },
|
||||
engine: { type: String, default: null },
|
||||
shield: { type: String, default: null },
|
||||
special: { type: String, default: null }
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance
|
||||
inventorySchema.index({ userId: 1 });
|
||||
inventorySchema.index({ 'items.id': 1 });
|
||||
inventorySchema.index({ 'items.type': 1 });
|
||||
|
||||
// Methods
|
||||
inventorySchema.methods.addItem = function(itemData) {
|
||||
// Check if item already exists and is stackable
|
||||
const existingItem = this.items.find(item =>
|
||||
item.id === itemData.id &&
|
||||
item.type === itemData.type &&
|
||||
item.stackable
|
||||
);
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity += itemData.quantity || 1;
|
||||
} else {
|
||||
// Add new item
|
||||
const newItem = {
|
||||
...itemData,
|
||||
quantity: itemData.quantity || 1,
|
||||
acquiredAt: new Date()
|
||||
};
|
||||
|
||||
this.items.push(newItem);
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.removeItem = function(itemId, quantity = 1) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === itemId);
|
||||
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
const item = this.items[itemIndex];
|
||||
|
||||
if (item.quantity > quantity) {
|
||||
item.quantity -= quantity;
|
||||
} else {
|
||||
// Remove item completely
|
||||
this.items.splice(itemIndex, 1);
|
||||
|
||||
// Unequip if it was equipped
|
||||
Object.keys(this.equippedItems).forEach(slot => {
|
||||
if (this.equippedItems[slot] === itemId) {
|
||||
this.equippedItems[slot] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.hasItem = function(itemId, quantity = 1) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
return item && item.quantity >= quantity;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemCount = function(itemId) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
|
||||
inventorySchema.methods.equipItem = function(itemId, slot) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
|
||||
if (!item) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
if (!item.equipable) {
|
||||
throw new Error('Item is not equipable');
|
||||
}
|
||||
|
||||
if (item.slot !== slot) {
|
||||
throw new Error('Item cannot be equipped in this slot');
|
||||
}
|
||||
|
||||
// Unequip current item in slot
|
||||
if (this.equippedItems[slot]) {
|
||||
const currentItem = this.items.find(item => item.id === this.equippedItems[slot]);
|
||||
if (currentItem) {
|
||||
currentItem.isEquipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Equip new item
|
||||
this.equippedItems[slot] = itemId;
|
||||
item.isEquipped = true;
|
||||
item.lastUsed = new Date();
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.unequipItem = function(slot) {
|
||||
const itemId = this.equippedItems[slot];
|
||||
|
||||
if (!itemId) {
|
||||
throw new Error('No item equipped in this slot');
|
||||
}
|
||||
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
if (item) {
|
||||
item.isEquipped = false;
|
||||
}
|
||||
|
||||
this.equippedItems[slot] = null;
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.useConsumable = function(itemId) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
|
||||
if (!item) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
if (!item.consumable) {
|
||||
throw new Error('Item is not consumable');
|
||||
}
|
||||
|
||||
if (item.quantity <= 0) {
|
||||
throw new Error('No quantity left');
|
||||
}
|
||||
|
||||
// Apply effects
|
||||
const effects = { ...item.effect };
|
||||
|
||||
// Remove one from quantity
|
||||
item.quantity -= 1;
|
||||
item.lastUsed = new Date();
|
||||
|
||||
// Remove item if quantity is 0
|
||||
if (item.quantity === 0) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === itemId);
|
||||
this.items.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
this.save();
|
||||
|
||||
return effects;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getInventorySummary = function() {
|
||||
const summary = {
|
||||
totalItems: this.items.length,
|
||||
usedSlots: this.items.length,
|
||||
maxSlots: this.maxSlots,
|
||||
itemsByType: {},
|
||||
equippedItems: this.equippedItems
|
||||
};
|
||||
|
||||
// Count items by type
|
||||
this.items.forEach(item => {
|
||||
summary.itemsByType[item.type] = (summary.itemsByType[item.type] || 0) + item.quantity;
|
||||
});
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemsByType = function(type) {
|
||||
return this.items.filter(item => item.type === type);
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemsByRarity = function(rarity) {
|
||||
return this.items.filter(item => item.rarity === rarity);
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Inventory', inventorySchema);
|
||||
169
API/models/Player.js
Normal file
@ -0,0 +1,169 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { calculateXPToNextLevel, getLevelFromXP } = require('../config/xp-progression');
|
||||
|
||||
const playerSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
// Authentication
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false // Don't include password in queries by default
|
||||
},
|
||||
|
||||
// Player stats
|
||||
stats: {
|
||||
level: { type: Number, default: 1 },
|
||||
experience: { type: Number, default: 0 },
|
||||
totalXP: { type: Number, default: 0 }, // Total accumulated XP across all levels
|
||||
experienceToNext: { type: Number, default: function() { return calculateXPToNextLevel(1, 0); } },
|
||||
credits: { type: Number, default: 1000 },
|
||||
dungeonsCleared: { type: Number, default: 0 },
|
||||
playTime: { type: Number, default: 0 },
|
||||
lastLogin: { type: Date, default: Date.now }
|
||||
},
|
||||
|
||||
// Base attributes
|
||||
attributes: {
|
||||
health: { type: Number, default: 100 },
|
||||
maxHealth: { type: Number, default: 100 },
|
||||
energy: { type: Number, default: 100 },
|
||||
maxEnergy: { type: Number, default: 100 },
|
||||
attack: { type: Number, default: 10 },
|
||||
defense: { type: Number, default: 5 },
|
||||
speed: { type: Number, default: 10 },
|
||||
criticalChance: { type: Number, default: 0.05 },
|
||||
criticalDamage: { type: Number, default: 1.5 }
|
||||
},
|
||||
|
||||
// Player info
|
||||
info: {
|
||||
name: { type: String, default: 'Commander' },
|
||||
title: { type: String, default: 'Rookie Pilot' },
|
||||
guild: { type: String, default: null },
|
||||
rank: { type: String, default: 'Cadet' }
|
||||
},
|
||||
|
||||
// Current ship
|
||||
currentShip: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Ship'
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
autoSave: { type: Boolean, default: true },
|
||||
notifications: { type: Boolean, default: true },
|
||||
soundEffects: { type: Boolean, default: true },
|
||||
music: { type: Boolean, default: false },
|
||||
discordIntegration: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// Daily rewards
|
||||
dailyRewards: {
|
||||
lastClaim: { type: Date, default: null },
|
||||
consecutiveDays: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Server info
|
||||
currentServer: { type: String, default: null },
|
||||
|
||||
// Timestamps
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now }
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance (only for non-unique fields)
|
||||
playerSchema.index({ 'stats.level': 1 });
|
||||
playerSchema.index({ currentServer: 1 });
|
||||
|
||||
// Methods
|
||||
playerSchema.methods.addExperience = function(amount) {
|
||||
// Add to total accumulated XP
|
||||
this.stats.totalXP += amount;
|
||||
|
||||
// Calculate new level based on total XP
|
||||
const levelInfo = getLevelFromXP(this.stats.totalXP);
|
||||
const oldLevel = this.stats.level;
|
||||
|
||||
this.stats.level = levelInfo.level;
|
||||
this.stats.experience = levelInfo.xpIntoLevel;
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
|
||||
// Return whether the player leveled up
|
||||
return this.stats.level > oldLevel;
|
||||
};
|
||||
|
||||
playerSchema.methods.addCredits = function(amount) {
|
||||
this.stats.credits += amount;
|
||||
return this.stats.credits;
|
||||
};
|
||||
|
||||
playerSchema.methods.canAfford = function(cost) {
|
||||
return this.stats.credits >= cost;
|
||||
};
|
||||
|
||||
playerSchema.methods.spendCredits = function(cost) {
|
||||
if (this.canAfford(cost)) {
|
||||
this.stats.credits -= cost;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
playerSchema.methods.updatePlayTime = function(sessionTime) {
|
||||
this.stats.playTime += sessionTime;
|
||||
this.stats.lastLogin = new Date();
|
||||
};
|
||||
|
||||
playerSchema.methods.claimDailyReward = function() {
|
||||
const today = new Date();
|
||||
const lastClaim = this.dailyRewards.lastClaim;
|
||||
|
||||
// Check if already claimed today
|
||||
if (lastClaim && lastClaim.toDateString() === today.toDateString()) {
|
||||
return { success: false, message: 'Daily reward already claimed today' };
|
||||
}
|
||||
|
||||
// Check consecutive days
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (lastClaim && lastClaim.toDateString() === yesterday.toDateString()) {
|
||||
this.dailyRewards.consecutiveDays += 1;
|
||||
} else {
|
||||
this.dailyRewards.consecutiveDays = 1;
|
||||
}
|
||||
|
||||
this.dailyRewards.lastClaim = today;
|
||||
|
||||
// Calculate reward based on consecutive days
|
||||
const baseReward = 100;
|
||||
const consecutiveBonus = (this.dailyRewards.consecutiveDays - 1) * 50;
|
||||
const totalReward = baseReward + consecutiveBonus;
|
||||
|
||||
this.addCredits(totalReward);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reward: totalReward,
|
||||
consecutiveDays: this.dailyRewards.consecutiveDays
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Player', playerSchema);
|
||||
189
API/models/Ship.js
Normal file
@ -0,0 +1,189 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const shipSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'Player'
|
||||
},
|
||||
|
||||
// Ship identification
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['Fighter', 'Cruiser', 'Battleship', 'Carrier', 'Explorer']
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
|
||||
// Ship stats
|
||||
stats: {
|
||||
health: { type: Number, required: true },
|
||||
maxHealth: { type: Number, required: true },
|
||||
attack: { type: Number, required: true },
|
||||
defense: { type: Number, required: true },
|
||||
speed: { type: Number, required: true },
|
||||
criticalChance: { type: Number, default: 0.05 },
|
||||
criticalDamage: { type: Number, default: 1.5 },
|
||||
hull: { type: Number, required: true }
|
||||
},
|
||||
|
||||
// Ship appearance
|
||||
texture: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// Ship progression
|
||||
experience: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
requiredExp: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
upgrades: [{
|
||||
type: String
|
||||
}],
|
||||
|
||||
// Ship status
|
||||
isEquipped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Shop information (if purchased)
|
||||
price: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
|
||||
default: 'common'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
acquiredAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance
|
||||
shipSchema.index({ userId: 1 });
|
||||
shipSchema.index({ id: 1 });
|
||||
shipSchema.index({ isEquipped: 1 });
|
||||
shipSchema.index({ isCurrent: 1 });
|
||||
|
||||
// Methods
|
||||
shipSchema.methods.addExperience = function(amount) {
|
||||
this.experience += amount;
|
||||
|
||||
// Level up logic
|
||||
while (this.experience >= this.requiredExp) {
|
||||
this.experience -= this.requiredExp;
|
||||
this.level += 1;
|
||||
this.requiredExp = this.level * 100;
|
||||
|
||||
// Increase stats on level up
|
||||
this.stats.maxHealth += 10;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
this.stats.attack += 2;
|
||||
this.stats.defense += 1;
|
||||
this.stats.speed += 1;
|
||||
}
|
||||
|
||||
return this.level;
|
||||
};
|
||||
|
||||
shipSchema.methods.takeDamage = function(damage) {
|
||||
const actualDamage = Math.max(0, damage - this.stats.defense);
|
||||
this.stats.health = Math.max(0, this.stats.health - actualDamage);
|
||||
|
||||
if (this.stats.health === 0) {
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
return actualDamage;
|
||||
};
|
||||
|
||||
shipSchema.methods.heal = function(amount) {
|
||||
const healAmount = Math.min(amount, this.stats.maxHealth - this.stats.health);
|
||||
this.stats.health += healAmount;
|
||||
this.isDestroyed = false;
|
||||
|
||||
return healAmount;
|
||||
};
|
||||
|
||||
shipSchema.methods.isAlive = function() {
|
||||
return this.stats.health > 0;
|
||||
};
|
||||
|
||||
shipSchema.methods.getStatSummary = function() {
|
||||
return {
|
||||
name: this.name,
|
||||
class: this.class,
|
||||
level: this.level,
|
||||
health: `${this.stats.health}/${this.stats.maxHealth}`,
|
||||
attack: this.stats.attack,
|
||||
defense: this.stats.defense,
|
||||
speed: this.stats.speed,
|
||||
criticalChance: `${(this.stats.criticalChance * 100).toFixed(1)}%`,
|
||||
criticalDamage: `${this.stats.criticalDamage}x`
|
||||
};
|
||||
};
|
||||
|
||||
shipSchema.methods.upgrade = function(upgradeType) {
|
||||
switch (upgradeType) {
|
||||
case 'health':
|
||||
this.stats.maxHealth += 20;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
break;
|
||||
case 'attack':
|
||||
this.stats.attack += 5;
|
||||
break;
|
||||
case 'defense':
|
||||
this.stats.defense += 3;
|
||||
break;
|
||||
case 'speed':
|
||||
this.stats.speed += 2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown upgrade type');
|
||||
}
|
||||
|
||||
if (!this.upgrades.includes(upgradeType)) {
|
||||
this.upgrades.push(upgradeType);
|
||||
}
|
||||
|
||||
this.lastUsed = new Date();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Ship', shipSchema);
|
||||
6068
API/package-lock.json
generated
Normal file
41
API/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "galaxystrikeonline-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Galaxy Strike Online - Server Backend",
|
||||
"license": "MIT",
|
||||
"author": "Korvarix Studios",
|
||||
"type": "commonjs",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"debug": "node --inspect server.js",
|
||||
"test": "jest",
|
||||
"migrate": "node scripts/migrate.js",
|
||||
"seed": "node scripts/seed.js"
|
||||
},
|
||||
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"redis": "^4.6.11",
|
||||
"winston": "^3.11.0",
|
||||
"joi": "^17.11.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"compression": "^1.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
214
API/routes/auth.js
Normal file
@ -0,0 +1,214 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Joi = require('joi');
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const Player = require('../models/Player');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Rate limiting for auth routes
|
||||
const authLimiter = new RateLimiterMemory({
|
||||
keyGenerator: (req) => req.ip,
|
||||
points: 5, // Number of requests
|
||||
duration: 900, // Per 15 minutes (900 seconds)
|
||||
blockDuration: 900, // Block for 15 minutes
|
||||
message: 'Too many authentication attempts, please try again later.'
|
||||
});
|
||||
|
||||
// Validation schemas
|
||||
const registerSchema = Joi.object({
|
||||
username: Joi.string().min(3).max(30).required(),
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required()
|
||||
});
|
||||
|
||||
const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required()
|
||||
});
|
||||
|
||||
// Register route
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const resLimiter = await authLimiter.consume(req.ip);
|
||||
if (!resLimiter.remainingPoints) {
|
||||
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
|
||||
}
|
||||
|
||||
const { error } = registerSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
|
||||
const { username, email, password } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await Player.findOne({
|
||||
$or: [{ email }, { username }]
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
error: 'User with this email or username already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new player
|
||||
const player = new Player({
|
||||
userId: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
lastLogin: new Date()
|
||||
});
|
||||
|
||||
await player.save();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: player.userId, email: player.email },
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
logger.info(`New user registered: ${email}`);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User registered successfully',
|
||||
token,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login route
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const resLimiter = await authLimiter.consume(req.ip);
|
||||
if (!resLimiter.remainingPoints) {
|
||||
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
|
||||
}
|
||||
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find user
|
||||
const player = await Player.findOne({ email }).select('+password');
|
||||
if (!player) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Check if password exists (for backward compatibility with existing users)
|
||||
if (!player.password) {
|
||||
logger.error('Player password field is missing for user:', email);
|
||||
return res.status(401).json({
|
||||
error: 'Account migration required. Please re-register your account.',
|
||||
requiresMigration: true
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await bcrypt.compare(password, player.password);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
player.stats.lastLogin = new Date();
|
||||
await player.save();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: player.userId, email: player.email },
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
logger.info(`User logged in: ${email}`);
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats,
|
||||
info: player.info
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify token route
|
||||
router.get('/verify', async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
|
||||
const player = await Player.findOne({ userId: decoded.userId });
|
||||
if (!player) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
valid: true,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats,
|
||||
info: player.info
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Token verification error:', error);
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout route
|
||||
router.post('/logout', async (req, res) => {
|
||||
try {
|
||||
// In a real implementation, you might want to blacklist the token
|
||||
// For now, we'll just return success
|
||||
res.json({ message: 'Logout successful' });
|
||||
} catch (error) {
|
||||
logger.error('Logout error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
336
API/routes/game.js
Normal file
@ -0,0 +1,336 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
const { getGameSystem } = require('../systems/GameSystem');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get player data
|
||||
router.get('/player', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
const player = await gameSystem.loadPlayer(req.userId);
|
||||
|
||||
if (!player) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
stats: player.stats,
|
||||
attributes: player.attributes,
|
||||
info: player.info,
|
||||
settings: player.settings,
|
||||
dailyRewards: player.dailyRewards
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting player data:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get player's ships
|
||||
router.get('/ships', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const ships = await Ship.find({ userId: req.userId });
|
||||
|
||||
res.json({
|
||||
ships: ships.map(ship => ({
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.class,
|
||||
level: ship.level,
|
||||
stats: ship.stats,
|
||||
isEquipped: ship.isEquipped,
|
||||
isCurrent: ship.isCurrent,
|
||||
rarity: ship.rarity,
|
||||
texture: ship.texture,
|
||||
acquiredAt: ship.acquiredAt
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting player ships:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Equip a ship
|
||||
router.post('/ships/equip', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { shipId } = req.body;
|
||||
|
||||
if (!shipId) {
|
||||
return res.status(400).json({ error: 'Ship ID required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const ship = await gameSystem.equipShip(req.userId, shipId);
|
||||
|
||||
res.json({
|
||||
message: 'Ship equipped successfully',
|
||||
ship: {
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.class,
|
||||
level: ship.level,
|
||||
stats: ship.stats,
|
||||
isEquipped: ship.isEquipped,
|
||||
isCurrent: ship.isCurrent
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error equipping ship:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get player's inventory
|
||||
router.get('/inventory', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
let inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
// Create new inventory if it doesn't exist
|
||||
inventory = new Inventory({ userId: req.userId });
|
||||
await inventory.save();
|
||||
}
|
||||
|
||||
res.json({
|
||||
items: inventory.items,
|
||||
equippedItems: inventory.equippedItems,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting inventory:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add item to inventory
|
||||
router.post('/inventory/add', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemData } = req.body;
|
||||
|
||||
if (!itemData) {
|
||||
return res.status(400).json({ error: 'Item data required' });
|
||||
}
|
||||
|
||||
let inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
inventory = new Inventory({ userId: req.userId });
|
||||
}
|
||||
|
||||
await inventory.addItem(itemData);
|
||||
|
||||
res.json({
|
||||
message: 'Item added to inventory',
|
||||
item: itemData,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error adding item to inventory:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Equip item
|
||||
router.post('/inventory/equip', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId, slot } = req.body;
|
||||
|
||||
if (!itemId || !slot) {
|
||||
return res.status(400).json({ error: 'Item ID and slot required' });
|
||||
}
|
||||
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
await inventory.equipItem(itemId, slot);
|
||||
|
||||
res.json({
|
||||
message: 'Item equipped successfully',
|
||||
equippedItems: inventory.equippedItems
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error equipping item:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Use consumable
|
||||
router.post('/inventory/use', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.body;
|
||||
|
||||
if (!itemId) {
|
||||
return res.status(400).json({ error: 'Item ID required' });
|
||||
}
|
||||
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
const effects = await inventory.useConsumable(itemId);
|
||||
|
||||
res.json({
|
||||
message: 'Item used successfully',
|
||||
effects,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error using consumable:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Process game action
|
||||
router.post('/action', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { actionData } = req.body;
|
||||
|
||||
if (!actionData) {
|
||||
return res.status(400).json({ error: 'Action data required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const result = await gameSystem.processGameAction(req.userId, actionData);
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error processing game action:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Claim daily reward
|
||||
router.post('/daily-reward', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
const rewardResult = player.claimDailyReward();
|
||||
await player.save();
|
||||
|
||||
res.json(rewardResult);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error claiming daily reward:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save player data
|
||||
router.post('/save', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
await gameSystem.savePlayer(req.userId);
|
||||
|
||||
res.json({ message: 'Game saved successfully' });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error saving game:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get shop items
|
||||
router.get('/shop/:category?', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
const category = req.params.category;
|
||||
const items = gameSystem.economy.getShopItems(category);
|
||||
|
||||
res.json({ items });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting shop items:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Purchase item
|
||||
router.post('/shop/purchase', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId, quantity = 1 } = req.body;
|
||||
|
||||
if (!itemId) {
|
||||
return res.status(400).json({ error: 'Item ID required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const purchaseInfo = gameSystem.economy.purchaseItem(req.userId, itemId, quantity);
|
||||
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player.canAfford(purchaseInfo.totalCost)) {
|
||||
return res.status(400).json({ error: 'Insufficient credits' });
|
||||
}
|
||||
|
||||
// Deduct credits
|
||||
player.spendCredits(purchaseInfo.totalCost);
|
||||
await player.save();
|
||||
|
||||
// Add item to inventory
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
await inventory.addItem({
|
||||
...purchaseInfo.item,
|
||||
quantity
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Purchase successful',
|
||||
item: purchaseInfo.item,
|
||||
quantity,
|
||||
totalCost: purchaseInfo.totalCost,
|
||||
remainingCredits: player.stats.credits
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error purchasing item:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
232
API/routes/mods.js
Normal file
@ -0,0 +1,232 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const logger = require('../utils/logger');
|
||||
const ModService = require('../services/ModService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get all mods
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { enabledOnly } = req.query;
|
||||
const mods = await ModService.getMods(enabledOnly === 'true');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mods
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const mod = await ModService.getMod(parseInt(id));
|
||||
|
||||
if (!mod) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mod
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Enable mod
|
||||
router.post('/:id/enable', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.enableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod enabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error enabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Disable mod
|
||||
router.post('/:id/disable', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.disableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod disabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error disabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod assets by type
|
||||
router.get('/assets/:type', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const assets = await ModService.getModAssets(type);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
assets
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod assets:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod asset
|
||||
router.get('/assets/:type/:assetId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type, assetId } = req.params;
|
||||
const asset = await ModService.getModAsset(type, assetId);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({ error: 'Asset not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
asset
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod asset:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod load order
|
||||
router.get('/load-order', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const loadOrder = await ModService.getLoadOrder();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
loadOrder
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set mod load order
|
||||
router.post('/load-order', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { modIds } = req.body;
|
||||
|
||||
if (!Array.isArray(modIds)) {
|
||||
return res.status(400).json({ error: 'modIds must be an array' });
|
||||
}
|
||||
|
||||
const success = await ModService.setLoadOrder(modIds);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set load order' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Load order updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error setting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server mod preferences
|
||||
router.get('/preferences/server', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const preferences = await ModService.getServerPreferences();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
preferences
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting server preferences:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set server mod preference
|
||||
router.post('/preferences/server', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { key, value } = req.body;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: 'key and value are required' });
|
||||
}
|
||||
|
||||
const success = await ModService.setServerPreference(key, value);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set preference' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Preference set successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error setting server preference:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reload mods from filesystem
|
||||
router.post('/reload', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
await ModService.reloadMods();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mods reloaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error reloading mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
419
API/routes/servers.js
Normal file
@ -0,0 +1,419 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const GameServer = require('../models/GameServer');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register new game server (for GameServer instances to register themselves)
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner } = req.body;
|
||||
|
||||
logger.info(`[API SERVER] Game server registration request:`, {
|
||||
serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner
|
||||
});
|
||||
|
||||
// Check if server already exists
|
||||
const existingServer = await GameServer.findOne({ serverId });
|
||||
if (existingServer) {
|
||||
// Update existing server
|
||||
existingServer.name = name || existingServer.name;
|
||||
existingServer.type = type || existingServer.type;
|
||||
existingServer.region = region || existingServer.region;
|
||||
existingServer.maxPlayers = maxPlayers || existingServer.maxPlayers;
|
||||
existingServer.currentPlayers = currentPlayers !== undefined ? currentPlayers : existingServer.currentPlayers;
|
||||
existingServer.gameServerUrl = gameServerUrl || existingServer.gameServerUrl;
|
||||
existingServer.status = 'waiting';
|
||||
existingServer.lastActivity = new Date();
|
||||
|
||||
await existingServer.save();
|
||||
logger.info(`[API SERVER] Updated existing server: ${serverId} with ${existingServer.currentPlayers} players`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Server updated successfully',
|
||||
server: existingServer
|
||||
});
|
||||
}
|
||||
|
||||
// Create new server
|
||||
const newServer = new GameServer({
|
||||
serverId,
|
||||
name: name || `Game Server ${serverId}`,
|
||||
type: type || 'public',
|
||||
region: region || 'us-east',
|
||||
maxPlayers: maxPlayers || 10,
|
||||
currentPlayers: currentPlayers !== undefined ? currentPlayers : 0,
|
||||
owner: owner || {
|
||||
userId: 'system',
|
||||
username: 'System'
|
||||
},
|
||||
status: 'waiting',
|
||||
gameServerUrl,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
await newServer.save();
|
||||
logger.info(`[API SERVER] Registered new server: ${serverId}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Server registered successfully',
|
||||
server: newServer
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error registering server:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to register server'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update server status (for GameServer instances to update their status)
|
||||
router.post('/update-status/:serverId', async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
const { currentPlayers, status } = req.body;
|
||||
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
if (!server) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (currentPlayers !== undefined) {
|
||||
server.currentPlayers = currentPlayers;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
server.status = status;
|
||||
}
|
||||
|
||||
server.lastActivity = new Date();
|
||||
await server.save();
|
||||
|
||||
logger.info(`[API SERVER] Updated server ${serverId} status:`, {
|
||||
currentPlayers: server.currentPlayers,
|
||||
status: server.status
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server status updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error updating server status:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update server status'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unregister game server (for GameServer instances to unregister themselves)
|
||||
router.delete('/unregister/:serverId', async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
logger.info(`[API SERVER] Game server unregistration request:`, { serverId });
|
||||
|
||||
// Find and remove server
|
||||
const server = await GameServer.findOneAndDelete({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`[API SERVER] Unregistered server: ${serverId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server unregistered successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error unregistering server:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to unregister server'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get server list
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type, region } = req.query;
|
||||
|
||||
// Build filters
|
||||
const filters = {};
|
||||
if (type) filters.type = type;
|
||||
if (region) filters.region = region;
|
||||
|
||||
logger.info(`[API SERVER] Fetching servers for user ${req.userId} with filters:`, filters);
|
||||
|
||||
// Get available servers from database
|
||||
const servers = await GameServer.findAvailableServers(filters);
|
||||
|
||||
logger.info(`[API SERVER] Found ${servers.length} servers in database`);
|
||||
|
||||
// Format server list for client
|
||||
const serverList = servers.map(server => ({
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
region: server.region,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
status: server.status,
|
||||
ownerName: server.owner.username,
|
||||
createdAt: server.createdAt,
|
||||
lastActivity: server.lastActivity
|
||||
}));
|
||||
|
||||
logger.info(`[API SERVER] Returning ${serverList.length} servers to client`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
servers: serverList,
|
||||
totalServers: serverList.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting server list:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new server
|
||||
router.post('/create', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { name, type = 'public', maxPlayers = 10, region = 'us-east', settings = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Server name required' });
|
||||
}
|
||||
|
||||
// Generate unique server ID
|
||||
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Get user info from token (you might want to fetch full user data)
|
||||
const ownerUsername = req.body.username || 'Unknown'; // This should come from user data
|
||||
|
||||
// Create new server in database
|
||||
const newServer = new GameServer({
|
||||
serverId,
|
||||
name,
|
||||
type,
|
||||
region,
|
||||
maxPlayers,
|
||||
owner: {
|
||||
userId: req.userId,
|
||||
username: ownerUsername
|
||||
},
|
||||
settings,
|
||||
gameServerUrl: process.env.GAME_SERVER_URL || 'https://api.korvarix.com'
|
||||
});
|
||||
|
||||
await newServer.save();
|
||||
|
||||
logger.info(`Server created: ${serverId} by user ${req.userId}`);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Server created successfully',
|
||||
server: {
|
||||
id: newServer.serverId,
|
||||
name: newServer.name,
|
||||
type: newServer.type,
|
||||
region: newServer.region,
|
||||
currentPlayers: newServer.currentPlayers,
|
||||
maxPlayers: newServer.maxPlayers,
|
||||
status: newServer.status,
|
||||
ownerName: newServer.owner.username,
|
||||
createdAt: newServer.createdAt
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error creating server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Join server
|
||||
router.post('/:serverId/join', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
// Find server in database
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
// Check if server can be joined
|
||||
if (!server.canJoin()) {
|
||||
return res.status(400).json({ error: 'Server is full or offline' });
|
||||
}
|
||||
|
||||
// Add player to server
|
||||
const playerAdded = server.addPlayer();
|
||||
if (!playerAdded) {
|
||||
return res.status(400).json({ error: 'Server is full' });
|
||||
}
|
||||
|
||||
await server.save();
|
||||
|
||||
logger.info(`User ${req.userId} joined server ${serverId}`);
|
||||
|
||||
res.json({
|
||||
message: 'Joined server successfully',
|
||||
server: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
gameServerUrl: server.gameServerUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error joining server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Leave server
|
||||
router.post('/:serverId/leave', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
// Find server in database
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
// Remove player from server
|
||||
const playerRemoved = server.removePlayer();
|
||||
if (playerRemoved) {
|
||||
await server.save();
|
||||
logger.info(`User ${req.userId} left server ${serverId}`);
|
||||
}
|
||||
|
||||
// Update player's current server
|
||||
const Player = require('../models/Player');
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: req.userId },
|
||||
{ currentServer: null }
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Left server successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error leaving server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server details
|
||||
router.get('/:serverId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
server: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
region: server.region,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
status: server.status,
|
||||
ownerName: server.owner.username,
|
||||
settings: server.settings,
|
||||
createdAt: server.createdAt,
|
||||
lastActivity: server.lastActivity
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting server details:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user's current server
|
||||
router.get('/user/current', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const Player = require('../models/Player');
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player || !player.currentServer) {
|
||||
return res.json({ currentServer: null });
|
||||
}
|
||||
|
||||
const server = await GameServer.findOne({ serverId: player.currentServer });
|
||||
|
||||
if (!server) {
|
||||
// Clear invalid server reference
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: req.userId },
|
||||
{ currentServer: null }
|
||||
);
|
||||
return res.json({ currentServer: null });
|
||||
}
|
||||
|
||||
res.json({
|
||||
currentServer: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting current server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
67
API/scripts/createTestServer.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Create Test Server Script
|
||||
* Adds a test server to the database for testing the server browser
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GameServer = require('../models/GameServer');
|
||||
require('dotenv').config();
|
||||
|
||||
async function createTestServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log('Connected to database');
|
||||
|
||||
// Check if test server already exists
|
||||
const existingServer = await GameServer.findOne({ serverId: 'test_server_001' });
|
||||
if (existingServer) {
|
||||
console.log('Test server already exists, deleting it first...');
|
||||
await GameServer.deleteOne({ serverId: 'test_server_001' });
|
||||
}
|
||||
|
||||
// Create test server
|
||||
const testServer = new GameServer({
|
||||
serverId: 'test_server_001',
|
||||
name: 'Test Server - Galaxy Strike',
|
||||
type: 'public',
|
||||
region: 'us-east',
|
||||
maxPlayers: 10,
|
||||
currentPlayers: 2,
|
||||
owner: {
|
||||
userId: 'test_user_001',
|
||||
username: 'TestAdmin'
|
||||
},
|
||||
settings: {
|
||||
description: 'A test server for Galaxy Strike Online',
|
||||
tags: ['test', 'beginner', 'pve']
|
||||
},
|
||||
status: 'active',
|
||||
gameServerUrl: 'https://api.korvarix.com'
|
||||
});
|
||||
|
||||
await testServer.save();
|
||||
console.log('Test server created successfully!');
|
||||
console.log('Server details:', {
|
||||
id: testServer.serverId,
|
||||
name: testServer.name,
|
||||
type: testServer.type,
|
||||
region: testServer.region,
|
||||
currentPlayers: testServer.currentPlayers,
|
||||
maxPlayers: testServer.maxPlayers,
|
||||
status: testServer.status
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating test server:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (require.main === module) {
|
||||
createTestServer();
|
||||
}
|
||||
|
||||
module.exports = createTestServer;
|
||||
50
API/scripts/migrate.js
Normal file
@ -0,0 +1,50 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
require('dotenv').config();
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
logger.info('Starting database migration...');
|
||||
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
|
||||
|
||||
logger.info('Connected to database');
|
||||
|
||||
// Create indexes for performance
|
||||
const db = mongoose.connection.db;
|
||||
|
||||
// Player indexes
|
||||
await db.collection('players').createIndex({ userId: 1 }, { unique: true });
|
||||
await db.collection('players').createIndex({ email: 1 }, { unique: true });
|
||||
await db.collection('players').createIndex({ 'stats.level': 1 });
|
||||
await db.collection('players').createIndex({ currentServer: 1 });
|
||||
|
||||
// Ship indexes
|
||||
await db.collection('ships').createIndex({ userId: 1 });
|
||||
await db.collection('ships').createIndex({ id: 1 }, { unique: true });
|
||||
await db.collection('ships').createIndex({ isEquipped: 1 });
|
||||
await db.collection('ships').createIndex({ isCurrent: 1 });
|
||||
|
||||
// Inventory indexes
|
||||
await db.collection('inventories').createIndex({ userId: 1 }, { unique: true });
|
||||
await db.collection('inventories').createIndex({ 'items.id': 1 });
|
||||
await db.collection('inventories').createIndex({ 'items.type': 1 });
|
||||
|
||||
logger.info('Database migration completed successfully');
|
||||
|
||||
// Close connection
|
||||
await mongoose.connection.close();
|
||||
logger.info('Database connection closed');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
migrate();
|
||||
}
|
||||
|
||||
module.exports = migrate;
|
||||
71
API/scripts/migratePasswords.js
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Password Migration Script
|
||||
* Updates existing users to have password fields
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const Player = require('../models/Player');
|
||||
const logger = require('../utils/logger');
|
||||
require('dotenv').config();
|
||||
|
||||
async function migratePasswords() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
logger.info('Connected to database for password migration');
|
||||
|
||||
// Find all users without passwords
|
||||
const usersWithoutPasswords = await Player.find({
|
||||
password: { $exists: false }
|
||||
});
|
||||
|
||||
logger.info(`Found ${usersWithoutPasswords.length} users without passwords`);
|
||||
|
||||
if (usersWithoutPasswords.length === 0) {
|
||||
logger.info('No users need password migration');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update each user with a default password
|
||||
for (const user of usersWithoutPasswords) {
|
||||
// Generate a default password (you might want to use a different approach)
|
||||
const defaultPassword = 'tempPassword123!';
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(defaultPassword, salt);
|
||||
|
||||
await Player.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
'stats.lastLogin': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Migrated user: ${user.email} with default password`);
|
||||
}
|
||||
|
||||
logger.info('Password migration completed successfully');
|
||||
|
||||
// Output the default password for users to change
|
||||
console.log('\n=== MIGRATION COMPLETE ===');
|
||||
console.log(`Updated ${usersWithoutPasswords.length} users`);
|
||||
console.log('Default password for all migrated users: tempPassword123!');
|
||||
console.log('Users should change their password after first login\n');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Password migration error:', error);
|
||||
console.error('Migration failed:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
if (require.main === module) {
|
||||
migratePasswords();
|
||||
}
|
||||
|
||||
module.exports = migratePasswords;
|
||||
196
API/scripts/seed.js
Normal file
@ -0,0 +1,196 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
require('dotenv').config();
|
||||
|
||||
async function seed() {
|
||||
try {
|
||||
logger.info('Starting database seeding...');
|
||||
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
|
||||
|
||||
logger.info('Connected to database');
|
||||
|
||||
// Clear existing data (optional - remove if you want to preserve data)
|
||||
logger.info('Clearing existing data...');
|
||||
await Player.deleteMany({});
|
||||
await Ship.deleteMany({});
|
||||
await Inventory.deleteMany({});
|
||||
|
||||
// Create a test user
|
||||
const testUser = new Player({
|
||||
userId: 'test_user_001',
|
||||
username: 'TestPlayer',
|
||||
email: 'test@example.com',
|
||||
password: '$2a$10$example_hashed_password_here',
|
||||
stats: {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
credits: 5000,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: new Date()
|
||||
},
|
||||
attributes: {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
},
|
||||
info: {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
},
|
||||
settings: {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
discordIntegration: false
|
||||
},
|
||||
dailyRewards: {
|
||||
lastClaim: null,
|
||||
consecutiveDays: 0
|
||||
}
|
||||
});
|
||||
|
||||
await testUser.save();
|
||||
logger.info('Created test user');
|
||||
|
||||
// Create starter ship for test user
|
||||
const starterShip = new Ship({
|
||||
userId: testUser.userId,
|
||||
id: 'starter_cruiser_001',
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
level: 1,
|
||||
stats: {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
attack: 15,
|
||||
defense: 12,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
hull: 100
|
||||
},
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
experience: 0,
|
||||
requiredExp: 100,
|
||||
upgrades: [],
|
||||
isEquipped: true,
|
||||
isCurrent: true,
|
||||
price: 5000,
|
||||
rarity: 'common',
|
||||
description: 'Reliable starter cruiser for new pilots',
|
||||
acquiredAt: new Date(),
|
||||
lastUsed: new Date()
|
||||
});
|
||||
|
||||
await starterShip.save();
|
||||
logger.info('Created starter ship');
|
||||
|
||||
// Update player with current ship
|
||||
testUser.currentShip = starterShip._id;
|
||||
await testUser.save();
|
||||
|
||||
// Create inventory for test user
|
||||
const inventory = new Inventory({
|
||||
userId: testUser.userId,
|
||||
maxSlots: 50,
|
||||
items: [
|
||||
{
|
||||
id: 'starter_blaster_common',
|
||||
name: 'Common Blaster',
|
||||
type: 'weapon',
|
||||
rarity: 'common',
|
||||
quantity: 1,
|
||||
stats: {
|
||||
attack: 5,
|
||||
criticalChance: 0.02,
|
||||
damage: 10,
|
||||
fireRate: 2,
|
||||
range: 5,
|
||||
energy: 5
|
||||
},
|
||||
description: 'A reliable basic blaster for new pilots',
|
||||
equipable: true,
|
||||
slot: 'weapon',
|
||||
isEquipped: false,
|
||||
stackable: false,
|
||||
acquiredAt: new Date()
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_common',
|
||||
name: 'Basic Armor',
|
||||
type: 'armor',
|
||||
rarity: 'common',
|
||||
quantity: 1,
|
||||
stats: {
|
||||
defense: 3,
|
||||
durability: 20,
|
||||
weight: 2,
|
||||
energyShield: 0
|
||||
},
|
||||
description: 'Light armor providing basic protection',
|
||||
equipable: true,
|
||||
slot: 'armor',
|
||||
isEquipped: false,
|
||||
stackable: false,
|
||||
acquiredAt: new Date()
|
||||
},
|
||||
{
|
||||
id: 'health_kit',
|
||||
name: 'Health Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
quantity: 3,
|
||||
stats: {},
|
||||
description: 'A medical kit that restores health',
|
||||
consumable: true,
|
||||
effect: {
|
||||
health: 50
|
||||
},
|
||||
stackable: true,
|
||||
acquiredAt: new Date()
|
||||
}
|
||||
],
|
||||
equippedItems: {
|
||||
weapon: null,
|
||||
armor: null,
|
||||
engine: null,
|
||||
shield: null,
|
||||
special: null
|
||||
}
|
||||
});
|
||||
|
||||
await inventory.save();
|
||||
logger.info('Created inventory with starter items');
|
||||
|
||||
logger.info('Database seeding completed successfully');
|
||||
|
||||
// Close connection
|
||||
await mongoose.connection.close();
|
||||
logger.info('Database connection closed');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Seeding failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
seed();
|
||||
}
|
||||
|
||||
module.exports = seed;
|
||||
154
API/server.js
Normal file
@ -0,0 +1,154 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('rate-limiter-flexible');
|
||||
require('dotenv').config();
|
||||
|
||||
const logger = require('./utils/logger');
|
||||
const connectDB = require('./config/database');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const serverRoutes = require('./routes/servers');
|
||||
const modsRoutes = require('./routes/mods');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
|
||||
// Override console.error to properly log error objects
|
||||
const originalConsoleError = console.error;
|
||||
console.error = (...args) => {
|
||||
args.forEach(arg => {
|
||||
if (arg instanceof Error) {
|
||||
logger.error('Console Error:', {
|
||||
message: arg.message,
|
||||
stack: arg.stack,
|
||||
name: arg.name
|
||||
});
|
||||
} else if (typeof arg === 'object' && arg !== null) {
|
||||
logger.error('Console Error Object:', arg);
|
||||
} else {
|
||||
logger.error('Console Error:', arg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(compression());
|
||||
app.use(cors({
|
||||
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://api.korvarix.com:3001"],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Static file serving
|
||||
app.use(express.static('../Website/dist'));
|
||||
|
||||
// Rate limiting
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const limiter = new RateLimiterMemory({
|
||||
keyGenerator: (req) => req.ip,
|
||||
points: 100, // limit each IP to 100 requests per windowMs
|
||||
duration: 900, // 15 minutes
|
||||
blockDuration: 900, // Block for 15 minutes
|
||||
});
|
||||
app.use('/api/', async (req, res, next) => {
|
||||
try {
|
||||
const resLimiter = await limiter.consume(req.ip);
|
||||
if (!resLimiter.remainingPoints) {
|
||||
return res.status(429).json({ error: 'Too many requests, please try again later.' });
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Routes - API Server Only (Auth + Server Browser + Mods)
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/servers', serverRoutes);
|
||||
app.use('/api/mods', modsRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'API Server OK',
|
||||
service: 'galaxystrikeonline-api',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
// API version endpoint
|
||||
app.get('/api/ssc/version', (req, res) => {
|
||||
res.status(200).json({
|
||||
version: '1.0.0',
|
||||
service: 'galaxystrikeonline-api',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Fallback route for SPA - only serve index.html for non-API routes
|
||||
app.get('*', (req, res) => {
|
||||
// Don't try to serve index.html for API routes
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(404).json({ error: 'API endpoint not found' });
|
||||
}
|
||||
|
||||
// Try dist first (for built files), fallback to public (for development)
|
||||
const distPath = require('path').resolve(__dirname, '../dist/index.html');
|
||||
const publicPath = require('path').resolve(__dirname, '../public/index.html');
|
||||
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(distPath)) {
|
||||
res.sendFile(distPath);
|
||||
} else if (fs.existsSync(publicPath)) {
|
||||
res.sendFile(publicPath);
|
||||
} else {
|
||||
res.status(404).json({ error: 'Frontend not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Initialize database only (no game systems for API server)
|
||||
async function startServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB();
|
||||
logger.info('Database connected successfully');
|
||||
|
||||
// Start API server
|
||||
const PORT = process.env.PORT || 3001;
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`API Server running on port ${PORT}`);
|
||||
logger.info('API Server handles: Authentication, Server Browser, User Data');
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start API server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('Uncaught Exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// Handle HTTP server errors
|
||||
server.on('error', (error) => {
|
||||
logger.error('HTTP Server error:', error);
|
||||
});
|
||||
|
||||
startServer();
|
||||
|
||||
module.exports = { app, server };
|
||||
182
API/services/ModService.js
Normal file
@ -0,0 +1,182 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* API Mod Service - Handles mod management through API communication with GameServer
|
||||
* This service acts as a client to the GameServer's mod functionality
|
||||
*/
|
||||
class ApiModService {
|
||||
constructor() {
|
||||
// Use service discovery - try multiple common GameServer locations
|
||||
this.gameServerUrls = [
|
||||
process.env.GAME_SERVER_URL,
|
||||
'http://localhost:3002',
|
||||
'http://127.0.0.1:3002',
|
||||
'http://game-server:3002' // Docker service name
|
||||
].filter(url => url); // Remove null/undefined values
|
||||
|
||||
this.currentUrlIndex = 0;
|
||||
}
|
||||
|
||||
async getGameServerUrl() {
|
||||
// Try each URL until we find a working one
|
||||
for (let i = 0; i < this.gameServerUrls.length; i++) {
|
||||
const url = this.gameServerUrls[i];
|
||||
try {
|
||||
const response = await fetch(`${url}/health`, {
|
||||
timeout: 5000 // 5 second timeout
|
||||
});
|
||||
if (response.ok) {
|
||||
this.currentUrlIndex = i;
|
||||
return url;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`[API MOD SERVICE] GameServer at ${url} not available:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No GameServer instances available');
|
||||
}
|
||||
|
||||
async makeRequest(endpoint, options = {}) {
|
||||
const url = await this.getGameServerUrl();
|
||||
const fullUrl = `${url}${endpoint}`;
|
||||
|
||||
const defaultOptions = {
|
||||
timeout: 10000, // 10 second timeout
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'API-ModService/1.0'
|
||||
}
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, finalOptions);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
logger.error(`[API MOD SERVICE] Request to ${fullUrl} failed:`, error);
|
||||
|
||||
// If request failed, try next GameServer URL
|
||||
this.currentUrlIndex = (this.currentUrlIndex + 1) % this.gameServerUrls.length;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMods(enabledOnly = false) {
|
||||
try {
|
||||
const endpoint = `/api/mods${enabledOnly ? '?enabledOnly=true' : ''}`;
|
||||
return await this.makeRequest(endpoint);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mods:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async enableMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}/enable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error enabling mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async disableMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}/disable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error disabling mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getModAssets(assetType) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/assets/${assetType}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getModAsset(assetType, assetId) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/assets/${assetType}/${assetId}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod asset:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getLoadOrder() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/load-order');
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting load order:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setLoadOrder(modIds) {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/load-order', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ modIds })
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error setting load order:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getServerPreferences() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/preferences/server');
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting server preferences:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setServerPreference(key, value) {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/preferences/server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error setting server preference:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reloadMods() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/reload', {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error reloading mods:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ApiModService();
|
||||
272
API/socket/socketHandlers.js
Normal file
@ -0,0 +1,272 @@
|
||||
const logger = require('../utils/logger');
|
||||
const { getGameSystem } = require('../systems/GameSystem');
|
||||
const Player = require('../models/Player');
|
||||
|
||||
class SocketHandlers {
|
||||
constructor(io) {
|
||||
this.io = io;
|
||||
this.connectedUsers = new Map(); // userId -> socket.id
|
||||
this.userSockets = new Map(); // socket.id -> userId
|
||||
}
|
||||
|
||||
handleConnection(socket) {
|
||||
logger.info(`Client connected: ${socket.id}`);
|
||||
|
||||
// Authentication
|
||||
socket.on('authenticate', async (token) => {
|
||||
try {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
|
||||
const player = await Player.findOne({ userId: decoded.userId });
|
||||
if (!player) {
|
||||
socket.emit('auth_error', { error: 'Player not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Store user connection
|
||||
this.connectedUsers.set(decoded.userId, socket.id);
|
||||
this.userSockets.set(socket.id, decoded.userId);
|
||||
|
||||
socket.userId = decoded.userId;
|
||||
socket.emit('authenticated', { userId: decoded.userId });
|
||||
|
||||
logger.info(`User authenticated: ${decoded.userId}`);
|
||||
|
||||
// Join user to their current server if any
|
||||
if (player.currentServer) {
|
||||
socket.join(player.currentServer);
|
||||
this.broadcastToServer(player.currentServer, 'user_joined', {
|
||||
userId: decoded.userId,
|
||||
username: player.username
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Authentication error:', error);
|
||||
socket.emit('auth_error', { error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Server management
|
||||
socket.on('join_server', async (data) => {
|
||||
try {
|
||||
if (!socket.userId) {
|
||||
socket.emit('error', { error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const server = await gameSystem.joinServer(data.serverId, socket.userId);
|
||||
|
||||
// Update player's current server
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: socket.userId },
|
||||
{ currentServer: data.serverId }
|
||||
);
|
||||
|
||||
// Join socket room
|
||||
socket.join(data.serverId);
|
||||
|
||||
socket.emit('server_joined', { server });
|
||||
this.broadcastToServer(data.serverId, 'user_joined', {
|
||||
userId: socket.userId,
|
||||
serverId: data.serverId
|
||||
});
|
||||
|
||||
logger.info(`User ${socket.userId} joined server ${data.serverId}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error joining server:', error);
|
||||
socket.emit('error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('leave_server', async (data) => {
|
||||
try {
|
||||
if (!socket.userId) {
|
||||
socket.emit('error', { error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const server = await gameSystem.leaveServer(data.serverId, socket.userId);
|
||||
|
||||
// Update player's current server
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: socket.userId },
|
||||
{ currentServer: null }
|
||||
);
|
||||
|
||||
// Leave socket room
|
||||
socket.leave(data.serverId);
|
||||
|
||||
socket.emit('server_left', { server });
|
||||
this.broadcastToServer(data.serverId, 'user_left', {
|
||||
userId: socket.userId,
|
||||
serverId: data.serverId
|
||||
});
|
||||
|
||||
logger.info(`User ${socket.userId} left server ${data.serverId}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error leaving server:', error);
|
||||
socket.emit('error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Game actions
|
||||
socket.on('game_action', async (data) => {
|
||||
try {
|
||||
if (!socket.userId) {
|
||||
socket.emit('error', { error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const result = await gameSystem.processGameAction(socket.userId, data);
|
||||
|
||||
socket.emit('action_result', { action: data.type, result });
|
||||
|
||||
// Broadcast relevant actions to server
|
||||
if (data.broadcast && socket.userId) {
|
||||
const player = await Player.findOne({ userId: socket.userId });
|
||||
if (player && player.currentServer) {
|
||||
this.broadcastToServer(player.currentServer, 'user_action', {
|
||||
userId: socket.userId,
|
||||
username: player.username,
|
||||
action: data.type,
|
||||
result
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error processing game action:', error);
|
||||
socket.emit('error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Chat functionality
|
||||
socket.on('send_message', async (data) => {
|
||||
try {
|
||||
if (!socket.userId) {
|
||||
socket.emit('error', { error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await Player.findOne({ userId: socket.userId });
|
||||
if (!player || !player.currentServer) {
|
||||
socket.emit('error', { error: 'Not in a server' });
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
userId: socket.userId,
|
||||
username: player.username,
|
||||
message: data.message,
|
||||
timestamp: new Date(),
|
||||
type: data.type || 'chat'
|
||||
};
|
||||
|
||||
// Broadcast to server
|
||||
this.broadcastToServer(player.currentServer, 'new_message', message);
|
||||
|
||||
logger.info(`Chat message from ${socket.userId} in server ${player.currentServer}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error sending message:', error);
|
||||
socket.emit('error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time updates
|
||||
socket.on('request_server_status', async () => {
|
||||
try {
|
||||
if (!socket.userId) {
|
||||
socket.emit('error', { error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await Player.findOne({ userId: socket.userId });
|
||||
if (!player || !player.currentServer) {
|
||||
socket.emit('server_status', { server: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const server = gameSystem.servers.get(player.currentServer);
|
||||
|
||||
if (server) {
|
||||
const players = await Player.find({
|
||||
userId: { $in: server.players }
|
||||
}).select('userId username info.stats.level');
|
||||
|
||||
socket.emit('server_status', {
|
||||
server: {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
currentPlayers: server.players.length,
|
||||
maxPlayers: server.maxPlayers,
|
||||
players: players.map(p => ({
|
||||
userId: p.userId,
|
||||
username: p.username,
|
||||
level: p.info.stats.level
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting server status:', error);
|
||||
socket.emit('error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnection
|
||||
socket.on('disconnect', async () => {
|
||||
logger.info(`Client disconnected: ${socket.id}`);
|
||||
|
||||
const userId = this.userSockets.get(socket.id);
|
||||
if (userId) {
|
||||
// Remove from tracking
|
||||
this.connectedUsers.delete(userId);
|
||||
this.userSockets.delete(socket.id);
|
||||
|
||||
// Notify server if user was in one
|
||||
const player = await Player.findOne({ userId });
|
||||
if (player && player.currentServer) {
|
||||
this.broadcastToServer(player.currentServer, 'user_disconnected', {
|
||||
userId,
|
||||
username: player.username
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
broadcastToServer(serverId, event, data) {
|
||||
this.io.to(serverId).emit(event, data);
|
||||
}
|
||||
|
||||
sendToUser(userId, event, data) {
|
||||
const socketId = this.connectedUsers.get(userId);
|
||||
if (socketId) {
|
||||
this.io.to(socketId).emit(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
broadcastToAll(event, data) {
|
||||
this.io.emit(event, data);
|
||||
}
|
||||
|
||||
getConnectedUsers() {
|
||||
return Array.from(this.connectedUsers.keys());
|
||||
}
|
||||
|
||||
getUserCount() {
|
||||
return this.connectedUsers.size;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SocketHandlers;
|
||||
385
API/systems/EconomySystem.js
Normal file
@ -0,0 +1,385 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class EconomySystem {
|
||||
constructor() {
|
||||
this.shopItems = {
|
||||
ships: [],
|
||||
weapons: [],
|
||||
armors: [],
|
||||
materials: [],
|
||||
consumables: []
|
||||
};
|
||||
|
||||
this.dailyRewards = {
|
||||
baseReward: 100,
|
||||
consecutiveBonus: 50,
|
||||
maxConsecutiveDays: 30
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
logger.info('Initializing Economy System...');
|
||||
|
||||
// Initialize shop items
|
||||
await this.initializeShopItems();
|
||||
|
||||
logger.info('Economy System initialized successfully');
|
||||
}
|
||||
|
||||
async initializeShopItems() {
|
||||
// Ships
|
||||
this.shopItems.ships = [
|
||||
// Starter Cruiser Variants
|
||||
{
|
||||
id: 'starter_cruiser_common',
|
||||
name: 'Starter Cruiser',
|
||||
type: 'ship',
|
||||
rarity: 'common',
|
||||
price: 5000,
|
||||
currency: 'credits',
|
||||
description: 'Reliable starter cruiser for new pilots',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 15, speed: 10, defense: 12, hull: 100 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_uncommon',
|
||||
name: 'Starter Cruiser II',
|
||||
type: 'ship',
|
||||
rarity: 'uncommon',
|
||||
price: 12000,
|
||||
currency: 'credits',
|
||||
description: 'Upgraded starter cruiser with enhanced systems',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 18, speed: 12, defense: 15, hull: 120 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_rare',
|
||||
name: 'Starter Cruiser III',
|
||||
type: 'ship',
|
||||
rarity: 'rare',
|
||||
price: 25000,
|
||||
currency: 'credits',
|
||||
description: 'Elite starter cruiser with advanced weaponry',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 22, speed: 14, defense: 18, hull: 140 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_epic',
|
||||
name: 'Starter Cruiser IV',
|
||||
type: 'ship',
|
||||
rarity: 'epic',
|
||||
price: 50000,
|
||||
currency: 'credits',
|
||||
description: 'Master starter cruiser with elite modifications',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 28, speed: 16, defense: 22, hull: 160 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_legendary',
|
||||
name: 'Starter Cruiser V',
|
||||
type: 'ship',
|
||||
rarity: 'legendary',
|
||||
price: 100000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary starter cruiser with unparalleled performance',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 35, speed: 18, defense: 28, hull: 180 }
|
||||
}
|
||||
];
|
||||
|
||||
// Weapons
|
||||
this.shopItems.weapons = [
|
||||
// Starter Blaster Variants
|
||||
{
|
||||
id: 'starter_blaster_common',
|
||||
name: 'Common Blaster',
|
||||
type: 'weapon',
|
||||
rarity: 'common',
|
||||
price: 1000,
|
||||
currency: 'credits',
|
||||
description: 'Basic blaster for new pilots',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 10, fireRate: 2, range: 5, energy: 5 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_uncommon',
|
||||
name: 'Starter Blaster II',
|
||||
type: 'weapon',
|
||||
rarity: 'uncommon',
|
||||
price: 2500,
|
||||
currency: 'credits',
|
||||
description: 'Improved blaster with better damage output',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 12, fireRate: 2.2, range: 5.5, energy: 6 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_rare',
|
||||
name: 'Starter Blaster III',
|
||||
type: 'weapon',
|
||||
rarity: 'rare',
|
||||
price: 5000,
|
||||
currency: 'credits',
|
||||
description: 'Advanced blaster with enhanced capabilities',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 15, fireRate: 2.5, range: 6, energy: 7 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_epic',
|
||||
name: 'Starter Blaster IV',
|
||||
type: 'weapon',
|
||||
rarity: 'epic',
|
||||
price: 10000,
|
||||
currency: 'credits',
|
||||
description: 'Elite blaster with superior performance',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 18, fireRate: 3, range: 6.5, energy: 8 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_legendary',
|
||||
name: 'Starter Blaster V',
|
||||
type: 'weapon',
|
||||
rarity: 'legendary',
|
||||
price: 20000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary starter blaster with ultimate power',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 22, fireRate: 4, range: 7, energy: 10 }
|
||||
}
|
||||
];
|
||||
|
||||
// Armors
|
||||
this.shopItems.armors = [
|
||||
// Basic Armor Variants
|
||||
{
|
||||
id: 'basic_armor_common',
|
||||
name: 'Basic Armor',
|
||||
type: 'armor',
|
||||
rarity: 'common',
|
||||
price: 1500,
|
||||
currency: 'credits',
|
||||
description: 'Light protection for beginners',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 5, durability: 20, weight: 2, energyShield: 0 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_uncommon',
|
||||
name: 'Basic Armor II',
|
||||
type: 'armor',
|
||||
rarity: 'uncommon',
|
||||
price: 4000,
|
||||
currency: 'credits',
|
||||
description: 'Improved basic armor with better durability',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 7, durability: 25, weight: 2.2, energyShield: 2 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_rare',
|
||||
name: 'Basic Armor III',
|
||||
type: 'armor',
|
||||
rarity: 'rare',
|
||||
price: 8000,
|
||||
currency: 'credits',
|
||||
description: 'Enhanced armor with energy shielding',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 10, durability: 30, weight: 2.5, energyShield: 5 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_epic',
|
||||
name: 'Basic Armor IV',
|
||||
type: 'armor',
|
||||
rarity: 'epic',
|
||||
price: 15000,
|
||||
currency: 'credits',
|
||||
description: 'Elite armor with advanced protection systems',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 15, durability: 35, weight: 3, energyShield: 10 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_legendary',
|
||||
name: 'Basic Armor V',
|
||||
type: 'armor',
|
||||
rarity: 'legendary',
|
||||
price: 30000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary armor with ultimate protection',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 20, durability: 40, weight: 3.5, energyShield: 15 }
|
||||
}
|
||||
];
|
||||
|
||||
// Materials
|
||||
this.shopItems.materials = [
|
||||
{
|
||||
id: 'iron_ore',
|
||||
name: 'Iron Ore',
|
||||
type: 'material',
|
||||
rarity: 'common',
|
||||
price: 50,
|
||||
currency: 'credits',
|
||||
description: 'Raw iron ore used for crafting basic weapons and armor',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'copper_wire',
|
||||
name: 'Copper Wire',
|
||||
type: 'material',
|
||||
rarity: 'common',
|
||||
price: 75,
|
||||
currency: 'credits',
|
||||
description: 'Copper wiring used in electronic components',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'energy_crystal',
|
||||
name: 'Energy Crystal',
|
||||
type: 'material',
|
||||
rarity: 'uncommon',
|
||||
price: 200,
|
||||
currency: 'credits',
|
||||
description: 'Crystallized energy used for powered equipment',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'rare_metal',
|
||||
name: 'Rare Metal',
|
||||
type: 'material',
|
||||
rarity: 'rare',
|
||||
price: 500,
|
||||
currency: 'credits',
|
||||
description: 'Rare metallic alloy used for high-end crafting',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'advanced_components',
|
||||
name: 'Advanced Components',
|
||||
type: 'material',
|
||||
rarity: 'rare',
|
||||
price: 1000,
|
||||
currency: 'credits',
|
||||
description: 'Sophisticated electronic components for advanced ship systems',
|
||||
stackable: true
|
||||
}
|
||||
];
|
||||
|
||||
// Consumables
|
||||
this.shopItems.consumables = [
|
||||
{
|
||||
id: 'health_kit',
|
||||
name: 'Health Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
price: 100,
|
||||
currency: 'credits',
|
||||
description: 'A medical kit that restores health',
|
||||
consumable: true,
|
||||
effect: { health: 50 }
|
||||
},
|
||||
{
|
||||
id: 'energy_pack',
|
||||
name: 'Energy Pack',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
price: 150,
|
||||
currency: 'credits',
|
||||
description: 'A pack that restores energy',
|
||||
consumable: true,
|
||||
effect: { energy: 25 }
|
||||
},
|
||||
{
|
||||
id: 'repair_kit',
|
||||
name: 'Repair Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'uncommon',
|
||||
price: 300,
|
||||
currency: 'credits',
|
||||
description: 'A kit that repairs ship damage',
|
||||
consumable: true,
|
||||
effect: { health: 100 }
|
||||
}
|
||||
];
|
||||
|
||||
logger.info(`Shop initialized with ${this.getTotalShopItems()} items`);
|
||||
}
|
||||
|
||||
getTotalShopItems() {
|
||||
return Object.values(this.shopItems).reduce((total, category) => total + category.length, 0);
|
||||
}
|
||||
|
||||
getShopItems(category = null) {
|
||||
if (category && this.shopItems[category]) {
|
||||
return this.shopItems[category];
|
||||
}
|
||||
return this.shopItems;
|
||||
}
|
||||
|
||||
getItem(itemId) {
|
||||
for (const category of Object.values(this.shopItems)) {
|
||||
const item = category.find(item => item.id === itemId);
|
||||
if (item) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
purchaseItem(userId, itemId, quantity = 1) {
|
||||
const item = this.getItem(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Item not found in shop');
|
||||
}
|
||||
|
||||
const totalCost = item.price * quantity;
|
||||
|
||||
return {
|
||||
item,
|
||||
quantity,
|
||||
totalCost,
|
||||
currency: item.currency
|
||||
};
|
||||
}
|
||||
|
||||
calculateDailyReward(consecutiveDays) {
|
||||
const bonusMultiplier = Math.min(consecutiveDays - 1, this.dailyRewards.maxConsecutiveDays - 1);
|
||||
const bonusAmount = bonusMultiplier * this.dailyRewards.consecutiveBonus;
|
||||
const totalReward = this.dailyRewards.baseReward + bonusAmount;
|
||||
|
||||
return {
|
||||
baseReward: this.dailyRewards.baseReward,
|
||||
consecutiveBonus: bonusAmount,
|
||||
totalReward,
|
||||
consecutiveDays
|
||||
};
|
||||
}
|
||||
|
||||
getRandomShopItems(category, count = 6) {
|
||||
const items = this.shopItems[category] || [];
|
||||
const shuffled = [...items].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, Math.min(count, items.length));
|
||||
}
|
||||
|
||||
refreshShopInventory() {
|
||||
logger.info('Refreshing shop inventory...');
|
||||
// This would typically involve database operations
|
||||
// For now, we'll just log the refresh
|
||||
return true;
|
||||
}
|
||||
|
||||
getShopStats() {
|
||||
const stats = {
|
||||
totalItems: this.getTotalShopItems(),
|
||||
itemsByCategory: {},
|
||||
averagePriceByCategory: {}
|
||||
};
|
||||
|
||||
for (const [category, items] of Object.entries(this.shopItems)) {
|
||||
stats.itemsByCategory[category] = items.length;
|
||||
|
||||
if (items.length > 0) {
|
||||
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
|
||||
stats.averagePriceByCategory[category] = Math.round(totalPrice / items.length);
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EconomySystem;
|
||||
293
API/systems/GameSystem.js
Normal file
@ -0,0 +1,293 @@
|
||||
const logger = require('../utils/logger');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
const Economy = require('./EconomySystem');
|
||||
|
||||
class GameSystem {
|
||||
constructor() {
|
||||
this.players = new Map();
|
||||
this.servers = new Map();
|
||||
this.economy = new Economy();
|
||||
}
|
||||
|
||||
async initializeGameSystems() {
|
||||
logger.info('Initializing server-side game systems...');
|
||||
|
||||
// Initialize economy system
|
||||
await this.economy.initialize();
|
||||
|
||||
logger.info('Game systems initialized successfully');
|
||||
}
|
||||
|
||||
// Player management
|
||||
async createPlayer(userId, playerData) {
|
||||
try {
|
||||
const player = new Player({
|
||||
userId,
|
||||
...playerData,
|
||||
createdAt: new Date(),
|
||||
lastLogin: new Date()
|
||||
});
|
||||
|
||||
await player.save();
|
||||
this.players.set(userId, player);
|
||||
|
||||
logger.info(`Created new player for user: ${userId}`);
|
||||
return player;
|
||||
} catch (error) {
|
||||
logger.error('Error creating player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadPlayer(userId) {
|
||||
try {
|
||||
let player = this.players.get(userId);
|
||||
|
||||
if (!player) {
|
||||
player = await Player.findOne({ userId }).populate('ships inventory');
|
||||
if (player) {
|
||||
this.players.set(userId, player);
|
||||
}
|
||||
}
|
||||
|
||||
return player;
|
||||
} catch (error) {
|
||||
logger.error('Error loading player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async savePlayer(userId) {
|
||||
try {
|
||||
const player = this.players.get(userId);
|
||||
if (player) {
|
||||
await player.save();
|
||||
logger.info(`Saved player data for user: ${userId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error saving player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Ship management
|
||||
async addShipToPlayer(userId, shipData) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
const ship = new Ship({
|
||||
...shipData,
|
||||
userId,
|
||||
acquiredAt: new Date()
|
||||
});
|
||||
|
||||
await ship.save();
|
||||
player.ships.push(ship._id);
|
||||
await player.save();
|
||||
|
||||
logger.info(`Added ship ${ship.name} to player ${userId}`);
|
||||
return ship;
|
||||
} catch (error) {
|
||||
logger.error('Error adding ship to player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async equipShip(userId, shipId) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
const ship = await Ship.findOne({ _id: shipId, userId });
|
||||
if (!ship) {
|
||||
throw new Error('Ship not found');
|
||||
}
|
||||
|
||||
// Unequip current ship
|
||||
if (player.currentShip) {
|
||||
await Ship.findByIdAndUpdate(player.currentShip, { isEquipped: false });
|
||||
}
|
||||
|
||||
// Equip new ship
|
||||
ship.isEquipped = true;
|
||||
await ship.save();
|
||||
|
||||
player.currentShip = ship._id;
|
||||
await player.save();
|
||||
|
||||
logger.info(`Equipped ship ${ship.name} for player ${userId}`);
|
||||
return ship;
|
||||
} catch (error) {
|
||||
logger.error('Error equipping ship:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Server management
|
||||
async createServer(serverData) {
|
||||
try {
|
||||
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const server = {
|
||||
id: serverId,
|
||||
...serverData,
|
||||
createdAt: new Date(),
|
||||
players: [],
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this.servers.set(serverId, server);
|
||||
logger.info(`Created new server: ${serverId}`);
|
||||
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error creating server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async joinServer(serverId, userId) {
|
||||
try {
|
||||
const server = this.servers.get(serverId);
|
||||
if (!server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
|
||||
if (server.players.length >= server.maxPlayers) {
|
||||
throw new Error('Server is full');
|
||||
}
|
||||
|
||||
if (!server.players.includes(userId)) {
|
||||
server.players.push(userId);
|
||||
}
|
||||
|
||||
logger.info(`Player ${userId} joined server ${serverId}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error joining server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async leaveServer(serverId, userId) {
|
||||
try {
|
||||
const server = this.servers.get(serverId);
|
||||
if (!server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
|
||||
server.players = server.players.filter(id => id !== userId);
|
||||
|
||||
if (server.players.length === 0) {
|
||||
this.servers.delete(serverId);
|
||||
logger.info(`Server ${serverId} deleted (no players)`);
|
||||
}
|
||||
|
||||
logger.info(`Player ${userId} left server ${serverId}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error leaving server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getServerList() {
|
||||
return Array.from(this.servers.values()).map(server => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
maxPlayers: server.maxPlayers,
|
||||
currentPlayers: server.players.length,
|
||||
status: server.status,
|
||||
region: server.region,
|
||||
createdAt: server.createdAt
|
||||
}));
|
||||
}
|
||||
|
||||
// Game actions
|
||||
async processGameAction(userId, actionData) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
switch (actionData.type) {
|
||||
case 'dungeon_enter':
|
||||
return await this.handleDungeonEnter(player, actionData);
|
||||
case 'ship_upgrade':
|
||||
return await this.handleShipUpgrade(player, actionData);
|
||||
case 'item_purchase':
|
||||
return await this.handleItemPurchase(player, actionData);
|
||||
case 'daily_reward':
|
||||
return await this.handleDailyReward(player, actionData);
|
||||
default:
|
||||
throw new Error('Unknown action type');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing game action:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async handleDungeonEnter(player, data) {
|
||||
// Dungeon logic will be implemented here
|
||||
logger.info(`Player ${player.userId} entering dungeon`);
|
||||
return { success: true, message: 'Dungeon entered' };
|
||||
}
|
||||
|
||||
async handleShipUpgrade(player, data) {
|
||||
// Ship upgrade logic will be implemented here
|
||||
logger.info(`Player ${player.userId} upgrading ship`);
|
||||
return { success: true, message: 'Ship upgraded' };
|
||||
}
|
||||
|
||||
async handleItemPurchase(player, data) {
|
||||
// Item purchase logic will be implemented here
|
||||
logger.info(`Player ${player.userId} purchasing item`);
|
||||
return { success: true, message: 'Item purchased' };
|
||||
}
|
||||
|
||||
async handleDailyReward(player, data) {
|
||||
// Daily reward logic will be implemented here
|
||||
logger.info(`Player ${player.userId} claiming daily reward`);
|
||||
return { success: true, message: 'Daily reward claimed' };
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let gameSystem = null;
|
||||
|
||||
async function initializeGameSystems() {
|
||||
if (!gameSystem) {
|
||||
gameSystem = new GameSystem();
|
||||
try {
|
||||
await gameSystem.initializeGameSystems();
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize game systems:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
function getGameSystem() {
|
||||
if (!gameSystem) {
|
||||
logger.warn('Game system not initialized. Call initializeGameSystems() first.');
|
||||
return null;
|
||||
}
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GameSystem,
|
||||
initializeGameSystems,
|
||||
getGameSystem
|
||||
};
|
||||
220
API/tests/api.test.js
Normal file
@ -0,0 +1,220 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const app = require('../server');
|
||||
const Player = require('../models/Player');
|
||||
|
||||
describe('API Tests', () => {
|
||||
let token;
|
||||
let testUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Connect to test database
|
||||
const mongoUri = process.env.MONGODB_TEST_URI || 'mongodb://localhost:27017/galaxystrikeonline_test';
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up and close connection
|
||||
await Player.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up before each test
|
||||
await Player.deleteMany({});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('POST /api/auth/register - should register new user', async () => {
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
expect(response.body.user).toHaveProperty('email', userData.email);
|
||||
});
|
||||
|
||||
test('POST /api/auth/login - should login existing user', async () => {
|
||||
// First register a user
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
// Then login
|
||||
const loginData = {
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
token = response.body.token;
|
||||
testUser = response.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/auth/verify - should verify token', async () => {
|
||||
// First login to get token
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const registerResponse = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
token = registerResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('valid', true);
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for game tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/game/player - should get player data', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/player')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('userId');
|
||||
expect(response.body).toHaveProperty('stats');
|
||||
expect(response.body).toHaveProperty('attributes');
|
||||
});
|
||||
|
||||
test('GET /api/game/ships - should get player ships', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/ships')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('ships');
|
||||
expect(Array.isArray(response.body.ships)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/game/inventory - should get player inventory', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/inventory')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('items');
|
||||
expect(response.body).toHaveProperty('summary');
|
||||
});
|
||||
|
||||
test('POST /api/game/daily-reward - should claim daily reward', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/game/daily-reward')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for server tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/servers - should get server list', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/servers')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('servers');
|
||||
expect(response.body).toHaveProperty('totalServers');
|
||||
expect(Array.isArray(response.body.servers)).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /api/servers/create - should create new server', async () => {
|
||||
const serverData = {
|
||||
name: 'Test Server',
|
||||
type: 'public',
|
||||
maxPlayers: 10,
|
||||
region: 'us-east'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/servers/create')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send(serverData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.server).toHaveProperty('name', serverData.name);
|
||||
expect(response.body.server).toHaveProperty('type', serverData.type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check', () => {
|
||||
test('GET /health - should return health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('status', 'OK');
|
||||
expect(response.body).toHaveProperty('timestamp');
|
||||
expect(response.body).toHaveProperty('uptime');
|
||||
});
|
||||
});
|
||||
});
|
||||
27
API/utils/logger.js
Normal file
@ -0,0 +1,27 @@
|
||||
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()
|
||||
),
|
||||
defaultMeta: { service: 'galaxystrikeonline-server' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
]
|
||||
});
|
||||
|
||||
// Add console transport for development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
BIN
Client/assets/textures/armors/basic_armor.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
Client/assets/textures/armors/heavy_armor.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
Client/assets/textures/armors/medium_armor.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
Client/assets/textures/base/command_center.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
Client/assets/textures/base/mining_facility.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
Client/assets/textures/items/advanced_circuitboard.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
Client/assets/textures/items/advanced_component.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
Client/assets/textures/items/advanced_components.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
Client/assets/textures/items/bandages.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
Client/assets/textures/items/basic_circuitboard.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
Client/assets/textures/items/battery.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
Client/assets/textures/items/common_circuitboard.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
Client/assets/textures/items/copper_ore.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
Client/assets/textures/items/copper_wire.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
Client/assets/textures/items/energy_crystal.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
Client/assets/textures/items/health_pack.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
Client/assets/textures/items/herbs.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
Client/assets/textures/items/iron_ore.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
Client/assets/textures/items/leather.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
Client/assets/textures/items/mega_health_pack.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
Client/assets/textures/items/stell_plate.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
Client/assets/textures/items/tin_bar.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
Client/assets/textures/missing-texture.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Client/assets/textures/ships/heavy_cruiser.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
Client/assets/textures/ships/heavy_destroyer.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
Client/assets/textures/ships/light_destroyer.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
Client/assets/textures/ships/starter_cruiser.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
Client/assets/textures/weapons/laser_pistol.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
Client/assets/textures/weapons/laser_sniper_rifle.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
Client/assets/textures/weapons/starter_blaster.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
451
Client/electron-main.js
Normal file
@ -0,0 +1,451 @@
|
||||
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const logger = require('./js/core/Logger');
|
||||
|
||||
console.log('[MAIN PROCESS] Electron main process starting...');
|
||||
console.log('[MAIN PROCESS] Node.js version:', process.version);
|
||||
console.log('[MAIN PROCESS] Electron version:', process.versions.electron);
|
||||
console.log('[MAIN PROCESS] Platform:', process.platform);
|
||||
console.log('[MAIN PROCESS] Current working directory:', process.cwd());
|
||||
|
||||
// Keep a global reference of the window object
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
console.log('[MAIN PROCESS] createWindow() called');
|
||||
|
||||
try {
|
||||
console.log('[MAIN PROCESS] Creating BrowserWindow...');
|
||||
// Create the browser window
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 832, // 800 + 32px for custom title bar
|
||||
minWidth: 1200,
|
||||
minHeight: 832,
|
||||
maxWidth: 1200,
|
||||
maxHeight: 832,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true,
|
||||
webSecurity: true
|
||||
},
|
||||
icon: path.join(__dirname, 'assets/icon.png'),
|
||||
show: false, // Don't show until ready-to-show
|
||||
title: 'Galaxy Strike Online'
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] BrowserWindow created successfully');
|
||||
console.log('[MAIN PROCESS] Loading index.html...');
|
||||
|
||||
// Load the index.html file
|
||||
mainWindow.loadFile('index.html');
|
||||
|
||||
console.log('[MAIN PROCESS] index.html loaded, setting up electronAPI...');
|
||||
|
||||
// Set up electronAPI after DOM is ready
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log('[MAIN PROCESS] DOM is ready, setting up electronAPI...');
|
||||
mainWindow.webContents.executeJavaScript(`
|
||||
console.log('[RENDERER] Setting up electronAPI...');
|
||||
window.electronAPI = {
|
||||
minimizeWindow: () => require('electron').ipcRenderer.send('minimize-window'),
|
||||
closeWindow: () => require('electron').ipcRenderer.send('close-window'),
|
||||
toggleFullscreen: () => require('electron').ipcRenderer.send('toggle-fullscreen'),
|
||||
log: (level, message, data) => require('electron').ipcRenderer.send('log-message', { level, message, data }),
|
||||
createSaveFolders: (saveSlots) => require('electron').ipcRenderer.invoke('create-save-folders', saveSlots),
|
||||
testFileAccess: (slotPath) => require('electron').ipcRenderer.invoke('test-file-access', slotPath),
|
||||
saveGame: (slot, saveData) => require('electron').ipcRenderer.invoke('save-game', slot, saveData),
|
||||
loadGame: (slot) => require('electron').ipcRenderer.invoke('load-game', slot),
|
||||
getPath: (name) => require('electron').ipcRenderer.invoke('get-path', name),
|
||||
deleteSaveFile: (slot) => require('electron').ipcRenderer.invoke('delete-save-file', slot)
|
||||
};
|
||||
console.log('[RENDERER] electronAPI setup completed');
|
||||
`).then(() => {
|
||||
console.log('[MAIN PROCESS] electronAPI setup completed');
|
||||
}).catch((error) => {
|
||||
console.error('[MAIN PROCESS] Failed to setup electronAPI:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Show window when ready
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
console.log('[MAIN PROCESS] Window ready-to-show event fired');
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// Open DevTools in development
|
||||
if (process.argv.includes('--dev')) {
|
||||
console.log('[MAIN PROCESS] Opening DevTools...');
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Handle window closed
|
||||
mainWindow.on('closed', () => {
|
||||
console.log('[MAIN PROCESS] Window closed event fired');
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Handle renderer process crashes
|
||||
mainWindow.webContents.on('render-process-gone', (event, details) => {
|
||||
console.error('[MAIN PROCESS] Renderer process crashed:', details);
|
||||
console.error('[MAIN PROCESS] Crash reason:', details.reason);
|
||||
console.error('[MAIN PROCESS] Exit code:', details.exitCode);
|
||||
});
|
||||
|
||||
// Handle renderer process unresponsive
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
console.warn('[MAIN PROCESS] Renderer process unresponsive');
|
||||
});
|
||||
|
||||
// Handle renderer process responsive again
|
||||
mainWindow.webContents.on('responsive', () => {
|
||||
console.log('[MAIN PROCESS] Renderer process responsive again');
|
||||
});
|
||||
|
||||
// Handle console messages from renderer
|
||||
mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||||
console.log(`[RENDERER CONSOLE] [${level}] ${message} (line: ${line}, source: ${sourceId})`);
|
||||
});
|
||||
|
||||
// Handle page load errors
|
||||
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
console.error('[MAIN PROCESS] Page failed to load:', errorCode, errorDescription, validatedURL);
|
||||
});
|
||||
|
||||
// Handle page load success
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
console.log('[MAIN PROCESS] Page finished loading');
|
||||
});
|
||||
|
||||
// Handle DOM ready
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log('[MAIN PROCESS] DOM is ready');
|
||||
});
|
||||
|
||||
// Handle external links
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
console.log('[MAIN PROCESS] External link requested:', url);
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] createWindow() completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Error in createWindow():', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// IPC handlers for save operations
|
||||
ipcMain.handle('create-save-folders', async (event, saveSlots) => {
|
||||
console.log('[MAIN PROCESS] create-save-folders called with saveSlots:', saveSlots);
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
console.log('[MAIN PROCESS] userDataPath:', userDataPath);
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
console.log('[MAIN PROCESS] savesDir:', savesDir);
|
||||
|
||||
// Create main saves directory
|
||||
if (!fs.existsSync(savesDir)) {
|
||||
console.log('[MAIN PROCESS] Creating saves directory:', savesDir);
|
||||
fs.mkdirSync(savesDir, { recursive: true });
|
||||
console.log('[MAIN PROCESS] Saves directory created successfully');
|
||||
} else {
|
||||
console.log('[MAIN PROCESS] Saves directory already exists');
|
||||
}
|
||||
|
||||
const paths = {
|
||||
base: savesDir,
|
||||
slots: []
|
||||
};
|
||||
|
||||
// Create save slot directories
|
||||
for (let i = 1; i <= saveSlots; i++) {
|
||||
const slotDir = path.join(savesDir, `slot${i}`);
|
||||
console.log(`[MAIN PROCESS] Checking/creating slot ${i} directory:`, slotDir);
|
||||
if (!fs.existsSync(slotDir)) {
|
||||
console.log(`[MAIN PROCESS] Creating slot ${i} directory`);
|
||||
fs.mkdirSync(slotDir, { recursive: true });
|
||||
|
||||
// Create initial save info file
|
||||
const saveInfo = {
|
||||
slot: i,
|
||||
created: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: false
|
||||
};
|
||||
|
||||
const infoPath = path.join(slotDir, 'saveinfo.json');
|
||||
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
|
||||
console.log(`[MAIN PROCESS] Created save info for slot ${i}`);
|
||||
} else {
|
||||
console.log(`[MAIN PROCESS] Slot ${i} directory already exists`);
|
||||
}
|
||||
paths.slots.push(slotDir);
|
||||
}
|
||||
|
||||
console.log('[MAIN PROCESS] Save folders created successfully, returning paths:', paths);
|
||||
return { success: true, paths };
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Failed to create save folders:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('test-file-access', async (event, slotPath) => {
|
||||
try {
|
||||
const testFile = path.join(slotPath, 'access_test.txt');
|
||||
fs.writeFileSync(testFile, 'test');
|
||||
fs.unlinkSync(testFile);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-game', async (event, slot, saveData) => {
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
|
||||
// Save game data
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
fs.writeFileSync(saveFilePath, JSON.stringify(saveData, null, 2));
|
||||
|
||||
// Update save info
|
||||
const infoPath = path.join(slotDir, 'saveinfo.json');
|
||||
const saveInfo = {
|
||||
slot: slot,
|
||||
created: new Date().toISOString(),
|
||||
lastSaved: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: true,
|
||||
playTime: saveData.gameTime || 0
|
||||
};
|
||||
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to save game:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('load-game', async (event, slot) => {
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
|
||||
if (fs.existsSync(saveFilePath)) {
|
||||
const saveContent = fs.readFileSync(saveFilePath, 'utf8');
|
||||
const saveData = JSON.parse(saveContent);
|
||||
return { success: true, data: saveData };
|
||||
} else {
|
||||
return { success: false, error: 'Save file not found' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load game:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-path', async (event, name) => {
|
||||
try {
|
||||
return app.getPath(name);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-save-file', async (event, slot) => {
|
||||
console.log('[MAIN PROCESS] delete-save-file called for slot:', slot);
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
const infoFilePath = path.join(slotDir, 'saveinfo.json');
|
||||
|
||||
console.log('[MAIN PROCESS] Attempting to delete save files from:', slotDir);
|
||||
|
||||
let deletedFiles = [];
|
||||
|
||||
// Delete save file if it exists
|
||||
if (fs.existsSync(saveFilePath)) {
|
||||
console.log('[MAIN PROCESS] Deleting save file:', saveFilePath);
|
||||
fs.unlinkSync(saveFilePath);
|
||||
deletedFiles.push('save.json');
|
||||
}
|
||||
|
||||
// Delete save info file if it exists
|
||||
if (fs.existsSync(infoFilePath)) {
|
||||
console.log('[MAIN PROCESS] Deleting save info file:', infoFilePath);
|
||||
fs.unlinkSync(infoFilePath);
|
||||
deletedFiles.push('saveinfo.json');
|
||||
}
|
||||
|
||||
// Create empty save info file to indicate slot is empty
|
||||
const saveInfo = {
|
||||
slot: slot,
|
||||
created: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: false,
|
||||
deleted: new Date().toISOString()
|
||||
};
|
||||
fs.writeFileSync(infoFilePath, JSON.stringify(saveInfo, null, 2));
|
||||
|
||||
console.log('[MAIN PROCESS] Successfully deleted save files for slot', slot, ':', deletedFiles);
|
||||
return { success: true, deletedFiles };
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Failed to delete save file:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// IPC handlers for window controls
|
||||
// Handle logging from renderer process
|
||||
ipcMain.on('log-message', async (event, { level, message, data }) => {
|
||||
try {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
await logger.error(message, data);
|
||||
break;
|
||||
case 'warn':
|
||||
await logger.warn(message, data);
|
||||
break;
|
||||
case 'info':
|
||||
await logger.info(message, data);
|
||||
break;
|
||||
case 'debug':
|
||||
await logger.debug(message, data);
|
||||
break;
|
||||
default:
|
||||
await logger.info(message, data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to log message from renderer:', error);
|
||||
// Fallback to console logging to prevent infinite loops
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('minimize-window', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-window', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-fullscreen', () => {
|
||||
if (mainWindow) {
|
||||
const isFullscreen = mainWindow.isFullScreen();
|
||||
if (isFullscreen) {
|
||||
mainWindow.setFullScreen(false);
|
||||
mainWindow.setSize(1200, 832);
|
||||
mainWindow.center();
|
||||
} else {
|
||||
mainWindow.setFullScreen(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished initialization
|
||||
app.whenReady().then(async () => {
|
||||
console.log('[MAIN PROCESS] Electron app ready, starting initialization...');
|
||||
|
||||
try {
|
||||
// Initialize logger with app data path
|
||||
console.log('[MAIN PROCESS] Initializing logger...');
|
||||
await logger.initialize(app.getPath('userData'));
|
||||
console.log('[MAIN PROCESS] Logger initialized');
|
||||
|
||||
await logger.info('Galaxy Strike Online application starting');
|
||||
console.log('[MAIN PROCESS] Logger info message sent');
|
||||
|
||||
console.log('[MAIN PROCESS] Creating main window...');
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
console.log('[MAIN PROCESS] Activate event fired');
|
||||
// On macOS it's common to re-create a window in the app when the dock icon is clicked
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
console.log('[MAIN PROCESS] No windows exist, creating new window');
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] App initialization completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Error during app initialization:', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('[MAIN PROCESS] Error in app.whenReady():', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
});
|
||||
|
||||
// Quit when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it's common for applications and their menu bar to stay active
|
||||
if (process.platform !== 'darwin') {
|
||||
logger.info('Application shutting down');
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('[MAIN PROCESS] Uncaught Exception:', error);
|
||||
console.error('[MAIN PROCESS] Uncaught Exception stack:', error.stack);
|
||||
|
||||
try {
|
||||
if (logger && typeof logger.errorEvent === 'function') {
|
||||
await logger.errorEvent(error, 'Uncaught Exception in Main Process');
|
||||
}
|
||||
} catch (logError) {
|
||||
console.error('[MAIN PROCESS] Failed to log uncaught exception:', logError);
|
||||
}
|
||||
|
||||
console.error('[MAIN PROCESS] Application will continue running despite uncaught exception');
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[MAIN PROCESS] Unhandled Promise Rejection at:', promise, 'reason:', reason);
|
||||
console.error('[MAIN PROCESS] Rejection reason stack:', reason.stack);
|
||||
});
|
||||
|
||||
// Handle unhandled rejections
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
// Avoid logging the logging system's own errors to prevent infinite loops
|
||||
if (reason && reason.message && reason.message.includes('object could not be cloned')) {
|
||||
console.warn('IPC cloning error detected - this is expected during logger initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
await logger.error('Unhandled Rejection', { reason: reason.toString(), promise: promise.toString() });
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// Security: Prevent new window creation
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('new-window', (event, navigationUrl) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(navigationUrl);
|
||||
});
|
||||
});
|
||||
696
Client/index.html
Normal file
@ -0,0 +1,696 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Galaxy Strike Online - Space Idle MMORPG</title>
|
||||
<link rel="stylesheet" href="styles/main.css?v=2">
|
||||
<link rel="stylesheet" href="styles/components.css">
|
||||
<link rel="stylesheet" href="styles/tables.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||
<script src="js/SimpleLocalServer.js"></script>
|
||||
<script src="js/LocalServerManager.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Custom Title Bar -->
|
||||
<div id="titleBar" class="title-bar">
|
||||
<div class="title-bar-left">
|
||||
<span class="title-bar-title">Galaxy Strike Online</span>
|
||||
</div>
|
||||
<div class="title-bar-right">
|
||||
<button class="title-bar-btn" id="minimizeBtn" title="Minimize">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<button class="title-bar-btn" id="fullscreenBtn" title="Toggle Fullscreen">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
<button class="title-bar-btn close-btn" id="closeBtn" title="Close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<!-- Main Menu -->
|
||||
<div id="mainMenu" class="main-menu">
|
||||
<div class="menu-container">
|
||||
<div class="menu-header">
|
||||
<h1 class="menu-title">GALAXY STRIKE ONLINE</h1>
|
||||
<p class="menu-subtitle">Space Idle MMORPG</p>
|
||||
</div>
|
||||
|
||||
<div class="menu-content">
|
||||
<!-- Login Section -->
|
||||
<div id="loginSection" class="menu-section">
|
||||
<h2 class="section-title">Account Access</h2>
|
||||
<div class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="emailInput">Email</label>
|
||||
<input type="email" id="emailInput" placeholder="Enter your email" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="passwordInput">Password</label>
|
||||
<input type="password" id="passwordInput" placeholder="Enter your password" class="form-input">
|
||||
</div>
|
||||
<div class="login-options">
|
||||
<button class="btn btn-primary btn-large" id="loginBtn">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Login
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-large" id="registerBtn">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-notice" id="loginNotice">
|
||||
<p><i class="fas fa-info-circle"></i> Connect to the live server to play</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Browser Section -->
|
||||
<div id="serverSection" class="menu-section hidden">
|
||||
<h2 class="section-title">Server Browser</h2>
|
||||
<div class="server-controls">
|
||||
<button class="btn btn-primary" id="createServerBtn">
|
||||
<i class="fas fa-server"></i>
|
||||
Start Local Server
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="refreshServersBtn">
|
||||
<i class="fas fa-sync"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<div class="server-filters">
|
||||
<select id="regionFilter" class="filter-select">
|
||||
<option value="">All Regions</option>
|
||||
<option value="us-east">US East</option>
|
||||
<option value="us-west">US West</option>
|
||||
<option value="europe">Europe</option>
|
||||
<option value="asia">Asia</option>
|
||||
</select>
|
||||
<select id="typeFilter" class="filter-select">
|
||||
<option value="">All Types</option>
|
||||
<option value="public">Public</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-list" id="serverList">
|
||||
<div class="server-loading" id="serverLoading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Loading servers...</p>
|
||||
</div>
|
||||
<div class="server-empty hidden" id="serverEmpty">
|
||||
<i class="fas fa-server"></i>
|
||||
<p>No servers found. Create your own server to play!</p>
|
||||
</div>
|
||||
<!-- Servers will be populated here -->
|
||||
</div>
|
||||
<div class="server-actions">
|
||||
<button class="btn btn-secondary" id="backToLoginBtn">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Join Confirmation Section -->
|
||||
<div id="serverConfirmSection" class="menu-section hidden">
|
||||
<h2 class="section-title">Server Selected</h2>
|
||||
<div class="server-confirmation">
|
||||
<div class="confirm-actions-left">
|
||||
<button class="btn btn-primary btn-large btn-join-server" id="joinServerBtn">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Join Server
|
||||
</button>
|
||||
</div>
|
||||
<div class="selected-server-info-center">
|
||||
<div class="server-preview">
|
||||
<h3 id="selectedServerName">Server Name</h3>
|
||||
<div class="server-details" id="selectedServerDetails">
|
||||
<p class="server-info">Type: <span id="selectedServerType">Public</span></p>
|
||||
<p class="server-info">Region: <span id="selectedServerRegion">US East</span></p>
|
||||
<p class="server-info">Players: <span id="selectedServerPlayers">0/10</span></p>
|
||||
<p class="server-info">Owner: <span id="selectedServerOwner">Unknown</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="confirm-actions-right">
|
||||
<button class="btn btn-info btn-large" id="serverInfoBtn">
|
||||
<i class="fas fa-info"></i>
|
||||
More Info
|
||||
</button>
|
||||
<button class="btn btn-warning btn-large" id="backToServersBtn">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Options Section -->
|
||||
<div id="optionsSection" class="menu-section hidden">
|
||||
<h2 class="section-title">Game Options</h2>
|
||||
<div class="options-grid" style="display: flex !important; justify-content: space-between !important; gap: 30px !important; margin-bottom: 30px !important;">
|
||||
<div class="options-left" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: flex-end !important; min-width: 200px !important;">
|
||||
<button class="btn btn-primary btn-large" id="continueBtn" style="width: 200px !important;">
|
||||
<i class="fas fa-gamepad"></i>
|
||||
Continue
|
||||
</button>
|
||||
<button class="btn btn-primary btn-large" id="newGameBtn" style="width: 200px !important;">
|
||||
<i class="fas fa-play"></i>
|
||||
New Game
|
||||
</button>
|
||||
</div>
|
||||
<div class="options-center" style="display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; flex: 1 !important; min-width: 300px !important;">
|
||||
<div class="save-info-display" style="background: rgba(0, 212, 255, 0.1) !important; border: 2px solid rgba(0, 212, 255, 0.3) !important; border-radius: 10px !important; padding: 20px !important; text-align: center !important; width: 100% !important; max-width: 400px !important;">
|
||||
<h3 style="color: #00d4ff !important; margin-bottom: 15px !important; font-size: 1.2em !important;">Save Information</h3>
|
||||
<div id="saveInfoDetails" style="color: #ffffff !important; font-size: 0.9em !important; line-height: 1.4 !important;">
|
||||
<p>Select a save slot to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-right" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: space-between !important; min-width: 200px !important;">
|
||||
<button class="btn btn-info btn-large" id="settingsBtn" style="width: 200px !important;">
|
||||
<i class="fas fa-cog"></i>
|
||||
Settings
|
||||
</button>
|
||||
<button class="btn btn-warning btn-large" id="deleteSaveBtn" style="width: 200px !important;">
|
||||
<i class="fas fa-trash"></i>
|
||||
Delete Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-secondary" id="backToSavesBtn">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Saves
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-footer">
|
||||
<p class="version-text">Version 1.0.0</p>
|
||||
<div class="footer-links">
|
||||
<button class="link-btn" id="aboutBtn">About</button>
|
||||
<button class="link-btn" id="helpBtn">Help</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Screen -->
|
||||
<div id="loadingScreen" class="loading-screen hidden">
|
||||
<div class="loading-content">
|
||||
<h1 class="game-title">GALAXY STRIKE ONLINE</h1>
|
||||
<div class="loading-bar">
|
||||
<div class="loading-progress"></div>
|
||||
</div>
|
||||
<p class="loading-text">Initializing Universe...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Game Interface -->
|
||||
<div id="gameInterface" class="game-interface hidden">
|
||||
<!-- Header -->
|
||||
<header class="game-header">
|
||||
<div class="header-left">
|
||||
<h1 class="logo">GSO</h1>
|
||||
<div class="player-info">
|
||||
<span class="player-name" id="playerName">Commander</span>
|
||||
<span class="player-level" id="playerLevel">Lv. 1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="resources">
|
||||
<div class="resource">
|
||||
<i class="fas fa-coins"></i>
|
||||
<span id="credits">1,000</span>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<i class="fas fa-gem"></i>
|
||||
<span id="gems">10</span>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<i class="fas fa-bolt"></i>
|
||||
<span id="energy">100/100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-secondary" id="settingsBtn">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="discordBtn">
|
||||
<i class="fab fa-discord"></i>
|
||||
</button>
|
||||
<button class="btn btn-info" id="localServerBtn" title="Local Server">
|
||||
<i class="fas fa-server"></i>
|
||||
</button>
|
||||
<!-- <button class="btn btn-primary" id="saveBtn" title="Save Game">
|
||||
<i class="fas fa-save"></i>
|
||||
</button> -->
|
||||
<button class="btn btn-warning" id="returnToMenuBtn">
|
||||
<i class="fas fa-home"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="main-nav">
|
||||
<button class="nav-btn active" data-tab="dashboard">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>Dashboard</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="dungeons">
|
||||
<i class="fas fa-dungeon"></i>
|
||||
<span>Dungeons</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="skills">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="base">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>Base</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="quests">
|
||||
<i class="fas fa-scroll"></i>
|
||||
<span>Quests</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="inventory">
|
||||
<i class="fas fa-backpack"></i>
|
||||
<span>Inventory</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="crafting">
|
||||
<i class="fas fa-hammer"></i>
|
||||
<span>Crafting</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="shop">
|
||||
<i class="fas fa-store"></i>
|
||||
<span>Shop</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content">
|
||||
<!-- Dashboard Tab -->
|
||||
<div class="tab-content active" id="dashboard-tab">
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<h3>Fleet Status</h3>
|
||||
<div class="fleet-info">
|
||||
<div class="ship-status">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<div>
|
||||
<p>Flagship: <span id="flagshipName">Starter Cruiser</span></p>
|
||||
<p>Health: <span id="shipHealth">100%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Idle Progress</h3>
|
||||
<div class="idle-stats">
|
||||
<p>Offline Time: <span id="offlineTime">0h 0m</span></p>
|
||||
<p>Resources Gained: <span id="offlineResources">0</span></p>
|
||||
<button class="btn btn-primary" id="claimOfflineBtn">Claim Rewards</button>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Total Kills</span>
|
||||
<span class="stat-value" id="totalKills">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Dungeons Cleared</span>
|
||||
<span class="stat-value" id="dungeonsCleared">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Play Time</span>
|
||||
<span class="stat-value" id="playTime">0h 0m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dungeons Tab -->
|
||||
<div class="tab-content" id="dungeons-tab">
|
||||
<div class="dungeons-container">
|
||||
<div class="dungeon-selector">
|
||||
<h2>Select Dungeon</h2>
|
||||
<div class="dungeon-list" id="dungeonList">
|
||||
<!-- Dungeons will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="dungeon-view" id="dungeonView">
|
||||
<div class="dungeon-placeholder">
|
||||
<i class="fas fa-dungeon"></i>
|
||||
<p>Select a dungeon to begin your adventure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Tab -->
|
||||
<div class="tab-content" id="skills-tab">
|
||||
<div class="skills-container">
|
||||
<div class="skills-header">
|
||||
<h2><i class="fas fa-graduation-cap"></i> Skills</h2>
|
||||
<div class="skill-points-display">
|
||||
<span class="skill-points">Skill Points: 0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-categories">
|
||||
<button class="skill-cat-btn active" data-category="combat">Combat</button>
|
||||
<button class="skill-cat-btn" data-category="science">Science</button>
|
||||
<button class="skill-cat-btn" data-category="crafting">Crafting</button>
|
||||
</div>
|
||||
<div class="skills-grid" id="skillsGrid">
|
||||
<!-- Skills will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Tab -->
|
||||
<div class="tab-content" id="base-tab">
|
||||
<div class="base-navigation">
|
||||
<button class="base-nav-btn active" data-view="overview">Base Overview</button>
|
||||
<button class="base-nav-btn" data-view="visualization">Base Visualization</button>
|
||||
<button class="base-nav-btn" data-view="ships">Ship Gallery</button>
|
||||
<button class="base-nav-btn" data-view="starbases">Starbases</button>
|
||||
</div>
|
||||
|
||||
<!-- Base Overview -->
|
||||
<div class="base-view-content" id="base-overview">
|
||||
<div class="base-container">
|
||||
<div class="base-view">
|
||||
<div class="base-info">
|
||||
<h3>Base Information</h3>
|
||||
<div class="base-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Power Usage:</span>
|
||||
<span class="stat-value" id="basePowerUsage">0/100</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Storage:</span>
|
||||
<span class="stat-value" id="baseStorage">1000</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Production Rate:</span>
|
||||
<span class="stat-value" id="baseProduction">0/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-rooms" id="baseRooms">
|
||||
<!-- Base rooms will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-upgrades">
|
||||
<h3>Base Upgrades</h3>
|
||||
<div class="upgrade-list" id="baseUpgrades">
|
||||
<!-- Upgrades will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base Visualization -->
|
||||
<div class="base-view-content hidden" id="base-visualization">
|
||||
<div class="base-visualization-container">
|
||||
<canvas id="baseCanvas" width="800" height="600"></canvas>
|
||||
<div class="base-info-overlay">
|
||||
<div class="base-stats-overlay">
|
||||
<h3>Base Information</h3>
|
||||
<div id="baseInfoDisplay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Gallery -->
|
||||
<div class="base-view-content hidden" id="base-ships">
|
||||
<div class="ship-gallery-container">
|
||||
<h3>Your Ships</h3>
|
||||
|
||||
<div class="ship-layout">
|
||||
<!-- Current Ship Section -->
|
||||
<div class="current-ship-section">
|
||||
<h4>Current Ship</h4>
|
||||
<div class="current-ship-display" id="currentShipDisplay" data-debug-id="current-ship-panel">
|
||||
<div class="current-ship-info">
|
||||
<div class="current-ship-image">
|
||||
<img src="assets/textures/ships/starter_cruiser.png" alt="Current Ship" id="currentShipImage">
|
||||
</div>
|
||||
<div class="current-ship-details">
|
||||
<h5 id="currentShipName">Starter Cruiser</h5>
|
||||
<div class="current-ship-stats">
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Class:</span>
|
||||
<span class="stat-value" id="currentShipClass">Light</span>
|
||||
</div>
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Level:</span>
|
||||
<span class="stat-value" id="currentShipLevel">1</span>
|
||||
</div>
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Health:</span>
|
||||
<span class="stat-value" id="currentShipHealth">100/100</span>
|
||||
</div>
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Attack:</span>
|
||||
<span class="stat-value" id="currentShipAttack">10</span>
|
||||
</div>
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Defense:</span>
|
||||
<span class="stat-value" id="currentShipDefense">5</span>
|
||||
</div>
|
||||
<div class="ship-stat">
|
||||
<span class="stat-label">Speed:</span>
|
||||
<span class="stat-value" id="currentShipSpeed">15</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Grid -->
|
||||
<div class="ship-grid-section">
|
||||
<h4>Ships Collected</h4>
|
||||
<div class="ship-grid" id="shipGrid">
|
||||
<!-- Ships will be displayed here as cards -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Starbases -->
|
||||
<div class="base-view-content hidden" id="base-starbases">
|
||||
<div class="starbases-container">
|
||||
<div class="starbase-section">
|
||||
<h3>Starbase Management</h3>
|
||||
<div class="starbase-list" id="starbaseList">
|
||||
<!-- Starbases will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="starbase-section">
|
||||
<h3>Available Starbases</h3>
|
||||
<div class="starbase-shop" id="starbasePurchaseList">
|
||||
<div class="starbase-purchase-list" id="starbasePurchaseItems">
|
||||
<!-- Available starbases for purchase -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quests Tab -->
|
||||
<div class="tab-content" id="quests-tab">
|
||||
<div class="quests-container">
|
||||
<div class="quest-tabs">
|
||||
<button class="quest-tab-btn active" data-type="main">Main Story</button>
|
||||
<button class="quest-tab-btn" data-type="daily">Daily</button>
|
||||
<button class="quest-tab-btn" data-type="weekly">Weekly</button>
|
||||
<button class="quest-tab-btn" data-type="completed">Completed</button>
|
||||
<button class="quest-tab-btn" data-type="failed">Failed Quests</button>
|
||||
</div>
|
||||
<div class="daily-countdown" id="dailyCountdown">Daily quests reset in: 00:00:00</div>
|
||||
<div class="weekly-countdown" id="weeklyCountdown">Weekly quests reset in: 0d 00:00</div>
|
||||
<div class="quest-list" id="questList">
|
||||
<!-- Quests will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory Tab -->
|
||||
<div class="tab-content" id="inventory-tab">
|
||||
<div class="inventory-container">
|
||||
<div class="equipment-section">
|
||||
<h3>Equipment</h3>
|
||||
<div class="equipment-slots">
|
||||
<div class="equipment-slot">
|
||||
<div class="slot-label">Weapon</div>
|
||||
<div class="slot-container" id="equip-weapon">
|
||||
<div class="empty-equip-slot">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equipment-slot">
|
||||
<div class="slot-label">Armor</div>
|
||||
<div class="slot-container" id="equip-armor">
|
||||
<div class="empty-equip-slot">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equipment-slot">
|
||||
<div class="slot-label">Engine</div>
|
||||
<div class="slot-container" id="equip-engine">
|
||||
<div class="empty-equip-slot">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equipment-slot">
|
||||
<div class="slot-label">Shield</div>
|
||||
<div class="slot-container" id="equip-shield">
|
||||
<div class="empty-equip-slot">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equipment-slot">
|
||||
<div class="slot-label">Accessory</div>
|
||||
<div class="slot-container" id="equip-accessory">
|
||||
<div class="empty-equip-slot">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inventory-main">
|
||||
<div class="inventory-section">
|
||||
<h3>Inventory</h3>
|
||||
<div class="inventory-grid" id="inventoryGrid">
|
||||
<!-- Inventory items will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-details" id="itemDetails">
|
||||
<p>Select an item to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crafting Tab -->
|
||||
<div class="tab-content" id="crafting-tab">
|
||||
<div class="crafting-container">
|
||||
<div class="crafting-header">
|
||||
<h2><i class="fas fa-hammer"></i> Crafting Station</h2>
|
||||
<div class="crafting-info">
|
||||
<div class="crafting-level">
|
||||
<i class="fas fa-level-up-alt"></i>
|
||||
<span>Crafting Level: </span>
|
||||
<span id="craftingLevel">1</span>
|
||||
</div>
|
||||
<div class="crafting-experience">
|
||||
<i class="fas fa-star"></i>
|
||||
<span>Experience: </span>
|
||||
<span id="craftingExp">0/100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crafting-categories">
|
||||
<button class="crafting-cat-btn active" data-category="weapons">Weapons</button>
|
||||
<button class="crafting-cat-btn" data-category="armor">Armor</button>
|
||||
<button class="crafting-cat-btn" data-category="items">Items</button>
|
||||
<button class="crafting-cat-btn" data-category="ships">Ships</button>
|
||||
</div>
|
||||
<div class="crafting-grid" id="recipeList">
|
||||
<!-- Recipes will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shop Tab -->
|
||||
<div class="tab-content" id="shop-tab">
|
||||
<div class="shop-container">
|
||||
<div class="shop-header">
|
||||
<div class="shop-categories">
|
||||
<button class="shop-cat-btn active" data-category="ships">Ships</button>
|
||||
<button class="shop-cat-btn" data-category="weapons">Weapons</button>
|
||||
<button class="shop-cat-btn" data-category="armors">Armors</button>
|
||||
<!-- <button class="shop-cat-btn" data-category="upgrades">Upgrades</button> -->
|
||||
<button class="shop-cat-btn" data-category="cosmetics">Cosmetics</button>
|
||||
<button class="shop-cat-btn" data-category="consumables">Consumables</button>
|
||||
<button class="shop-cat-btn" data-category="materials">Materials</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-content">
|
||||
<div class="shop-items-container">
|
||||
<div class="shop-items" id="shopItems">
|
||||
<!-- Shop items will be generated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<div class="modal-overlay hidden" id="modalOverlay">
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Modal Title</h3>
|
||||
<button class="modal-close" id="modalClose">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Modal content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Progress Indicator -->
|
||||
<div class="loading-indicator" id="loadingIndicator"></div>
|
||||
<div class="loading-status hidden" id="loadingStatus">Initializing...</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="../config/xp-progression.js"></script>
|
||||
<script src="js/core/DebugLogger.js"></script>
|
||||
<script src="js/core/Logger.js"></script>
|
||||
<script src="js/core/TextureManager.js"></script>
|
||||
<script src="js/core/GameEngine.js"></script>
|
||||
<script src="js/core/Player.js"></script>
|
||||
<script src="js/core/Inventory.js"></script>
|
||||
<script src="js/core/Economy.js"></script>
|
||||
<script src="js/systems/DungeonSystem.js"></script>
|
||||
<script src="js/systems/SkillSystem.js"></script>
|
||||
<script src="js/systems/BaseSystem.js"></script>
|
||||
<script src="js/systems/QuestSystem.js"></script>
|
||||
<script src="js/systems/ShipSystem.js"></script>
|
||||
<script src="js/systems/IdleSystem.js"></script>
|
||||
<script src="js/systems/CraftingSystem.js"></script>
|
||||
<script src="js/data/GameData.js"></script>
|
||||
<script src="js/ui/UIManager.js"></script>
|
||||
<script src="js/GameInitializer.js"></script>
|
||||
<script src="js/ui/LiveMainMenu.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<!-- Hidden Console Window -->
|
||||
<div id="consoleWindow" class="console-window">
|
||||
<div class="console-header">
|
||||
<span>Developer Console</span>
|
||||
<button class="console-close" onclick="toggleConsole()">×</button>
|
||||
</div>
|
||||
<div class="console-content">
|
||||
<div id="consoleOutput" class="console-output"></div>
|
||||
<div class="console-input-container">
|
||||
<input type="text" id="consoleInput" class="console-input" placeholder="Type command here..." onkeypress="handleConsoleInput(event)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
365
Client/js/GameInitializer.js
Normal file
@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Game Initializer
|
||||
* Handles initialization of multiplayer game modes
|
||||
*/
|
||||
|
||||
console.log('[GAME INITIALIZER] GameInitializer.js script loaded');
|
||||
|
||||
class GameInitializer {
|
||||
constructor() {
|
||||
console.log('[GAME INITIALIZER] Constructor called');
|
||||
this.gameMode = 'multiplayer';
|
||||
this.serverData = null;
|
||||
this.authToken = null;
|
||||
this.currentUser = null;
|
||||
this.socket = null;
|
||||
this.apiBaseUrl = 'https://api.korvarix.com/api'; // API Server
|
||||
this.gameServerUrl = 'https://api.korvarix.com'; // Game Server for Socket.IO
|
||||
}
|
||||
|
||||
updateServerUrls(apiUrl, gameUrl) {
|
||||
console.log('[GAME INITIALIZER] Updating server URLs:', { apiUrl, gameUrl });
|
||||
this.apiBaseUrl = apiUrl;
|
||||
this.gameServerUrl = gameUrl;
|
||||
}
|
||||
|
||||
initializeMultiplayer(server, serverData, authToken, currentUser) {
|
||||
console.log('[GAME INITIALIZER] Initializing multiplayer game mode');
|
||||
this.gameMode = 'multiplayer';
|
||||
this.serverData = { ...server, ...serverData };
|
||||
this.authToken = authToken;
|
||||
this.currentUser = currentUser;
|
||||
|
||||
// Initialize Socket.IO connection
|
||||
this.initializeSocketConnection();
|
||||
|
||||
// Initialize game systems with multiplayer support
|
||||
this.initializeGameSystems();
|
||||
|
||||
// Update UI for multiplayer mode
|
||||
this.updateUIForMultiplayerMode();
|
||||
|
||||
console.log('[GAME INITIALIZER] Multiplayer game initialized');
|
||||
}
|
||||
|
||||
initializeSocketConnection() {
|
||||
if (!this.serverData) {
|
||||
console.error('[GAME INITIALIZER] No server data for socket connection');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[GAME INITIALIZER] Initializing Socket.IO connection');
|
||||
|
||||
// Check if we're in local mode and should use mock socket
|
||||
if (this.gameServerUrl.includes('localhost') && window.localServerManager && window.localServerManager.localServer) {
|
||||
console.log('[GAME INITIALIZER] Using mock socket for local mode');
|
||||
this.socket = window.localServerManager.localServer.createMockSocket();
|
||||
|
||||
// Trigger connected event immediately since mock socket auto-connects
|
||||
setTimeout(() => {
|
||||
this.onSocketConnected();
|
||||
}, 200);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to the game server (different from API server)
|
||||
this.socket = io(this.gameServerUrl, {
|
||||
auth: {
|
||||
token: this.authToken,
|
||||
serverId: this.serverData.id
|
||||
}
|
||||
});
|
||||
|
||||
// Socket event handlers
|
||||
this.socket.on('connect', () => {
|
||||
console.log('[GAME INITIALIZER] Connected to server');
|
||||
this.onSocketConnected();
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('[GAME INITIALIZER] Disconnected from server');
|
||||
this.onSocketDisconnected();
|
||||
});
|
||||
|
||||
this.socket.on('error', (error) => {
|
||||
console.error('[GAME INITIALIZER] Socket error:', error);
|
||||
});
|
||||
|
||||
this.socket.on('force_disconnect', (data) => {
|
||||
console.warn('[GAME INITIALIZER] Force disconnected:', data);
|
||||
this.onForceDisconnect(data);
|
||||
});
|
||||
|
||||
// Game-specific events
|
||||
this.socket.on('playerJoined', (data) => {
|
||||
console.log('[GAME INITIALIZER] Player joined:', data);
|
||||
this.onPlayerJoined(data);
|
||||
});
|
||||
|
||||
this.socket.on('playerLeft', (data) => {
|
||||
console.log('[GAME INITIALIZER] Player left:', data);
|
||||
this.onPlayerLeft(data);
|
||||
});
|
||||
|
||||
this.socket.on('gameUpdate', (data) => {
|
||||
console.log('[GAME INITIALIZER] Game update:', data);
|
||||
this.onGameUpdate(data);
|
||||
});
|
||||
|
||||
this.socket.on('chatMessage', (data) => {
|
||||
console.log('[GAME INITIALIZER] Chat message:', data);
|
||||
this.onChatMessage(data);
|
||||
});
|
||||
}
|
||||
|
||||
onSocketConnected() {
|
||||
// Join the server room
|
||||
this.socket.emit('joinServer', {
|
||||
serverId: this.serverData.id,
|
||||
userId: this.currentUser.userId,
|
||||
username: this.currentUser.username
|
||||
});
|
||||
|
||||
// Show connected status
|
||||
this.showConnectionStatus('Connected', 'success');
|
||||
}
|
||||
|
||||
onSocketDisconnected() {
|
||||
// Show disconnected status
|
||||
this.showConnectionStatus('Disconnected', 'error');
|
||||
}
|
||||
|
||||
onPlayerJoined(data) {
|
||||
// Handle player joining
|
||||
this.updatePlayerList();
|
||||
this.showNotification(`${data.username} joined the server`, 'info');
|
||||
}
|
||||
|
||||
onPlayerLeft(data) {
|
||||
// Handle player leaving
|
||||
this.updatePlayerList();
|
||||
this.showNotification(`${data.username} left the server`, 'info');
|
||||
}
|
||||
|
||||
onGameUpdate(data) {
|
||||
// Handle game state updates
|
||||
if (window.game && window.game.handleServerUpdate) {
|
||||
window.game.handleServerUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
onChatMessage(data) {
|
||||
// Handle chat messages
|
||||
if (window.game && window.game.handleChatMessage) {
|
||||
window.game.handleChatMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
onForceDisconnect(data) {
|
||||
// Handle forced disconnection from server
|
||||
console.warn('[GAME INITIALIZER] Force disconnected by server:', data);
|
||||
|
||||
// Show notification to user
|
||||
if (window.game && window.game.showNotification) {
|
||||
window.game.showNotification(
|
||||
`Disconnected: ${data.reason}`,
|
||||
'warning',
|
||||
10000
|
||||
);
|
||||
}
|
||||
|
||||
// Disconnect the socket
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
// Clean up multiplayer mode
|
||||
if (window.game) {
|
||||
window.game.setMultiplayerMode(false);
|
||||
}
|
||||
|
||||
// Return to main menu after a delay
|
||||
setTimeout(() => {
|
||||
if (window.liveMainMenu) {
|
||||
window.liveMainMenu.showLoginSection();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
initializeGameSystems() {
|
||||
console.log('[GAME INITIALIZER] Initializing game systems');
|
||||
|
||||
// Wait for the main game script to be ready
|
||||
if (typeof window.game !== 'undefined') {
|
||||
console.log('[GAME INITIALIZER] window.game is available, calling setupGameSystems');
|
||||
this.setupGameSystems();
|
||||
} else {
|
||||
console.log('[GAME INITIALIZER] window.game not available, waiting 100ms');
|
||||
// Wait for the game to be initialized
|
||||
setTimeout(() => this.initializeGameSystems(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
setupGameSystems() {
|
||||
if (!window.game) {
|
||||
console.error('[GAME INITIALIZER] Game not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[GAME INITIALIZER] Setting up game systems for multiplayer mode');
|
||||
|
||||
// Configure game for multiplayer mode
|
||||
console.log('[GAME INITIALIZER] Configuring for multiplayer mode');
|
||||
window.game.setMultiplayerMode(true, this.socket, this.serverData, this.currentUser);
|
||||
|
||||
// Game is already set up with save data, just start the game loop
|
||||
if (window.game.start) {
|
||||
// console.log('[GAME INITIALIZER] Calling start() to begin game loop');
|
||||
window.game.start();
|
||||
} else if (window.game.startGame) {
|
||||
// console.log('[GAME INITIALIZER] Calling startGame(false) - save data already applied');
|
||||
window.game.startGame(false); // false = don't load again (save data already applied)
|
||||
} else {
|
||||
console.error('[GAME INITIALIZER] No start method available on window.game');
|
||||
}
|
||||
|
||||
console.log('[GAME INITIALIZER] Game systems configured');
|
||||
}
|
||||
|
||||
updateUIForMultiplayerMode() {
|
||||
// Update UI elements to show multiplayer mode
|
||||
const playerName = document.getElementById('playerName');
|
||||
if (playerName && this.currentUser) {
|
||||
playerName.textContent = this.currentUser.username;
|
||||
}
|
||||
|
||||
// Show multiplayer-specific UI elements
|
||||
this.showMultiplayerUI();
|
||||
|
||||
// Show server info
|
||||
this.showServerInfo();
|
||||
}
|
||||
|
||||
hideMultiplayerUI() {
|
||||
// Hide elements that are only relevant in multiplayer
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
if (chatContainer) {
|
||||
chatContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
const playerList = document.getElementById('playerList');
|
||||
if (playerList) {
|
||||
playerList.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
showMultiplayerUI() {
|
||||
// Show multiplayer-specific elements
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
if (chatContainer) {
|
||||
chatContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const playerList = document.getElementById('playerList');
|
||||
if (playerList) {
|
||||
playerList.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
showServerInfo() {
|
||||
// Add server information to the UI
|
||||
const header = document.querySelector('.game-header');
|
||||
if (header && !header.querySelector('.server-info')) {
|
||||
const serverInfo = document.createElement('div');
|
||||
serverInfo.className = 'server-info';
|
||||
serverInfo.innerHTML = `
|
||||
<i class="fas fa-server"></i>
|
||||
<span>${this.serverData.name} (${this.serverData.currentPlayers}/${this.serverData.maxPlayers})</span>
|
||||
`;
|
||||
serverInfo.style.cssText = `
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
color: #00d4ff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
header.appendChild(serverInfo);
|
||||
}
|
||||
}
|
||||
|
||||
showConnectionStatus(status, type) {
|
||||
// Show connection status in the UI
|
||||
const statusElement = document.getElementById('connectionStatus');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = status;
|
||||
statusElement.className = `connection-status ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayerList() {
|
||||
// Update the player list UI
|
||||
if (this.socket && this.serverData) {
|
||||
// Request updated player list from server
|
||||
this.socket.emit('getPlayerList', { serverId: this.serverData.id });
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Show a notification to the user
|
||||
if (window.game && window.game.showNotification) {
|
||||
window.game.showNotification(message, type, 3000);
|
||||
} else {
|
||||
// Fallback to alert
|
||||
console.log(`[GAME INITIALIZER] Notification: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to send actions to the server
|
||||
sendGameAction(actionType, actionData) {
|
||||
if (this.socket && this.gameMode === 'multiplayer') {
|
||||
this.socket.emit('gameAction', {
|
||||
type: actionType,
|
||||
data: actionData,
|
||||
userId: this.currentUser.userId,
|
||||
serverId: this.serverData.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method to send chat messages
|
||||
sendChatMessage(message) {
|
||||
if (this.socket && this.gameMode === 'multiplayer') {
|
||||
this.socket.emit('chatMessage', {
|
||||
message: message,
|
||||
userId: this.currentUser.userId,
|
||||
username: this.currentUser.username,
|
||||
serverId: this.serverData.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup method
|
||||
cleanup() {
|
||||
console.log('[GAME INITIALIZER] Cleaning up');
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
this.gameMode = null;
|
||||
this.serverData = null;
|
||||
this.authToken = null;
|
||||
this.currentUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.gameInitializer = new GameInitializer();
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = GameInitializer;
|
||||
}
|
||||
418
Client/js/LocalServer.js
Normal file
@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Local Server for Singleplayer Mode
|
||||
* A simplified server that runs within the Electron client for offline/singleplayer functionality
|
||||
* NOTE: This version requires express, socket.io, and cors dependencies to be installed
|
||||
*/
|
||||
|
||||
class LocalServer {
|
||||
constructor() {
|
||||
this.app = null;
|
||||
this.server = null;
|
||||
this.io = null;
|
||||
this.port = null;
|
||||
this.isRunning = false;
|
||||
this.connectedClients = new Map();
|
||||
|
||||
console.log('[LOCAL SERVER] LocalServer initialized');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Try to require dependencies
|
||||
if (typeof require !== 'undefined') {
|
||||
const express = require('express');
|
||||
const { createServer } = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const cors = require('cors');
|
||||
|
||||
this.setupExpress(express, cors);
|
||||
this.createServer = createServer;
|
||||
this.ServerClass = Server;
|
||||
|
||||
console.log('[LOCAL SERVER] Dependencies loaded successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[LOCAL SERVER] require() not available, running in browser context');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER] Failed to load dependencies:', error.message);
|
||||
console.log('[LOCAL SERVER] Please install dependencies: npm install express socket.io cors');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setupExpress(express, cors) {
|
||||
// Initialize Express app
|
||||
this.app = express();
|
||||
|
||||
// Middleware
|
||||
this.app.use(cors({
|
||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
||||
credentials: true
|
||||
}));
|
||||
this.app.use(express.json({ limit: '10mb' }));
|
||||
this.app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
mode: 'local'
|
||||
});
|
||||
});
|
||||
|
||||
// API version endpoint
|
||||
this.app.get('/api/ssc/version', (req, res) => {
|
||||
res.status(200).json({
|
||||
version: '1.0.0',
|
||||
service: 'galaxystrikeonline-local-server',
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: 'local'
|
||||
});
|
||||
});
|
||||
|
||||
// Mock authentication endpoints for singleplayer
|
||||
this.app.post('/api/auth/login', (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Auto-authenticate for singleplayer mode
|
||||
const mockUser = {
|
||||
id: 'local-user',
|
||||
email: email || 'local@player.com',
|
||||
username: 'Local Player',
|
||||
token: 'local-token-' + Date.now(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
user: mockUser,
|
||||
token: mockUser.token,
|
||||
message: 'Logged in to local mode'
|
||||
});
|
||||
});
|
||||
|
||||
this.app.post('/api/auth/register', (req, res) => {
|
||||
const { email, password, username } = req.body;
|
||||
|
||||
// Auto-register for singleplayer mode
|
||||
const mockUser = {
|
||||
id: 'local-user',
|
||||
email: email || 'local@player.com',
|
||||
username: username || 'Local Player',
|
||||
token: 'local-token-' + Date.now(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
user: mockUser,
|
||||
token: mockUser.token,
|
||||
message: 'Registered in local mode'
|
||||
});
|
||||
});
|
||||
|
||||
// Mock server browser endpoints
|
||||
this.app.get('/api/servers', (req, res) => {
|
||||
// Return a single local server
|
||||
const localServer = {
|
||||
id: 'local-server',
|
||||
name: 'Local Singleplayer',
|
||||
description: 'Your personal local server for singleplayer gaming',
|
||||
type: 'private',
|
||||
region: 'local',
|
||||
maxPlayers: 1,
|
||||
currentPlayers: 0,
|
||||
owner: 'Local Player',
|
||||
address: 'localhost',
|
||||
port: this.port,
|
||||
status: 'online',
|
||||
createdAt: new Date().toISOString(),
|
||||
ping: 0
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
servers: [localServer]
|
||||
});
|
||||
});
|
||||
|
||||
// Mock game data endpoints
|
||||
this.app.get('/api/game/player/:id', (req, res) => {
|
||||
// Return player data from local storage if available
|
||||
const playerId = req.params.id;
|
||||
let saveData;
|
||||
|
||||
try {
|
||||
// In Electron context, access localStorage differently
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
saveData = localStorage.getItem(`gso_save_slot_1`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[LOCAL SERVER] Could not access localStorage:', error);
|
||||
}
|
||||
|
||||
if (saveData) {
|
||||
try {
|
||||
const parsedData = JSON.parse(saveData);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
player: parsedData.player
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to parse save data'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'No save data found'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.app.post('/api/game/player/:id/save', (req, res) => {
|
||||
// Save player data to local storage
|
||||
const playerId = req.params.id;
|
||||
const playerData = req.body;
|
||||
|
||||
try {
|
||||
let existingSaveData = '{}';
|
||||
|
||||
// In Electron context, access localStorage differently
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
existingSaveData = localStorage.getItem(`gso_save_slot_1`) || '{}';
|
||||
}
|
||||
|
||||
const parsedExisting = JSON.parse(existingSaveData);
|
||||
|
||||
// Merge player data
|
||||
parsedExisting.player = playerData;
|
||||
parsedExisting.lastSave = Date.now();
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`gso_save_slot_1`, JSON.stringify(parsedExisting));
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Player data saved locally'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to save player data'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async start(port = 3004) {
|
||||
if (this.isRunning) {
|
||||
console.log('[LOCAL SERVER] Server is already running on port', this.port);
|
||||
return { success: false, error: 'Server already running' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize dependencies if not already done
|
||||
if (!this.app) {
|
||||
const initialized = await this.initialize();
|
||||
if (!initialized) {
|
||||
return { success: false, error: 'Failed to initialize server dependencies' };
|
||||
}
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
this.server = this.createServer(this.app);
|
||||
this.io = new this.ServerClass(this.server, {
|
||||
cors: {
|
||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
// Setup Socket.IO handlers
|
||||
this.setupSocketHandlers();
|
||||
|
||||
// Start the server
|
||||
await new Promise((resolve, reject) => {
|
||||
this.server.listen(port, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
console.log(`[LOCAL SERVER] Local server started on port ${port}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
port: port,
|
||||
url: `http://localhost:${port}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER] Failed to start server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
setupSocketHandlers() {
|
||||
this.io.on('connection', (socket) => {
|
||||
console.log('[LOCAL SERVER] Client connected:', socket.id);
|
||||
this.connectedClients.set(socket.id, {
|
||||
connectedAt: Date.now(),
|
||||
playerData: null
|
||||
});
|
||||
|
||||
// Handle authentication
|
||||
socket.on('authenticate', (data) => {
|
||||
console.log('[LOCAL SERVER] Authenticating client:', socket.id, data);
|
||||
|
||||
// Auto-authenticate for local mode
|
||||
socket.emit('authenticated', {
|
||||
success: true,
|
||||
user: {
|
||||
id: 'local-user',
|
||||
username: 'Local Player',
|
||||
token: 'local-token-' + Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
// Update client info
|
||||
const clientInfo = this.connectedClients.get(socket.id);
|
||||
if (clientInfo) {
|
||||
clientInfo.playerData = {
|
||||
id: 'local-user',
|
||||
username: 'Local Player'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Handle game data sync
|
||||
socket.on('saveGameData', (data) => {
|
||||
console.log('[LOCAL SERVER] Saving game data for:', socket.id);
|
||||
|
||||
// Save to localStorage (this will be handled by the client-side save system)
|
||||
socket.emit('gameDataSaved', {
|
||||
success: true,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('loadGameData', (data) => {
|
||||
console.log('[LOCAL SERVER] Loading game data for:', socket.id);
|
||||
|
||||
// Load from localStorage (this will be handled by the client-side load system)
|
||||
let saveData;
|
||||
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
saveData = localStorage.getItem(`gso_save_slot_1`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[LOCAL SERVER] Could not access localStorage:', error);
|
||||
}
|
||||
|
||||
if (saveData) {
|
||||
try {
|
||||
const parsedData = JSON.parse(saveData);
|
||||
socket.emit('gameDataLoaded', {
|
||||
success: true,
|
||||
data: parsedData
|
||||
});
|
||||
} catch (error) {
|
||||
socket.emit('gameDataLoaded', {
|
||||
success: false,
|
||||
error: 'Failed to parse save data'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameDataLoaded', {
|
||||
success: false,
|
||||
error: 'No save data found'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log('[LOCAL SERVER] Client disconnected:', socket.id);
|
||||
this.connectedClients.delete(socket.id);
|
||||
});
|
||||
|
||||
// Send welcome message
|
||||
socket.emit('welcome', {
|
||||
message: 'Connected to local server',
|
||||
serverInfo: {
|
||||
mode: 'local',
|
||||
port: this.port,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.isRunning) {
|
||||
console.log('[LOCAL SERVER] Server is not running');
|
||||
return { success: false, error: 'Server is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Disconnect all clients
|
||||
if (this.io) {
|
||||
this.io.disconnectSockets();
|
||||
}
|
||||
|
||||
// Close the server
|
||||
if (this.server) {
|
||||
await new Promise((resolve) => {
|
||||
this.server.close(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
this.port = null;
|
||||
this.connectedClients.clear();
|
||||
|
||||
console.log('[LOCAL SERVER] Local server stopped');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER] Failed to stop server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
port: this.port,
|
||||
connectedClients: this.connectedClients.size,
|
||||
uptime: this.isRunning ? process.uptime() : 0
|
||||
};
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return this.isRunning ? `http://localhost:${this.port}` : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in Node.js environment
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = LocalServer;
|
||||
}
|
||||
|
||||
// Export for use in browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
window.LocalServer = LocalServer;
|
||||
}
|
||||
224
Client/js/LocalServerManager.js
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Local Server Manager
|
||||
* Manages the local server for singleplayer mode within the Electron client
|
||||
*/
|
||||
|
||||
class LocalServerManager {
|
||||
constructor() {
|
||||
this.localServer = null;
|
||||
this.isRunning = false;
|
||||
this.port = 3004;
|
||||
this.startupAttempts = 0;
|
||||
this.maxStartupAttempts = 3;
|
||||
|
||||
console.log('[LOCAL SERVER MANAGER] LocalServerManager initialized');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('[LOCAL SERVER MANAGER] Initializing local server...');
|
||||
|
||||
try {
|
||||
// In Electron renderer context, use SimpleLocalServer which doesn't require Node.js modules
|
||||
if (typeof window !== 'undefined' && window.SimpleLocalServer) {
|
||||
this.localServer = new window.SimpleLocalServer();
|
||||
console.log('[LOCAL SERVER MANAGER] SimpleLocalServer class loaded from window');
|
||||
return true;
|
||||
} else if (typeof window !== 'undefined' && window.LocalServer) {
|
||||
// Fallback to original LocalServer if available
|
||||
this.localServer = new window.LocalServer();
|
||||
console.log('[LOCAL SERVER MANAGER] LocalServer class loaded from window');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[LOCAL SERVER MANAGER] No local server class available');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER MANAGER] Failed to initialize local server:', error);
|
||||
console.log('[LOCAL SERVER MANAGER] Please ensure SimpleLocalServer.js is loaded properly');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async startServer() {
|
||||
if (this.isRunning) {
|
||||
console.log('[LOCAL SERVER MANAGER] Server is already running');
|
||||
return { success: true, port: this.port };
|
||||
}
|
||||
|
||||
if (!this.localServer) {
|
||||
const initialized = await this.initialize();
|
||||
if (!initialized) {
|
||||
return { success: false, error: 'Failed to initialize server' };
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[LOCAL SERVER MANAGER] Attempting to start server on port ${this.port}`);
|
||||
|
||||
try {
|
||||
const result = await this.localServer.start(this.port);
|
||||
|
||||
if (result.success) {
|
||||
this.isRunning = true;
|
||||
this.port = result.port;
|
||||
this.startupAttempts = 0;
|
||||
|
||||
console.log(`[LOCAL SERVER MANAGER] Server started successfully on port ${this.port}`);
|
||||
console.log(`[LOCAL SERVER MANAGER] Server URL: ${result.url}`);
|
||||
|
||||
// Update LiveMainMenu to use local server
|
||||
this.updateMainMenuForLocalMode();
|
||||
|
||||
return result;
|
||||
} else {
|
||||
console.error('[LOCAL SERVER MANAGER] Failed to start server:', result.error);
|
||||
this.startupAttempts++;
|
||||
|
||||
// Try alternative ports if first attempt fails
|
||||
if (this.startupAttempts < this.maxStartupAttempts) {
|
||||
this.port = 3004 + this.startupAttempts;
|
||||
console.log(`[LOCAL SERVER MANAGER] Retrying with port ${this.port}`);
|
||||
return await this.startServer();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER MANAGER] Exception starting server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async stopServer() {
|
||||
if (!this.isRunning || !this.localServer) {
|
||||
console.log('[LOCAL SERVER MANAGER] Server is not running');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
console.log('[LOCAL SERVER MANAGER] Stopping local server...');
|
||||
|
||||
try {
|
||||
const result = await this.localServer.stop();
|
||||
|
||||
if (result.success) {
|
||||
this.isRunning = false;
|
||||
this.port = 3004;
|
||||
console.log('[LOCAL SERVER MANAGER] Server stopped successfully');
|
||||
} else {
|
||||
console.error('[LOCAL SERVER MANAGER] Failed to stop server:', result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[LOCAL SERVER MANAGER] Exception stopping server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
if (!this.localServer) {
|
||||
return {
|
||||
isRunning: false,
|
||||
port: null,
|
||||
connectedClients: 0,
|
||||
uptime: 0
|
||||
};
|
||||
}
|
||||
|
||||
const status = this.localServer.getStatus();
|
||||
return {
|
||||
...status,
|
||||
url: this.isRunning ? `http://localhost:${status.port}` : null
|
||||
};
|
||||
}
|
||||
|
||||
getServerUrl() {
|
||||
return this.isRunning ? `http://localhost:${this.port}` : null;
|
||||
}
|
||||
|
||||
updateMainMenuForLocalMode() {
|
||||
// Update LiveMainMenu to use local server URLs
|
||||
if (window.liveMainMenu) {
|
||||
console.log('[LOCAL SERVER MANAGER] Updating LiveMainMenu for local mode');
|
||||
|
||||
window.liveMainMenu.apiBaseUrl = `http://localhost:${this.port}/api`;
|
||||
window.liveMainMenu.gameServerUrl = `http://localhost:${this.port}`;
|
||||
window.liveMainMenu.isLocalMode = true;
|
||||
|
||||
console.log(`[LOCAL SERVER MANAGER] Updated API URL to: ${window.liveMainMenu.apiBaseUrl}`);
|
||||
console.log(`[LOCAL SERVER MANAGER] Updated Game Server URL to: ${window.liveMainMenu.gameServerUrl}`);
|
||||
|
||||
// Also update GameInitializer URLs
|
||||
if (window.gameInitializer) {
|
||||
window.gameInitializer.updateServerUrls(
|
||||
`http://localhost:${this.port}/api`,
|
||||
`http://localhost:${this.port}`
|
||||
);
|
||||
console.log('[LOCAL SERVER MANAGER] Updated GameInitializer URLs for local mode');
|
||||
} else {
|
||||
console.warn('[LOCAL SERVER MANAGER] GameInitializer not available for URL update');
|
||||
}
|
||||
} else {
|
||||
console.warn('[LOCAL SERVER MANAGER] LiveMainMenu not available for update');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-start server when in singleplayer mode
|
||||
async autoStartIfSingleplayer() {
|
||||
// Check if we should auto-start (no external server available)
|
||||
try {
|
||||
// Try to connect to external server first
|
||||
const response = await fetch('https://api.korvarix.com/health', {
|
||||
method: 'GET',
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('[LOCAL SERVER MANAGER] External server available, not starting local server');
|
||||
return { success: false, reason: 'External server available' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[LOCAL SERVER MANAGER] External server not available, starting local server');
|
||||
}
|
||||
|
||||
// Start local server
|
||||
return await this.startServer();
|
||||
}
|
||||
|
||||
// Handle server errors and restart if needed
|
||||
handleServerError(error) {
|
||||
console.error('[LOCAL SERVER MANAGER] Server error:', error);
|
||||
|
||||
// Try to restart server if it crashes
|
||||
if (this.isRunning) {
|
||||
console.log('[LOCAL SERVER MANAGER] Attempting to restart server...');
|
||||
this.stopServer().then(() => {
|
||||
setTimeout(() => {
|
||||
this.startServer();
|
||||
}, 2000); // Wait 2 seconds before restarting
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get local server info for UI display
|
||||
getServerInfo() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
port: this.port,
|
||||
url: this.getServerUrl(),
|
||||
status: this.isRunning ? 'Online' : 'Offline',
|
||||
mode: 'Local Singleplayer',
|
||||
connectedClients: this.localServer ? this.localServer.connectedClients.size : 0,
|
||||
uptime: this.localServer ? this.localServer.uptime : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.localServerManager = new LocalServerManager();
|
||||
|
||||
// Auto-export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = LocalServerManager;
|
||||
}
|
||||
|
||||
console.log('[LOCAL SERVER MANAGER] LocalServerManager loaded and global instance created');
|
||||
535
Client/js/SimpleLocalServer.js
Normal file
@ -0,0 +1,535 @@
|
||||
/**
|
||||
* Simple Local Server for Singleplayer Mode
|
||||
* A mock server that simulates server responses without requiring Node.js dependencies
|
||||
* This runs entirely in the browser/renderer context
|
||||
*/
|
||||
|
||||
class SimpleLocalServer {
|
||||
constructor() {
|
||||
this.isRunning = false;
|
||||
this.port = 3004;
|
||||
this.connectedClients = new Map();
|
||||
this.existingSaveData = null;
|
||||
|
||||
// Check for existing save data on initialization
|
||||
this.loadExistingSaveData();
|
||||
|
||||
this.mockData = {
|
||||
servers: [{
|
||||
id: 'local-server',
|
||||
name: 'Local Singleplayer',
|
||||
description: 'Your personal local server for singleplayer gaming',
|
||||
type: 'private',
|
||||
region: 'local',
|
||||
maxPlayers: 1,
|
||||
currentPlayers: 0,
|
||||
owner: 'Local Player',
|
||||
address: 'localhost',
|
||||
port: this.port,
|
||||
status: 'online',
|
||||
createdAt: new Date().toISOString(),
|
||||
ping: 0
|
||||
}],
|
||||
user: {
|
||||
id: 'local-user',
|
||||
email: 'local@player.com',
|
||||
username: 'Local Player',
|
||||
token: 'local-token-' + Date.now(),
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[SIMPLE LOCAL SERVER] SimpleLocalServer initialized');
|
||||
console.log('[SIMPLE LOCAL SERVER] Existing save data found:', !!this.existingSaveData);
|
||||
}
|
||||
|
||||
// Mock Socket.IO server for local mode
|
||||
createMockSocket() {
|
||||
console.log('[SIMPLE LOCAL SERVER] Creating mock Socket.IO connection');
|
||||
|
||||
const mockSocket = {
|
||||
connected: false,
|
||||
eventHandlers: {},
|
||||
|
||||
on: function(event, handler) {
|
||||
console.log(`[MOCKET SOCKET] Registering event handler for: ${event}`);
|
||||
if (!this.eventHandlers[event]) {
|
||||
this.eventHandlers[event] = [];
|
||||
}
|
||||
this.eventHandlers[event].push(handler);
|
||||
},
|
||||
|
||||
emit: function(event, data) {
|
||||
console.log(`[MOCKET SOCKET] Emitting event: ${event}`, data);
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
console.log('[MOCKET SOCKET] Connecting...');
|
||||
this.connected = true;
|
||||
|
||||
// Simulate successful connection
|
||||
setTimeout(() => {
|
||||
if (this.eventHandlers['connect']) {
|
||||
this.eventHandlers['connect'].forEach(handler => handler());
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
console.log('[MOCKET SOCKET] Disconnecting...');
|
||||
this.connected = false;
|
||||
if (this.eventHandlers['disconnect']) {
|
||||
this.eventHandlers['disconnect'].forEach(handler => handler());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-connect
|
||||
mockSocket.connect();
|
||||
|
||||
return mockSocket;
|
||||
}
|
||||
|
||||
loadExistingSaveData() {
|
||||
try {
|
||||
const saveData = localStorage.getItem(`gso_save_slot_1`);
|
||||
if (saveData) {
|
||||
this.existingSaveData = JSON.parse(saveData);
|
||||
console.log('[SIMPLE LOCAL SERVER] Loaded existing save data:', {
|
||||
hasPlayerData: !!this.existingSaveData.player,
|
||||
playerLevel: this.existingSaveData.player?.stats?.level,
|
||||
lastSave: this.existingSaveData.lastSave,
|
||||
gameTime: this.existingSaveData.gameTime
|
||||
});
|
||||
} else {
|
||||
console.log('[SIMPLE LOCAL SERVER] No existing save data found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[SIMPLE LOCAL SERVER] Error loading existing save data:', error);
|
||||
this.existingSaveData = null;
|
||||
}
|
||||
}
|
||||
|
||||
applyExistingSaveDataToGame() {
|
||||
if (!this.existingSaveData || !window.game) {
|
||||
console.log('[SIMPLE LOCAL SERVER] No existing save data or game not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[SIMPLE LOCAL SERVER] Applying existing save data to game...');
|
||||
|
||||
// Apply save data to game systems
|
||||
if (this.existingSaveData.player && window.game.systems.player) {
|
||||
window.game.systems.player.load(this.existingSaveData.player);
|
||||
console.log('[SIMPLE LOCAL SERVER] Player data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.inventory && window.game.systems.inventory) {
|
||||
window.game.systems.inventory.load(this.existingSaveData.inventory);
|
||||
console.log('[SIMPLE LOCAL SERVER] Inventory data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.economy && window.game.systems.economy) {
|
||||
window.game.systems.economy.load(this.existingSaveData.economy);
|
||||
console.log('[SIMPLE LOCAL SERVER] Economy data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.idleSystem && window.game.systems.idleSystem) {
|
||||
window.game.systems.idleSystem.load(this.existingSaveData.idleSystem);
|
||||
console.log('[SIMPLE LOCAL SERVER] Idle system data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.dungeonSystem && window.game.systems.dungeonSystem) {
|
||||
window.game.systems.dungeonSystem.load(this.existingSaveData.dungeonSystem);
|
||||
console.log('[SIMPLE LOCAL SERVER] Dungeon system data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.skillSystem && window.game.systems.skillSystem) {
|
||||
window.game.systems.skillSystem.load(this.existingSaveData.skillSystem);
|
||||
console.log('[SIMPLE LOCAL SERVER] Skill system data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.baseSystem && window.game.systems.baseSystem) {
|
||||
window.game.systems.baseSystem.load(this.existingSaveData.baseSystem);
|
||||
console.log('[SIMPLE LOCAL SERVER] Base system data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.questSystem && window.game.systems.questSystem) {
|
||||
window.game.systems.questSystem.load(this.existingSaveData.questSystem);
|
||||
console.log('[SIMPLE LOCAL SERVER] Quest system data applied');
|
||||
}
|
||||
|
||||
if (this.existingSaveData.gameTime && window.game) {
|
||||
window.game.gameTime = this.existingSaveData.gameTime;
|
||||
console.log('[SIMPLE LOCAL SERVER] Game time applied:', this.existingSaveData.gameTime);
|
||||
}
|
||||
|
||||
console.log('[SIMPLE LOCAL SERVER] All save data applied successfully');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SIMPLE LOCAL SERVER] Error applying save data to game:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async start(port = 3004) {
|
||||
if (this.isRunning) {
|
||||
console.log('[SIMPLE LOCAL SERVER] Server is already running on port', this.port);
|
||||
return { success: false, error: 'Server already running' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.port = port;
|
||||
this.isRunning = true;
|
||||
|
||||
// Update mock server data with actual port
|
||||
this.mockData.servers[0].port = port;
|
||||
|
||||
console.log(`[SIMPLE LOCAL SERVER] Mock local server started on port ${port}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
port: port,
|
||||
url: `http://localhost:${port}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SIMPLE LOCAL SERVER] Failed to start server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.isRunning) {
|
||||
console.log('[SIMPLE LOCAL SERVER] Server is not running');
|
||||
return { success: false, error: 'Server is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.isRunning = false;
|
||||
this.connectedClients.clear();
|
||||
|
||||
console.log('[SIMPLE LOCAL SERVER] Mock local server stopped');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SIMPLE LOCAL SERVER] Failed to stop server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
port: this.port,
|
||||
connectedClients: this.connectedClients.size,
|
||||
uptime: this.isRunning ? 0 : 0 // Mock uptime
|
||||
};
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return this.isRunning ? `http://localhost:${this.port}` : null;
|
||||
}
|
||||
|
||||
// Mock API methods that simulate server responses
|
||||
async mockRequest(method, url, data = null) {
|
||||
console.log(`[SIMPLE LOCAL SERVER] Mock ${method} ${url}`, data);
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
try {
|
||||
if (url === '/health') {
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 0,
|
||||
mode: 'local'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 0,
|
||||
mode: 'local'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url === '/api/ssc/version') {
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
version: '1.0.0',
|
||||
service: 'galaxystrikeonline-local-server',
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: 'local'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
version: '1.0.0',
|
||||
service: 'galaxystrikeonline-local-server',
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: 'local'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url === '/api/auth/login' && method === 'POST') {
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
user: this.mockData.user,
|
||||
token: this.mockData.user.token,
|
||||
message: 'Logged in to local mode'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
user: this.mockData.user,
|
||||
token: this.mockData.user.token,
|
||||
message: 'Logged in to local mode'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url === '/api/auth/register' && method === 'POST') {
|
||||
return {
|
||||
status: 201,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
user: this.mockData.user,
|
||||
token: this.mockData.user.token,
|
||||
message: 'Registered in local mode'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
user: this.mockData.user,
|
||||
token: this.mockData.user.token,
|
||||
message: 'Registered in local mode'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url === '/api/servers') {
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
servers: this.mockData.servers
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
servers: this.mockData.servers
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url.startsWith('/servers/') && url.endsWith('/join') && method === 'POST') {
|
||||
// Mock server join response
|
||||
const serverId = url.split('/')[2];
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
server: {
|
||||
id: serverId,
|
||||
name: 'Local Singleplayer',
|
||||
address: 'localhost',
|
||||
port: this.port,
|
||||
gamePort: this.port + 1,
|
||||
maxPlayers: 1,
|
||||
currentPlayers: 1,
|
||||
status: 'online'
|
||||
},
|
||||
message: 'Joined server successfully'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
server: {
|
||||
id: serverId,
|
||||
name: 'Local Singleplayer',
|
||||
address: 'localhost',
|
||||
port: this.port,
|
||||
gamePort: this.port + 1,
|
||||
maxPlayers: 1,
|
||||
currentPlayers: 1,
|
||||
status: 'online'
|
||||
},
|
||||
message: 'Joined server successfully'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (url.startsWith('/api/game/player/') && method === 'GET') {
|
||||
// Return player data from existing save data or localStorage
|
||||
let saveData = this.existingSaveData;
|
||||
|
||||
// If no existing save data, try localStorage
|
||||
if (!saveData) {
|
||||
try {
|
||||
const localStorageData = localStorage.getItem(`gso_save_slot_1`);
|
||||
if (localStorageData) {
|
||||
saveData = JSON.parse(localStorageData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[SIMPLE LOCAL SERVER] Could not access localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (saveData) {
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
player: saveData.player
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
player: saveData.player
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
ok: false,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: false,
|
||||
error: 'No save data found'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: false,
|
||||
error: 'No save data found'
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (url.startsWith('/api/game/player/') && method === 'POST') {
|
||||
// Save player data to localStorage
|
||||
try {
|
||||
let existingSaveData = '{}';
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
existingSaveData = localStorage.getItem(`gso_save_slot_1`) || '{}';
|
||||
}
|
||||
|
||||
const parsedExisting = JSON.parse(existingSaveData);
|
||||
parsedExisting.player = data;
|
||||
parsedExisting.lastSave = Date.now();
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`gso_save_slot_1`, JSON.stringify(parsedExisting));
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
message: 'Player data saved locally'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Player data saved locally'
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
ok: false,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: false,
|
||||
error: 'Failed to save player data'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to save player data'
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default response for unknown endpoints
|
||||
return {
|
||||
status: 404,
|
||||
ok: false,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: false,
|
||||
error: 'Endpoint not found'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Endpoint not found'
|
||||
}))
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SIMPLE LOCAL SERVER] Mock request error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
ok: false,
|
||||
headers: {
|
||||
get: (name) => name === 'content-type' ? 'application/json' : null
|
||||
},
|
||||
json: () => Promise.resolve({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}),
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SimpleLocalServer = SimpleLocalServer;
|
||||
}
|
||||
|
||||
console.log('[SIMPLE LOCAL SERVER] SimpleLocalServer loaded and exported to window');
|
||||
163
Client/js/core/DebugLogger.js
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Debug Logger
|
||||
* Enhanced debugging that integrates with existing Logger system
|
||||
*/
|
||||
|
||||
class DebugLogger {
|
||||
constructor() {
|
||||
this.debugEnabled = true; // Always enabled
|
||||
this.startTime = performance.now();
|
||||
this.stepTimers = new Map();
|
||||
this.debugLogs = []; // Store logs in memory
|
||||
this.maxLogs = 1000; // Limit memory usage
|
||||
|
||||
// Use the existing logger if available
|
||||
this.logger = window.logger || null;
|
||||
|
||||
// Log initialization
|
||||
this.log('=== DEBUG SESSION STARTED ===');
|
||||
}
|
||||
|
||||
async log(message, data = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const stackTrace = new Error().stack;
|
||||
|
||||
// Build performance object
|
||||
const performanceData = {
|
||||
elapsed: `${(performance.now() - this.startTime).toFixed(2)}ms`,
|
||||
memory: performance.memory ? {
|
||||
used: `${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
|
||||
total: `${(performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`
|
||||
} : null
|
||||
};
|
||||
|
||||
// Create single formatted log message
|
||||
let logMessage = `[DEBUG] ${message}`;
|
||||
if (data) {
|
||||
logMessage += `\n${JSON.stringify(data, null, 2)}`;
|
||||
}
|
||||
if (performanceData) {
|
||||
logMessage += `\n[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory?.used || 'N/A'}/${performanceData.memory?.total || 'N/A'}`;
|
||||
}
|
||||
|
||||
// Add to memory logs
|
||||
const logEntry = {
|
||||
timestamp: timestamp,
|
||||
message: message,
|
||||
data: data ? JSON.stringify(data, null, 2) : '',
|
||||
stackTrace: stackTrace ? stackTrace.split('\n').slice(0, 3).join('\n') : '',
|
||||
performance: performanceData
|
||||
};
|
||||
this.debugLogs.push(logEntry);
|
||||
|
||||
// Limit memory usage
|
||||
if (this.debugLogs.length > this.maxLogs) {
|
||||
this.debugLogs.shift();
|
||||
}
|
||||
|
||||
// Always log to console
|
||||
console.log(`[DEBUG] ${message}`, data || '');
|
||||
|
||||
// Log performance data to console
|
||||
if (performanceData.memory) {
|
||||
console.log(`[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory.used}/${performanceData.memory.total}`);
|
||||
}
|
||||
|
||||
// Use existing logger if available
|
||||
if (this.logger) {
|
||||
try {
|
||||
await this.logger.debug(logMessage);
|
||||
} catch (error) {
|
||||
console.error('[DEBUG LOGGER] Failed to log via existing logger:', error);
|
||||
}
|
||||
} else {
|
||||
// Fallback to electronAPI log
|
||||
if (window.electronAPI && window.electronAPI.log) {
|
||||
window.electronAPI.log('debug', logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async startStep(stepName) {
|
||||
this.stepTimers.set(stepName, performance.now());
|
||||
await this.log(`STEP START: ${stepName}`, {
|
||||
type: 'step_start',
|
||||
step: stepName,
|
||||
elapsed: '0ms'
|
||||
});
|
||||
}
|
||||
|
||||
async endStep(stepName, data = null) {
|
||||
const startTime = this.stepTimers.get(stepName);
|
||||
const duration = startTime ? (performance.now() - startTime).toFixed(2) : 'N/A';
|
||||
|
||||
this.stepTimers.delete(stepName);
|
||||
await this.log(`STEP END: ${stepName}`, {
|
||||
type: 'step_end',
|
||||
step: stepName,
|
||||
duration: `${duration}ms`,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async logStep(stepName, data = null) {
|
||||
await this.log(`STEP: ${stepName}`, {
|
||||
type: 'step',
|
||||
step: stepName,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return this.debugLogs;
|
||||
}
|
||||
|
||||
exportLogs() {
|
||||
const logText = this.debugLogs.map(entry =>
|
||||
`[${entry.timestamp}] ${entry.message}${entry.data ? '\n' + entry.data : ''}${entry.performance ? '\nPerf: ' + entry.performance.elapsed : ''}`
|
||||
).join('\n\n');
|
||||
|
||||
return logText;
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
this.debugLogs = [];
|
||||
this.log('=== LOGS CLEARED ===');
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
await this.log('=== DEBUG SESSION ENDING ===');
|
||||
await this.log('SESSION SUMMARY', {
|
||||
totalLogs: this.debugLogs.length,
|
||||
sessionDuration: `${(performance.now() - this.startTime).toFixed(2)}ms`
|
||||
});
|
||||
|
||||
// No need to finalize files - the existing Logger handles that
|
||||
console.log('[DEBUG LOGGER] Session ended cleanly');
|
||||
}
|
||||
|
||||
// Convenience methods for specific logging types
|
||||
async info(message, data = null) {
|
||||
await this.log(`[INFO] ${message}`, data);
|
||||
}
|
||||
|
||||
async error(message, data = null) {
|
||||
await this.log(`[ERROR] ${message}`, data);
|
||||
}
|
||||
|
||||
async warn(message, data = null) {
|
||||
await this.log(`[WARN] ${message}`, data);
|
||||
}
|
||||
|
||||
async errorEvent(error, context = 'Unknown') {
|
||||
await this.error(`Error in ${context}`, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global debug logger instance
|
||||
window.debugLogger = new DebugLogger();
|
||||
2335
Client/js/core/Economy.js
Normal file
1558
Client/js/core/GameEngine.js
Normal file
1134
Client/js/core/Inventory.js
Normal file
306
Client/js/core/Logger.js
Normal file
@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Logging System
|
||||
* Provides file-based logging with rotation and formatting
|
||||
* Renderer process version that uses IPC to communicate with main process
|
||||
*/
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.logLevel = 'INFO';
|
||||
this.isRenderer = typeof window !== 'undefined' && typeof window.electronAPI !== 'undefined';
|
||||
this.timers = new Map();
|
||||
|
||||
this.levels = {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(appDataPath) {
|
||||
if (this.isRenderer) {
|
||||
// In renderer process, just log that we're ready
|
||||
console.log('Logger initialized in renderer process');
|
||||
return;
|
||||
}
|
||||
|
||||
// Main process initialization (original code)
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
// Set up log directory in app storage location
|
||||
this.logDir = path.join(appDataPath, 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
await fs.mkdir(this.logDir, { recursive: true });
|
||||
|
||||
// Set current log file with full timestamp (YYYY-MM-DD-HH-MM-SS)
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
|
||||
|
||||
// Test write to ensure permissions
|
||||
await this.writeToFile('=== Galaxy Strike Online Log Session Started ===\n');
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log(`Logger initialized: ${this.logDir}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize logger:', error);
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
async writeToFile(message) {
|
||||
if (this.isRenderer) {
|
||||
// In renderer process, send to main process via IPC
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isInitialized || !this.currentLogFile) return;
|
||||
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Check if file needs rotation
|
||||
await this.rotateLogIfNeeded();
|
||||
|
||||
// Append message to current log file
|
||||
await fs.appendFile(this.currentLogFile, message, 'utf8');
|
||||
} catch (error) {
|
||||
console.error('Failed to write to log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async rotateLogIfNeeded() {
|
||||
if (this.isRenderer) return;
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(this.currentLogFile);
|
||||
|
||||
if (stats.size >= this.maxFileSize) {
|
||||
// Rotate log file with full timestamp
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
const rotatedFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
|
||||
|
||||
await fs.rename(this.currentLogFile, rotatedFile);
|
||||
|
||||
// Clean up old log files
|
||||
await this.cleanupOldLogs();
|
||||
|
||||
// Create new current log file with new timestamp
|
||||
const newTimestamp = new Date().toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${newTimestamp}.log`);
|
||||
|
||||
await this.writeToFile('=== Log Rotated ===\n');
|
||||
}
|
||||
} catch (error) {
|
||||
// File might not exist yet, which is fine
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error('Failed to rotate log:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupOldLogs() {
|
||||
if (this.isRenderer) return;
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.logDir);
|
||||
const logFiles = files
|
||||
.filter(file => file.startsWith('galaxy-strike-') && file.endsWith('.log'))
|
||||
.map(file => ({
|
||||
name: file,
|
||||
path: path.join(this.logDir, file)
|
||||
}));
|
||||
|
||||
if (logFiles.length > this.maxLogFiles) {
|
||||
// Get file stats and sort by modification time
|
||||
const filesWithStats = await Promise.all(
|
||||
logFiles.map(async file => {
|
||||
const stats = await fs.stat(file.path);
|
||||
return { ...file, mtime: stats.mtime };
|
||||
})
|
||||
);
|
||||
|
||||
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
// Delete oldest files
|
||||
const filesToDelete = filesWithStats.slice(this.maxLogFiles);
|
||||
for (const file of filesToDelete) {
|
||||
await fs.unlink(file.path);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(level, message, data = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const dataStr = data ? ` | Data: ${JSON.stringify(data)}` : '';
|
||||
return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
|
||||
}
|
||||
|
||||
shouldLog(level) {
|
||||
const currentLevel = this.levels[this.logLevel] || 2;
|
||||
const messageLevel = this.levels[level] || 2;
|
||||
return messageLevel <= currentLevel;
|
||||
}
|
||||
|
||||
async log(level, message, data = null) {
|
||||
if (!this.shouldLog(level)) return;
|
||||
|
||||
if (this.isRenderer && window.electronAPI) {
|
||||
// Send to main process via IPC - ensure data is serializable
|
||||
try {
|
||||
const serializableData = data ? this.makeSerializable(data) : null;
|
||||
window.electronAPI.log(level, message, serializableData);
|
||||
} catch (error) {
|
||||
console.error('Failed to send log to main process:', error);
|
||||
// Fallback to console
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
} else {
|
||||
// Main process logging
|
||||
const formattedMessage = this.formatMessage(level, message, data);
|
||||
await this.writeToFile(formattedMessage);
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
}
|
||||
|
||||
makeSerializable(obj) {
|
||||
try {
|
||||
// Convert to JSON and back to ensure it's serializable
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
// If not serializable, convert to string representation
|
||||
return {
|
||||
type: 'non-serializable',
|
||||
toString: obj.toString ? obj.toString() : String(obj),
|
||||
constructor: obj.constructor ? obj.constructor.name : 'Unknown'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async error(message, data = null) {
|
||||
await this.log('error', message, data);
|
||||
console.error(`[ERROR] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async warn(message, data = null) {
|
||||
await this.log('warn', message, data);
|
||||
console.warn(`[WARN] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async info(message, data = null) {
|
||||
await this.log('info', message, data);
|
||||
console.info(`[INFO] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async debug(message, data = null) {
|
||||
await this.log('debug', message, data);
|
||||
console.debug(`[DEBUG] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async gameEvent(eventType, details) {
|
||||
const message = `Game Event: ${eventType}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
// Timing methods
|
||||
startTimer(name) {
|
||||
this.timers.set(name, performance.now());
|
||||
}
|
||||
|
||||
endTimer(name) {
|
||||
const startTime = this.timers.get(name);
|
||||
if (startTime) {
|
||||
const duration = performance.now() - startTime;
|
||||
this.timers.delete(name);
|
||||
return duration;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async timeAsync(name, asyncFunction) {
|
||||
this.startTimer(name);
|
||||
try {
|
||||
const result = await asyncFunction();
|
||||
const duration = this.endTimer(name);
|
||||
await this.info(`${name} completed`, { duration: `${duration.toFixed(2)}ms` });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = this.endTimer(name);
|
||||
await this.error(`${name} failed`, { duration: `${duration.toFixed(2)}ms`, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async playerAction(action, details) {
|
||||
const message = `Player Action: ${action}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
async systemEvent(system, event, details) {
|
||||
const message = `System Event: ${system} - ${event}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
async errorEvent(error, context = null) {
|
||||
const message = `Error Event: ${error.message || error}`;
|
||||
const data = {
|
||||
stack: error.stack,
|
||||
context: context
|
||||
};
|
||||
await this.error(message, data);
|
||||
}
|
||||
|
||||
setLogLevel(level) {
|
||||
if (this.levels.hasOwnProperty(level)) {
|
||||
this.logLevel = level;
|
||||
this.info(`Log level changed to: ${level}`);
|
||||
}
|
||||
}
|
||||
|
||||
getLogInfo() {
|
||||
return {
|
||||
isInitialized: this.isInitialized,
|
||||
isRenderer: this.isRenderer,
|
||||
logDirectory: this.logDir,
|
||||
currentLogFile: this.currentLogFile,
|
||||
logLevel: this.logLevel
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
const logger = new Logger();
|
||||
|
||||
// For Node.js environments
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = logger;
|
||||
}
|
||||
|
||||
// For browser environments
|
||||
if (typeof window !== 'undefined') {
|
||||
window.logger = logger;
|
||||
}
|
||||
910
Client/js/core/Player.js
Normal file
@ -0,0 +1,910 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Player System
|
||||
* Manages player stats, levels, and progression
|
||||
*/
|
||||
|
||||
class Player {
|
||||
constructor(gameEngine) {
|
||||
const debugLogger = window.debugLogger;
|
||||
if (debugLogger) debugLogger.log('Player constructor called');
|
||||
|
||||
this.game = gameEngine;
|
||||
|
||||
// Player stats
|
||||
this.stats = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
totalXP: 0, // Total accumulated XP across all levels
|
||||
experienceToNext: window.XPProgression ? window.XPProgression.calculateXPToNextLevel(1, 0) : 100,
|
||||
skillPoints: 0,
|
||||
totalKills: 0,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: Date.now()
|
||||
};
|
||||
|
||||
// Base attributes
|
||||
this.attributes = {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Player info
|
||||
this.info = {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
};
|
||||
|
||||
// Ship info
|
||||
this.ship = {
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
level: 1,
|
||||
upgrades: []
|
||||
};
|
||||
|
||||
// Settings
|
||||
this.settings = {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
discordIntegration: false
|
||||
};
|
||||
|
||||
if (debugLogger) debugLogger.log('Player constructor completed', {
|
||||
initialLevel: this.stats.level,
|
||||
initialHealth: this.attributes.health,
|
||||
shipName: this.ship.name,
|
||||
shipClass: this.ship.class
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Player system initializing');
|
||||
if (debugLogger) await debugLogger.startStep('playerInitialize');
|
||||
|
||||
try {
|
||||
if (debugLogger) await debugLogger.logStep('Player initialization started');
|
||||
// Player initialization is handled by GameEngine
|
||||
// This method is kept for compatibility but doesn't load game data
|
||||
console.log('[PLAYER] Player system initialization completed');
|
||||
if (debugLogger) await debugLogger.endStep('playerInitialize');
|
||||
} catch (error) {
|
||||
console.error('[PLAYER] Error during initialization:', error);
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Player Initialize');
|
||||
}
|
||||
}
|
||||
|
||||
setupNewPlayer() {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Setting up new player');
|
||||
if (debugLogger) debugLogger.logStep('Setting up new player', {
|
||||
currentLevel: this.stats.level,
|
||||
currentTitle: this.info.title
|
||||
});
|
||||
|
||||
this.game.showNotification('Welcome to Galaxy Strike Online, Commander!', 'success', 5000);
|
||||
this.game.showNotification('Complete quests and explore dungeons to progress!', 'info', 4000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('New player setup completed', {
|
||||
notificationsShown: 2
|
||||
});
|
||||
}
|
||||
|
||||
// Experience and leveling
|
||||
addExperience(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldExperience = this.stats.experience;
|
||||
const oldLevel = this.stats.level;
|
||||
const oldTotalXP = this.stats.totalXP;
|
||||
|
||||
// Add to total accumulated XP
|
||||
this.stats.totalXP += amount;
|
||||
|
||||
// Calculate new level based on total XP
|
||||
if (window.XPProgression) {
|
||||
const levelInfo = window.XPProgression.getLevelFromXP(this.stats.totalXP);
|
||||
this.stats.level = levelInfo.level;
|
||||
this.stats.experience = levelInfo.xpIntoLevel;
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
} else {
|
||||
// Fallback to old system
|
||||
this.stats.experience += amount;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Experience added', {
|
||||
amount: amount,
|
||||
oldExperience: oldExperience,
|
||||
newExperience: this.stats.experience,
|
||||
oldTotalXP: oldTotalXP,
|
||||
newTotalXP: this.stats.totalXP,
|
||||
experienceToNext: this.stats.experienceToNext,
|
||||
currentLevel: oldLevel,
|
||||
newLevel: this.stats.level
|
||||
});
|
||||
|
||||
// Check for level up
|
||||
const levelsGained = this.stats.level - oldLevel;
|
||||
if (levelsGained > 0) {
|
||||
for (let i = 0; i < levelsGained; i++) {
|
||||
this.levelUp();
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogger && levelsGained > 0) {
|
||||
debugLogger.logStep('Level up(s) occurred', {
|
||||
levelsGained: levelsGained,
|
||||
newLevel: this.stats.level,
|
||||
remainingExperience: this.stats.experience,
|
||||
totalXP: this.stats.totalXP
|
||||
});
|
||||
}
|
||||
|
||||
this.game.showNotification(`+${this.game.formatNumber(amount)} XP`, 'success', 2000);
|
||||
}
|
||||
|
||||
levelUp() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldLevel = this.stats.level;
|
||||
const oldSkillPoints = this.stats.skillPoints;
|
||||
const oldMaxHealth = this.attributes.maxHealth;
|
||||
const oldMaxEnergy = this.attributes.maxEnergy;
|
||||
const oldAttack = this.attributes.attack;
|
||||
const oldDefense = this.attributes.defense;
|
||||
const oldShipMaxHealth = this.ship.maxHealth;
|
||||
const oldTitle = this.info.title;
|
||||
|
||||
console.log(`[PLAYER] Level up! New level: ${this.stats.level}`);
|
||||
if (debugLogger) debugLogger.logStep('Level up initiated', {
|
||||
oldLevel: oldLevel,
|
||||
newLevel: this.stats.level,
|
||||
skillPointsGained: 2,
|
||||
totalSkillPoints: this.stats.skillPoints + 2,
|
||||
currentXP: this.stats.experience,
|
||||
totalXP: this.stats.totalXP
|
||||
});
|
||||
|
||||
// Update quest progress for level objectives
|
||||
if (this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.updateLevelProgress(this.stats.level);
|
||||
if (debugLogger) debugLogger.logStep('Quest progress updated for new level');
|
||||
}
|
||||
|
||||
// Update experience requirement for next level
|
||||
if (window.XPProgression) {
|
||||
const levelInfo = window.XPProgression.getLevelFromXP(this.stats.totalXP);
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
} else {
|
||||
// Fallback to old system
|
||||
this.stats.experienceToNext = Math.floor(this.stats.experienceToNext * 1.5);
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Experience requirement updated', {
|
||||
newRequirement: this.stats.experienceToNext,
|
||||
usingNewSystem: !!window.XPProgression
|
||||
});
|
||||
|
||||
// Improve base stats
|
||||
this.attributes.maxHealth += 10;
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
|
||||
// Update UI to show new level
|
||||
if (this.game && this.game.systems && this.game.systems.ui) {
|
||||
this.game.systems.ui.updateUI();
|
||||
if (debugLogger) debugLogger.logStep('UI updated for new level');
|
||||
}
|
||||
|
||||
this.game.showNotification(`Level Up! You are now level ${this.stats.level}!`, 'success', 3000);
|
||||
this.attributes.maxEnergy += 5;
|
||||
this.attributes.energy = this.attributes.maxEnergy;
|
||||
this.attributes.attack += 2;
|
||||
this.attributes.defense += 1;
|
||||
|
||||
// Update ship health
|
||||
this.ship.maxHealth += 15;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
|
||||
// Update title based on level
|
||||
this.updateTitle();
|
||||
|
||||
this.game.showNotification(`Level Up! You are now level ${this.stats.level}!`, 'success', 5000);
|
||||
this.game.showNotification(`+2 Skill Points available`, 'info', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Level up completed', {
|
||||
levelChange: `${oldLevel} → ${this.stats.level}`,
|
||||
healthChange: `${oldMaxHealth} → ${this.attributes.maxHealth}`,
|
||||
energyChange: `${oldMaxEnergy} → ${this.attributes.maxEnergy}`,
|
||||
attackChange: `${oldAttack} → ${this.attributes.attack}`,
|
||||
defenseChange: `${oldDefense} → ${this.attributes.defense}`,
|
||||
shipHealthChange: `${oldShipMaxHealth} → ${this.ship.maxHealth}`,
|
||||
titleChange: `${oldTitle} → ${this.info.title}`,
|
||||
skillPointsChange: `${oldSkillPoints} → ${this.stats.skillPoints + 2}`
|
||||
});
|
||||
|
||||
// Add skill points after logging the changes
|
||||
this.stats.skillPoints += 2;
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldTitle = this.info.title;
|
||||
|
||||
const titles = {
|
||||
1: 'Rookie Pilot',
|
||||
5: 'Space Cadet',
|
||||
10: 'Star Explorer',
|
||||
15: 'Galaxy Ranger',
|
||||
20: 'Space Captain',
|
||||
25: 'Star Commander',
|
||||
30: 'Galaxy Admiral',
|
||||
40: 'Space Legend',
|
||||
50: 'Cosmic Master'
|
||||
};
|
||||
|
||||
for (const [level, title] of Object.entries(titles)) {
|
||||
if (this.stats.level >= parseInt(level)) {
|
||||
this.info.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogger && oldTitle !== this.info.title) {
|
||||
debugLogger.logStep('Player title updated', {
|
||||
level: this.stats.level,
|
||||
oldTitle: oldTitle,
|
||||
newTitle: this.info.title
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Combat stats
|
||||
takeDamage(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldHealth = this.attributes.health;
|
||||
const actualDamage = Math.max(1, amount - this.attributes.defense);
|
||||
this.attributes.health = Math.max(0, this.attributes.health - actualDamage);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player took damage', {
|
||||
damageAmount: amount,
|
||||
playerDefense: this.attributes.defense,
|
||||
actualDamage: actualDamage,
|
||||
oldHealth: oldHealth,
|
||||
newHealth: this.attributes.health,
|
||||
healthRemaining: this.attributes.health > 0
|
||||
});
|
||||
|
||||
if (this.attributes.health === 0) {
|
||||
this.onDeath();
|
||||
}
|
||||
return actualDamage;
|
||||
}
|
||||
|
||||
heal(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldHealth = this.attributes.health;
|
||||
const healAmount = Math.min(amount, this.attributes.maxHealth - this.attributes.health);
|
||||
this.attributes.health += healAmount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player healed', {
|
||||
healAmount: amount,
|
||||
actualHealAmount: healAmount,
|
||||
oldHealth: oldHealth,
|
||||
newHealth: this.attributes.health,
|
||||
maxHealth: this.attributes.maxHealth,
|
||||
healthPercent: Math.round((this.attributes.health / this.attributes.maxHealth) * 100)
|
||||
});
|
||||
|
||||
return healAmount;
|
||||
}
|
||||
|
||||
useEnergy(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldEnergy = this.attributes.energy;
|
||||
|
||||
if (this.attributes.energy < amount) {
|
||||
if (debugLogger) debugLogger.logStep('Energy use failed - insufficient energy', {
|
||||
requestedAmount: amount,
|
||||
currentEnergy: oldEnergy,
|
||||
deficit: amount - oldEnergy
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.attributes.energy -= amount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Energy used', {
|
||||
amountUsed: amount,
|
||||
oldEnergy: oldEnergy,
|
||||
newEnergy: this.attributes.energy,
|
||||
maxEnergy: this.attributes.maxEnergy,
|
||||
energyPercent: Math.round((this.attributes.energy / this.attributes.maxEnergy) * 100)
|
||||
});
|
||||
|
||||
// Update UI to show energy change
|
||||
this.updateUI();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
restoreEnergy(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldEnergy = this.attributes.energy;
|
||||
const restoreAmount = Math.min(amount, this.attributes.maxEnergy - this.attributes.energy);
|
||||
this.attributes.energy += restoreAmount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Energy restored', {
|
||||
restoreAmount: amount,
|
||||
actualRestoreAmount: restoreAmount,
|
||||
oldEnergy: oldEnergy,
|
||||
newEnergy: this.attributes.energy,
|
||||
maxEnergy: this.attributes.maxEnergy,
|
||||
energyPercent: Math.round((this.attributes.energy / this.attributes.maxEnergy) * 100)
|
||||
});
|
||||
|
||||
// Update UI to show energy change
|
||||
this.updateUI();
|
||||
|
||||
return restoreAmount;
|
||||
}
|
||||
|
||||
// Energy regeneration
|
||||
regenerateEnergy(deltaTime) {
|
||||
const regenerationRate = this.getMaxEnergy() * 0.1; // 10% of max energy per second
|
||||
const energyToRegen = (deltaTime / 1000) * regenerationRate;
|
||||
|
||||
if (this.attributes.energy < this.getMaxEnergy()) {
|
||||
this.attributes.energy = Math.min(this.attributes.energy + energyToRegen, this.getMaxEnergy());
|
||||
|
||||
const debugLogger = window.debugLogger;
|
||||
if (debugLogger) debugLogger.logStep('Energy regenerated', {
|
||||
deltaTime: deltaTime,
|
||||
energyRegenerated: energyToRegen,
|
||||
currentEnergy: this.attributes.energy,
|
||||
maxEnergy: this.getMaxEnergy()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Combat calculations
|
||||
calculateDamage(enemyDifficulty = 'normal') {
|
||||
const debugLogger = window.debugLogger;
|
||||
const baseDamage = this.ship.attack || this.attributes.attack;
|
||||
|
||||
// Adjust critical chance based on enemy difficulty
|
||||
let criticalChance = this.ship.criticalChance || this.attributes.criticalChance;
|
||||
const difficultyMultipliers = {
|
||||
'tutorial': 1.5, // Higher chance against easy enemies
|
||||
'easy': 1.2,
|
||||
'normal': 1.0,
|
||||
'medium': 0.9,
|
||||
'hard': 0.7, // Lower chance against hard enemies
|
||||
'extreme': 0.5 // Much lower chance against extreme enemies
|
||||
};
|
||||
|
||||
const originalCriticalChance = criticalChance;
|
||||
criticalChance *= (difficultyMultipliers[enemyDifficulty] || 1.0);
|
||||
|
||||
const criticalRoll = Math.random();
|
||||
|
||||
let damage = baseDamage;
|
||||
let isCritical = false;
|
||||
|
||||
if (criticalRoll < criticalChance) {
|
||||
damage *= (this.ship.criticalDamage || this.attributes.criticalDamage);
|
||||
isCritical = true;
|
||||
}
|
||||
|
||||
// Add some randomness
|
||||
const randomMultiplier = this.game.getRandomFloat(0.9, 1.1);
|
||||
damage *= randomMultiplier;
|
||||
|
||||
const finalDamage = Math.floor(damage);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Damage calculation completed', {
|
||||
enemyDifficulty: enemyDifficulty,
|
||||
baseDamage: baseDamage,
|
||||
originalCriticalChance: originalCriticalChance,
|
||||
adjustedCriticalChance: criticalChance,
|
||||
criticalRoll: criticalRoll,
|
||||
criticalDamageMultiplier: this.ship.criticalDamage || this.attributes.criticalDamage,
|
||||
randomMultiplier: randomMultiplier,
|
||||
isCritical: isCritical,
|
||||
finalDamage: finalDamage
|
||||
});
|
||||
|
||||
return {
|
||||
damage: finalDamage,
|
||||
isCritical
|
||||
};
|
||||
}
|
||||
|
||||
onDeath() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
console.log('[PLAYER] Player death occurred');
|
||||
if (debugLogger) debugLogger.logStep('Player death triggered', {
|
||||
currentLevel: this.stats.level,
|
||||
oldCredits: oldCredits,
|
||||
totalKills: this.stats.totalKills,
|
||||
dungeonsCleared: this.stats.dungeonsCleared
|
||||
});
|
||||
|
||||
this.game.showNotification('Your ship was destroyed! Respawning...', 'error', 3000);
|
||||
|
||||
// Reset health and energy
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
this.attributes.energy = this.attributes.maxEnergy;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
|
||||
// Apply death penalty
|
||||
const lostCredits = Math.floor(this.game.systems.economy.credits * 0.1);
|
||||
this.game.systems.economy.removeCredits(lostCredits);
|
||||
|
||||
const newCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
this.game.showNotification(`Death penalty: -${this.game.formatNumber(lostCredits)} credits`, 'warning', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player death completed', {
|
||||
healthRestored: this.attributes.health,
|
||||
energyRestored: this.attributes.energy,
|
||||
shipHealthRestored: this.ship.health,
|
||||
creditsLost: lostCredits,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: newCredits,
|
||||
penaltyPercentage: 10
|
||||
});
|
||||
}
|
||||
|
||||
resetToLevel1() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldStats = { ...this.stats };
|
||||
const oldAttributes = { ...this.attributes };
|
||||
const oldInfo = { ...this.info };
|
||||
const oldShip = { ...this.ship };
|
||||
|
||||
console.log('[PLAYER] Resetting player to level 1');
|
||||
if (debugLogger) debugLogger.logStep('Player reset to level 1 initiated', {
|
||||
oldLevel: oldStats.level,
|
||||
oldExperience: oldStats.experience,
|
||||
oldKills: oldStats.totalKills
|
||||
});
|
||||
|
||||
// Reset stats to initial values
|
||||
this.stats = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
totalXP: 0, // Total accumulated XP across all levels
|
||||
experienceToNext: window.XPProgression ? window.XPProgression.calculateXPToNextLevel(1, 0) : 100,
|
||||
skillPoints: 0,
|
||||
totalKills: 0,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: Date.now(),
|
||||
tutorialDungeonCompleted: false
|
||||
};
|
||||
|
||||
// Reset attributes to base values
|
||||
this.attributes = {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Reset info
|
||||
this.info = {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
};
|
||||
|
||||
// Reset ship
|
||||
this.ship = {
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
health: 1000,
|
||||
maxHealth: 1000,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
level: 1,
|
||||
upgrades: []
|
||||
};
|
||||
|
||||
console.log('=== DEBUG: Character Reset ===');
|
||||
console.log('Player health reset to:', this.attributes.health, '/', this.attributes.maxHealth);
|
||||
console.log('Ship health reset to:', this.ship.health, '/', this.ship.maxHealth);
|
||||
|
||||
// Reset skills
|
||||
this.skills = {};
|
||||
|
||||
// Reset settings to defaults
|
||||
this.settings = {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
theme: 'dark'
|
||||
};
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player reset to level 1 completed', {
|
||||
newLevel: this.stats.level,
|
||||
newHealth: this.attributes.health,
|
||||
newShipHealth: this.ship.health,
|
||||
skillsCleared: true,
|
||||
settingsReset: true
|
||||
});
|
||||
}
|
||||
|
||||
// Ship management
|
||||
upgradeShip(upgradeType) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const upgradeCosts = {
|
||||
health: 100,
|
||||
attack: 150,
|
||||
defense: 120,
|
||||
speed: 80,
|
||||
critical: 200
|
||||
};
|
||||
|
||||
const cost = upgradeCosts[upgradeType];
|
||||
const oldCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade attempted', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
currentCredits: oldCredits,
|
||||
canAfford: oldCredits >= cost
|
||||
});
|
||||
|
||||
if (!cost || !this.game.systems.economy || this.game.systems.economy.credits < cost) {
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade failed - insufficient funds or invalid type', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
currentCredits: oldCredits,
|
||||
deficit: cost - oldCredits,
|
||||
economySystemAvailable: !!this.game.systems.economy
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldShipStats = { ...this.ship };
|
||||
const oldPlayerStats = { ...this.attributes };
|
||||
|
||||
if (this.game.systems.economy) {
|
||||
this.game.systems.economy.removeCredits(cost);
|
||||
} else {
|
||||
if (debugLogger) debugLogger.log('Economy system not available during ship upgrade');
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (upgradeType) {
|
||||
case 'health':
|
||||
this.ship.maxHealth += 20;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
this.attributes.maxHealth += 10;
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
break;
|
||||
case 'attack':
|
||||
this.ship.attack += 3;
|
||||
break;
|
||||
case 'defense':
|
||||
this.ship.defense += 2;
|
||||
break;
|
||||
case 'speed':
|
||||
this.ship.speed += 2;
|
||||
break;
|
||||
case 'critical':
|
||||
this.ship.criticalChance = Math.min(0.5, this.ship.criticalChance + 0.02);
|
||||
this.ship.criticalDamage += 0.1;
|
||||
break;
|
||||
}
|
||||
|
||||
this.ship.upgrades.push(upgradeType);
|
||||
this.game.showNotification(`Ship upgraded: ${upgradeType}!`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade completed', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: this.game.systems.economy?.credits || 0,
|
||||
shipChanges: {
|
||||
oldMaxHealth: oldShipStats.maxHealth,
|
||||
newMaxHealth: this.ship.maxHealth,
|
||||
oldAttack: oldShipStats.attack,
|
||||
newAttack: this.ship.attack,
|
||||
oldDefense: oldShipStats.defense,
|
||||
newDefense: this.ship.defense,
|
||||
oldSpeed: oldShipStats.speed,
|
||||
newSpeed: this.ship.speed,
|
||||
oldCriticalChance: oldShipStats.criticalChance,
|
||||
newCriticalChance: this.ship.criticalChance,
|
||||
oldCriticalDamage: oldShipStats.criticalDamage,
|
||||
newCriticalDamage: this.ship.criticalDamage
|
||||
},
|
||||
playerChanges: {
|
||||
oldMaxHealth: oldPlayerStats.maxHealth,
|
||||
newMaxHealth: this.attributes.maxHealth,
|
||||
oldHealth: oldPlayerStats.health,
|
||||
newHealth: this.attributes.health
|
||||
},
|
||||
totalUpgrades: this.ship.upgrades.length
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Statistics tracking
|
||||
incrementKills() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldKills = this.stats.totalKills;
|
||||
this.stats.totalKills++;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Kill count incremented', {
|
||||
oldKills: oldKills,
|
||||
newKills: this.stats.totalKills,
|
||||
currentLevel: this.stats.level
|
||||
});
|
||||
|
||||
// Update quest progress for combat objectives
|
||||
if (this.game && this.game.systems && this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.onEnemyDefeated();
|
||||
if (debugLogger) debugLogger.logStep('Quest system notified of enemy defeat');
|
||||
}
|
||||
}
|
||||
|
||||
incrementDungeonsCleared() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldDungeons = this.stats.dungeonsCleared;
|
||||
this.stats.dungeonsCleared++;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Dungeons cleared incremented', {
|
||||
oldDungeons: oldDungeons,
|
||||
newDungeons: this.stats.dungeonsCleared,
|
||||
currentLevel: this.stats.level
|
||||
});
|
||||
}
|
||||
|
||||
updatePlayTime(deltaTime) {
|
||||
console.log('[PLAYER] updatePlayTime called with deltaTime:', deltaTime, 'ms');
|
||||
console.log('[PLAYER] Before update - playTime:', this.stats.playTime, 'ms');
|
||||
|
||||
// Use real computer time delta
|
||||
this.stats.playTime += deltaTime;
|
||||
|
||||
console.log('[PLAYER] After update - playTime:', this.stats.playTime, 'ms');
|
||||
console.log('[PLAYER] PlayTime in seconds:', this.stats.playTime / 1000, 'seconds');
|
||||
console.log('[PLAYER] PlayTime in minutes:', this.stats.playTime / 60000, 'minutes');
|
||||
console.log('[PLAYER] PlayTime in hours:', this.stats.playTime / 3600000, 'hours');
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player UI update started', {
|
||||
currentLevel: this.stats.level,
|
||||
currentHealth: this.attributes.health,
|
||||
currentEnergy: this.attributes.energy,
|
||||
totalKills: this.stats.totalKills
|
||||
});
|
||||
|
||||
// Update player info
|
||||
const playerNameElement = document.getElementById('playerName');
|
||||
const playerLevelElement = document.getElementById('playerLevel');
|
||||
|
||||
if (playerNameElement) {
|
||||
playerNameElement.textContent = `${this.info.name} - ${this.info.title}`;
|
||||
}
|
||||
|
||||
if (playerLevelElement) {
|
||||
playerLevelElement.textContent = `Lv. ${this.stats.level}`;
|
||||
}
|
||||
|
||||
// Update health and energy
|
||||
if (this.game && this.game.systems && this.game.systems.ui) {
|
||||
this.game.systems.ui.updateResourceDisplay();
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const totalKillsElement = document.getElementById('totalKills');
|
||||
const dungeonsClearedElement = document.getElementById('dungeonsCleared');
|
||||
const playTimeElement = document.getElementById('playTime');
|
||||
|
||||
if (totalKillsElement) {
|
||||
totalKillsElement.textContent = this.game.formatNumber(this.stats.totalKills);
|
||||
}
|
||||
|
||||
if (dungeonsClearedElement) {
|
||||
dungeonsClearedElement.textContent = this.game.formatNumber(this.stats.dungeonsCleared);
|
||||
}
|
||||
|
||||
if (playTimeElement) {
|
||||
playTimeElement.textContent = this.game.formatTime(this.stats.playTime / 1000);
|
||||
}
|
||||
|
||||
// Update ship info
|
||||
const flagshipNameElement = document.getElementById('flagshipName');
|
||||
const shipHealthElement = document.getElementById('shipHealth');
|
||||
|
||||
if (flagshipNameElement) {
|
||||
flagshipNameElement.textContent = this.ship.name;
|
||||
}
|
||||
|
||||
if (shipHealthElement) {
|
||||
const healthPercent = Math.round((this.ship.health / this.ship.maxHealth) * 100);
|
||||
shipHealthElement.textContent = `${healthPercent}%`;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player UI update completed', {
|
||||
elementsUpdated: {
|
||||
playerName: !!playerNameElement,
|
||||
playerLevel: !!playerLevelElement,
|
||||
totalKills: !!totalKillsElement,
|
||||
dungeonsCleared: !!dungeonsClearedElement,
|
||||
playTime: !!playTimeElement,
|
||||
flagshipName: !!flagshipNameElement,
|
||||
shipHealth: !!shipHealthElement
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save/Load
|
||||
save() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
const saveData = {
|
||||
stats: this.stats,
|
||||
attributes: this.attributes,
|
||||
info: this.info,
|
||||
ship: this.ship,
|
||||
settings: this.settings
|
||||
};
|
||||
|
||||
// if (debugLogger) debugLogger.logStep('Player save data prepared', {
|
||||
// level: this.stats.level,
|
||||
// experience: this.stats.experience,
|
||||
// totalKills: this.stats.totalKills,
|
||||
// dungeonsCleared: this.stats.dungeonsCleared,
|
||||
// playTime: this.stats.playTime,
|
||||
// shipName: this.ship.name,
|
||||
// shipLevel: this.ship.level,
|
||||
// upgradesCount: this.ship.upgrades.length,
|
||||
// dataSize: JSON.stringify(saveData).length
|
||||
// });
|
||||
|
||||
return saveData;
|
||||
}
|
||||
|
||||
load(data) {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Loading player data:', data);
|
||||
console.log('[PLAYER] Current level before load:', this.stats.level);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player load initiated', {
|
||||
hasData: !!data,
|
||||
dataKeys: data ? Object.keys(data) : [],
|
||||
currentLevel: this.stats.level,
|
||||
currentExperience: this.stats.experience
|
||||
});
|
||||
|
||||
try {
|
||||
if (data.stats) {
|
||||
console.log('[PLAYER] Loading stats:', data.stats);
|
||||
const oldStats = { ...this.stats };
|
||||
this.stats = { ...this.stats, ...data.stats };
|
||||
console.log('[PLAYER] Level after stats load:', this.stats.level);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player stats loaded', {
|
||||
oldLevel: oldStats.level,
|
||||
newLevel: this.stats.level,
|
||||
oldExperience: oldStats.experience,
|
||||
newExperience: this.stats.experience,
|
||||
oldKills: oldStats.totalKills,
|
||||
newKills: this.stats.totalKills
|
||||
});
|
||||
}
|
||||
|
||||
if (data.attributes) {
|
||||
console.log('[PLAYER] Loading attributes:', data.attributes);
|
||||
const oldAttributes = { ...this.attributes };
|
||||
this.attributes = { ...this.attributes, ...data.attributes };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player attributes loaded', {
|
||||
oldHealth: oldAttributes.health,
|
||||
newHealth: this.attributes.health,
|
||||
oldMaxHealth: oldAttributes.maxHealth,
|
||||
newMaxHealth: this.attributes.maxHealth,
|
||||
oldAttack: oldAttributes.attack,
|
||||
newAttack: this.attributes.attack,
|
||||
oldDefense: oldAttributes.defense,
|
||||
newDefense: this.attributes.defense
|
||||
});
|
||||
}
|
||||
|
||||
if (data.info) {
|
||||
console.log('[PLAYER] Loading info:', data.info);
|
||||
const oldInfo = { ...this.info };
|
||||
this.info = { ...this.info, ...data.info };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player info loaded', {
|
||||
oldName: oldInfo.name,
|
||||
newName: this.info.name,
|
||||
oldTitle: oldInfo.title,
|
||||
newTitle: this.info.title,
|
||||
oldGuild: oldInfo.guild,
|
||||
newGuild: this.info.guild
|
||||
});
|
||||
}
|
||||
|
||||
if (data.ship) {
|
||||
console.log('[PLAYER] Loading ship:', data.ship);
|
||||
const oldShip = { ...this.ship };
|
||||
this.ship = { ...this.ship, ...data.ship };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player ship loaded', {
|
||||
oldShipName: oldShip.name,
|
||||
newShipName: this.ship.name,
|
||||
oldShipLevel: oldShip.level,
|
||||
newShipLevel: this.ship.level,
|
||||
oldUpgrades: oldShip.upgrades.length,
|
||||
newUpgrades: this.ship.upgrades.length
|
||||
});
|
||||
}
|
||||
|
||||
if (data.settings) {
|
||||
console.log('[PLAYER] Loading settings:', data.settings);
|
||||
const oldSettings = { ...this.settings };
|
||||
this.settings = { ...this.settings, ...data.settings };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player settings loaded', {
|
||||
oldAutoSave: oldSettings.autoSave,
|
||||
newAutoSave: this.settings.autoSave,
|
||||
oldNotifications: oldSettings.notifications,
|
||||
newNotifications: this.settings.notifications
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[PLAYER] Final level after load:', this.stats.level);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player load completed successfully', {
|
||||
finalLevel: this.stats.level,
|
||||
finalExperience: this.stats.experience,
|
||||
finalHealth: this.attributes.health,
|
||||
finalShipHealth: this.ship.health,
|
||||
totalDataSections: ['stats', 'attributes', 'info', 'ship', 'settings'].filter(key => data[key]).length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PLAYER] Error loading player data:', error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, 'Player Load');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
142
Client/js/core/TextureManager.js
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Texture Manager
|
||||
* Handles texture loading and missing texture fallbacks
|
||||
*/
|
||||
|
||||
class TextureManager {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
this.textures = new Map();
|
||||
this.missingTextureUrl = 'assets/textures/missing-texture.png';
|
||||
|
||||
// Initialize missing texture
|
||||
this.loadMissingTexture();
|
||||
}
|
||||
|
||||
async loadMissingTexture() {
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = this.missingTextureUrl;
|
||||
await img.decode();
|
||||
this.textures.set('missing', img);
|
||||
} catch (error) {
|
||||
console.warn('Could not load missing texture, creating fallback');
|
||||
this.createFallbackTexture();
|
||||
}
|
||||
}
|
||||
|
||||
createFallbackTexture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Create a pink and black checkerboard pattern
|
||||
const squareSize = 8;
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ? '#FF00FF' : '#000000';
|
||||
ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Add "GSO Missing" text
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('GSO', 32, 28);
|
||||
ctx.fillText('Missing', 32, 36);
|
||||
|
||||
const img = new Image();
|
||||
img.src = canvas.toDataURL();
|
||||
this.textures.set('missing', img);
|
||||
}
|
||||
|
||||
async loadTexture(textureId, textureUrl) {
|
||||
// Check if already loaded
|
||||
if (this.textures.has(textureId)) {
|
||||
return this.textures.get(textureId);
|
||||
}
|
||||
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = textureUrl;
|
||||
await img.decode();
|
||||
this.textures.set(textureId, img);
|
||||
return img;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load texture ${textureId} from ${textureUrl}, using missing texture`);
|
||||
return this.getMissingTexture();
|
||||
}
|
||||
}
|
||||
|
||||
getTexture(textureId) {
|
||||
return this.textures.get(textureId) || this.getMissingTexture();
|
||||
}
|
||||
|
||||
getMissingTexture() {
|
||||
return this.textures.get('missing') || this.createFallbackTexture();
|
||||
}
|
||||
|
||||
// Icon fallback for FontAwesome icons
|
||||
getIcon(iconClass) {
|
||||
// Check if this is a valid FontAwesome icon
|
||||
const validIconPrefixes = ['fas', 'far', 'fab', 'fal'];
|
||||
const iconParts = iconClass.split(' ');
|
||||
const hasValidPrefix = iconParts.some(part => validIconPrefixes.includes(part));
|
||||
|
||||
if (hasValidPrefix) {
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
// Return missing icon fallback - use missing texture
|
||||
return 'missing-texture';
|
||||
}
|
||||
|
||||
// Get item icon as HTML element
|
||||
getItemIconElement(iconClass, size = '32px') {
|
||||
const icon = this.getIcon(iconClass);
|
||||
|
||||
if (icon === 'missing-texture') {
|
||||
return `<img src="assets/textures/missing-texture.png" style="width: ${size}; height: ${size}; object-fit: contain;" alt="Missing texture">`;
|
||||
}
|
||||
|
||||
return `<i class="fas ${icon}" style="font-size: ${size};"></i>`;
|
||||
}
|
||||
|
||||
// Preload common textures
|
||||
async preloadTextures() {
|
||||
const commonTextures = [
|
||||
'ship_fighter',
|
||||
'ship_cruiser',
|
||||
'room_command_center',
|
||||
'room_power_core',
|
||||
'item_weapon',
|
||||
'item_shield'
|
||||
];
|
||||
|
||||
const loadPromises = commonTextures.map(textureId => {
|
||||
const url = `assets/textures/${textureId}.png`;
|
||||
return this.loadTexture(textureId, url);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(loadPromises);
|
||||
console.log('Common textures preloaded');
|
||||
} catch (error) {
|
||||
console.warn('Some textures failed to preload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up unused textures
|
||||
cleanup() {
|
||||
// Keep only essential textures in memory
|
||||
const essentialTextures = ['missing'];
|
||||
for (const [textureId, texture] of this.textures) {
|
||||
if (!essentialTextures.includes(textureId)) {
|
||||
this.textures.delete(textureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
570
Client/js/data/GameData.js
Normal file
@ -0,0 +1,570 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Game Data
|
||||
* Static game data, constants, and configuration
|
||||
*/
|
||||
|
||||
// Game configuration
|
||||
const GAME_CONFIG = {
|
||||
version: '1.0.0',
|
||||
name: 'Galaxy Strike Online',
|
||||
maxLevel: 100,
|
||||
saveInterval: 30000, // 30 seconds
|
||||
notificationDuration: 3000,
|
||||
maxNotifications: 5
|
||||
};
|
||||
|
||||
// Player defaults
|
||||
const PLAYER_DEFAULTS = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
skillPoints: 0,
|
||||
credits: 1000,
|
||||
gems: 10,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Experience requirements
|
||||
const EXPERIENCE_TABLE = [];
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
|
||||
}
|
||||
|
||||
// Item rarities with colors and multipliers
|
||||
const ITEM_RARITIES = {
|
||||
common: {
|
||||
name: 'Common',
|
||||
color: '#888888',
|
||||
multiplier: 1.0,
|
||||
dropChance: 0.60
|
||||
},
|
||||
uncommon: {
|
||||
name: 'Uncommon',
|
||||
color: '#00ff00',
|
||||
multiplier: 1.2,
|
||||
dropChance: 0.25
|
||||
},
|
||||
rare: {
|
||||
name: 'Rare',
|
||||
color: '#0088ff',
|
||||
multiplier: 1.5,
|
||||
dropChance: 0.10
|
||||
},
|
||||
epic: {
|
||||
name: 'Epic',
|
||||
color: '#8833ff',
|
||||
multiplier: 2.0,
|
||||
dropChance: 0.04
|
||||
},
|
||||
legendary: {
|
||||
name: 'Legendary',
|
||||
color: '#ff8800',
|
||||
multiplier: 3.0,
|
||||
dropChance: 0.01
|
||||
}
|
||||
};
|
||||
|
||||
// Enemy types and stats
|
||||
const ENEMY_TEMPLATES = {
|
||||
space_pirate: {
|
||||
name: 'Space Pirate',
|
||||
health: 25,
|
||||
attack: 10,
|
||||
defense: 3,
|
||||
speed: 8,
|
||||
experience: 15,
|
||||
credits: 12,
|
||||
rarity: 'common'
|
||||
},
|
||||
alien_guardian: {
|
||||
name: 'Alien Guardian',
|
||||
health: 50,
|
||||
attack: 8,
|
||||
defense: 5,
|
||||
speed: 6,
|
||||
experience: 25,
|
||||
credits: 15,
|
||||
rarity: 'common'
|
||||
},
|
||||
mining_drone: {
|
||||
name: 'Mining Drone',
|
||||
health: 20,
|
||||
attack: 8,
|
||||
defense: 3,
|
||||
speed: 5,
|
||||
experience: 12,
|
||||
credits: 8,
|
||||
rarity: 'common'
|
||||
},
|
||||
security_drone: {
|
||||
name: 'Security Drone',
|
||||
health: 35,
|
||||
attack: 14,
|
||||
defense: 4,
|
||||
speed: 10,
|
||||
experience: 22,
|
||||
credits: 15,
|
||||
rarity: 'uncommon'
|
||||
},
|
||||
pirate_captain: {
|
||||
name: 'Pirate Captain',
|
||||
health: 40,
|
||||
attack: 15,
|
||||
defense: 6,
|
||||
speed: 12,
|
||||
experience: 30,
|
||||
credits: 20,
|
||||
rarity: 'uncommon'
|
||||
},
|
||||
crystal_golem: {
|
||||
name: 'Crystal Golem',
|
||||
health: 80,
|
||||
attack: 6,
|
||||
defense: 10,
|
||||
speed: 4,
|
||||
experience: 35,
|
||||
credits: 25,
|
||||
rarity: 'rare'
|
||||
},
|
||||
corrupted_ai: {
|
||||
name: 'Corrupted AI',
|
||||
health: 60,
|
||||
attack: 20,
|
||||
defense: 2,
|
||||
speed: 15,
|
||||
experience: 40,
|
||||
credits: 30,
|
||||
rarity: 'rare'
|
||||
},
|
||||
energy_being: {
|
||||
name: 'Energy Being',
|
||||
health: 55,
|
||||
attack: 22,
|
||||
defense: 3,
|
||||
speed: 18,
|
||||
experience: 45,
|
||||
credits: 35,
|
||||
rarity: 'epic'
|
||||
},
|
||||
quantum_entity: {
|
||||
name: 'Quantum Entity',
|
||||
health: 70,
|
||||
attack: 35,
|
||||
defense: 5,
|
||||
speed: 20,
|
||||
experience: 60,
|
||||
credits: 50,
|
||||
rarity: 'legendary'
|
||||
}
|
||||
};
|
||||
|
||||
// Dungeon configurations
|
||||
const DUNGEON_CONFIGS = {
|
||||
alien_ruins: {
|
||||
name: 'Alien Ruins',
|
||||
description: 'Ancient alien structures filled with mysterious technology',
|
||||
difficulty: 'medium',
|
||||
minLevel: 3,
|
||||
roomCount: [5, 8],
|
||||
enemyTypes: ['alien_guardian', 'ancient_drone', 'crystal_golem'],
|
||||
rewardMultiplier: 1.2,
|
||||
energyCost: 20
|
||||
},
|
||||
pirate_lair: {
|
||||
name: 'Pirate Lair',
|
||||
description: 'Dangerous pirate hideouts with valuable loot',
|
||||
difficulty: 'easy',
|
||||
minLevel: 1,
|
||||
roomCount: [4, 6],
|
||||
enemyTypes: ['space_pirate', 'pirate_captain', 'defense_turret'],
|
||||
rewardMultiplier: 1.0,
|
||||
energyCost: 15
|
||||
},
|
||||
corrupted_vault: {
|
||||
name: 'Corrupted AI Vault',
|
||||
description: 'Malfunctioning AI facilities with corrupted security',
|
||||
difficulty: 'hard',
|
||||
minLevel: 5,
|
||||
roomCount: [6, 9],
|
||||
enemyTypes: ['security_drone', 'corrupted_ai', 'virus_program'],
|
||||
rewardMultiplier: 1.5,
|
||||
energyCost: 25
|
||||
},
|
||||
asteroid_mine: {
|
||||
name: 'Asteroid Mine',
|
||||
description: 'Abandoned mining facilities in asteroid fields',
|
||||
difficulty: 'easy',
|
||||
minLevel: 2,
|
||||
roomCount: [4, 7],
|
||||
enemyTypes: ['mining_drone', 'rock_creature', 'explosive_asteroid'],
|
||||
rewardMultiplier: 0.8,
|
||||
energyCost: 10
|
||||
},
|
||||
nebula_anomaly: {
|
||||
name: 'Nebula Anomaly',
|
||||
description: 'Strange energy anomalies in deep space',
|
||||
difficulty: 'extreme',
|
||||
minLevel: 8,
|
||||
roomCount: [7, 10],
|
||||
enemyTypes: ['energy_being', 'phase_shifter', 'quantum_entity'],
|
||||
rewardMultiplier: 2.0,
|
||||
energyCost: 30
|
||||
}
|
||||
};
|
||||
|
||||
// Skill definitions
|
||||
const SKILL_DEFINITIONS = {
|
||||
combat: {
|
||||
weapons_mastery: {
|
||||
name: 'Weapons Mastery',
|
||||
description: 'Increases weapon damage and critical chance',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
attack: 2,
|
||||
criticalChance: 0.01
|
||||
},
|
||||
icon: 'fa-sword'
|
||||
},
|
||||
shield_techniques: {
|
||||
name: 'Shield Techniques',
|
||||
description: 'Improves defense and energy efficiency',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
defense: 2,
|
||||
maxEnergy: 5
|
||||
},
|
||||
icon: 'fa-shield-alt'
|
||||
},
|
||||
piloting: {
|
||||
name: 'Piloting',
|
||||
description: 'Enhances speed and evasion',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
speed: 2,
|
||||
criticalChance: 0.005
|
||||
},
|
||||
icon: 'fa-rocket'
|
||||
}
|
||||
},
|
||||
science: {
|
||||
energy_manipulation: {
|
||||
name: 'Energy Manipulation',
|
||||
description: 'Better energy control and regeneration',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
maxEnergy: 10,
|
||||
energyRegeneration: 0.1
|
||||
},
|
||||
icon: 'fa-bolt'
|
||||
},
|
||||
alien_technology: {
|
||||
name: 'Alien Technology',
|
||||
description: 'Understanding and using alien artifacts',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 150,
|
||||
effects: {
|
||||
findRarity: 0.05,
|
||||
itemValue: 0.1
|
||||
},
|
||||
icon: 'fa-atom'
|
||||
}
|
||||
},
|
||||
crafting: {
|
||||
weapon_crafting: {
|
||||
name: 'Weapon Crafting',
|
||||
description: 'Create and upgrade weapons',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.1,
|
||||
weaponStats: 0.05
|
||||
},
|
||||
icon: 'fa-hammer'
|
||||
},
|
||||
armor_forging: {
|
||||
name: 'Armor Forging',
|
||||
description: 'Forge protective armor and shields',
|
||||
maxLevel: 10,
|
||||
experiencePerLevel: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.1,
|
||||
armorStats: 0.05
|
||||
},
|
||||
icon: 'fa-anvil'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Shop items
|
||||
const SHOP_ITEMS = {
|
||||
ships: [
|
||||
{
|
||||
id: 'fighter_mk1',
|
||||
name: 'Fighter Mk. I',
|
||||
type: 'ship',
|
||||
rarity: 'common',
|
||||
price: 5000,
|
||||
currency: 'credits',
|
||||
description: 'Fast and agile fighter ship',
|
||||
stats: { attack: 15, speed: 20, defense: 8 }
|
||||
},
|
||||
{
|
||||
id: 'cruiser_mk1',
|
||||
name: 'Cruiser Mk. I',
|
||||
type: 'ship',
|
||||
rarity: 'uncommon',
|
||||
price: 15000,
|
||||
currency: 'credits',
|
||||
description: 'Well-balanced cruiser for combat',
|
||||
stats: { attack: 20, speed: 10, defense: 15 }
|
||||
}
|
||||
],
|
||||
upgrades: [
|
||||
{
|
||||
id: 'weapon_upgrade_1',
|
||||
name: 'Weapon Upgrade I',
|
||||
type: 'upgrade',
|
||||
rarity: 'common',
|
||||
price: 500,
|
||||
currency: 'credits',
|
||||
description: 'Increases weapon damage by 10%',
|
||||
effect: { attackMultiplier: 1.1 }
|
||||
},
|
||||
{
|
||||
id: 'shield_upgrade_1',
|
||||
name: 'Shield Upgrade I',
|
||||
type: 'upgrade',
|
||||
rarity: 'common',
|
||||
price: 400,
|
||||
currency: 'credits',
|
||||
description: 'Increases defense by 5 points',
|
||||
effect: { defense: 5 }
|
||||
}
|
||||
],
|
||||
cosmetics: [
|
||||
{
|
||||
id: 'blue_paint',
|
||||
name: 'Blue Paint Job',
|
||||
type: 'cosmetic',
|
||||
rarity: 'common',
|
||||
price: 100,
|
||||
currency: 'gems',
|
||||
description: 'Custom blue paint for your ship'
|
||||
},
|
||||
{
|
||||
id: 'golden_trim',
|
||||
name: 'Golden Trim',
|
||||
type: 'cosmetic',
|
||||
rarity: 'rare',
|
||||
price: 500,
|
||||
currency: 'gems',
|
||||
description: 'Luxurious golden trim for your ship'
|
||||
}
|
||||
],
|
||||
consumables: [
|
||||
{
|
||||
id: 'mega_health_kit',
|
||||
name: 'Mega Health Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'uncommon',
|
||||
price: 50,
|
||||
currency: 'credits',
|
||||
description: 'Restores full health',
|
||||
effect: { heal: 999 }
|
||||
},
|
||||
{
|
||||
id: 'energy_boost',
|
||||
name: 'Energy Boost',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
price: 25,
|
||||
currency: 'credits',
|
||||
description: 'Restores 50 energy',
|
||||
effect: { energy: 50 }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Starter equipment for new players
|
||||
const STARTER_EQUIPMENT = {
|
||||
starter_blaster: {
|
||||
id: 'starter_blaster',
|
||||
name: 'Common Blaster',
|
||||
type: 'weapon',
|
||||
rarity: 'common',
|
||||
description: 'A reliable basic blaster for new pilots',
|
||||
stats: { attack: 5, criticalChance: 0.02 },
|
||||
equipable: true,
|
||||
slot: 'weapon',
|
||||
value: 100,
|
||||
stackable: false
|
||||
},
|
||||
basic_armor: {
|
||||
id: 'basic_armor',
|
||||
name: 'Basic Armor',
|
||||
type: 'armor',
|
||||
rarity: 'common',
|
||||
description: 'Standard issue armor for basic protection',
|
||||
stats: { defense: 3, health: 10 },
|
||||
equipable: true,
|
||||
slot: 'armor',
|
||||
value: 150,
|
||||
stackable: false
|
||||
}
|
||||
};
|
||||
|
||||
// Achievement definitions
|
||||
const ACHIEVEMENTS = {
|
||||
first_victory: {
|
||||
name: 'First Victory',
|
||||
description: 'Win your first dungeon',
|
||||
requirement: { dungeonsCompleted: 1 },
|
||||
reward: { gems: 10, experience: 100 },
|
||||
icon: 'fa-trophy'
|
||||
},
|
||||
dungeon_master: {
|
||||
name: 'Dungeon Master',
|
||||
description: 'Complete 50 dungeons',
|
||||
requirement: { dungeonsCompleted: 50 },
|
||||
reward: { gems: 100, experience: 1000 },
|
||||
icon: 'fa-dungeon'
|
||||
},
|
||||
level_master: {
|
||||
name: 'Level Master',
|
||||
description: 'Reach level 50',
|
||||
requirement: { level: 50 },
|
||||
reward: { gems: 200, experience: 5000 },
|
||||
icon: 'fa-level-up-alt'
|
||||
},
|
||||
wealthy_commander: {
|
||||
name: 'Wealthy Commander',
|
||||
description: 'Accumulate 1,000,000 credits',
|
||||
requirement: { credits: 1000000 },
|
||||
reward: { gems: 150, experience: 2000 },
|
||||
icon: 'fa-coins'
|
||||
},
|
||||
skill_expert: {
|
||||
name: 'Skill Expert',
|
||||
description: 'Max out any skill',
|
||||
requirement: { maxSkillLevel: 10 },
|
||||
reward: { gems: 75, experience: 1500 },
|
||||
icon: 'fa-graduation-cap'
|
||||
}
|
||||
};
|
||||
|
||||
// Game messages and notifications
|
||||
const GAME_MESSAGES = {
|
||||
welcome: 'Welcome to Galaxy Strike Online, Commander!',
|
||||
levelUp: 'Level Up! You are now level {level}!',
|
||||
questCompleted: 'Quest completed: {questName}!',
|
||||
dungeonCompleted: 'Dungeon completed! Time: {time}',
|
||||
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
|
||||
insufficientCredits: 'Not enough credits!',
|
||||
insufficientGems: 'Not enough gems!',
|
||||
insufficientEnergy: 'Not enough energy!',
|
||||
inventoryFull: 'Inventory is full!',
|
||||
skillPointsAvailable: 'You have {points} skill points available!',
|
||||
dailyReward: 'Daily reward claimed! Day {day}',
|
||||
offlineRewards: 'Welcome back! You were offline for {time}'
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
const GameUtils = {
|
||||
// Get random item from array
|
||||
getRandomItem(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
},
|
||||
|
||||
// Get random integer between min and max (inclusive)
|
||||
getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
|
||||
// Get random float between min and max
|
||||
getRandomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
},
|
||||
|
||||
// Check if chance succeeds
|
||||
checkChance(chance) {
|
||||
return Math.random() < chance;
|
||||
},
|
||||
|
||||
// Format large numbers with suffixes
|
||||
formatNumber(num) {
|
||||
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
||||
return Math.floor(num).toString();
|
||||
},
|
||||
|
||||
// Format time in milliseconds to readable string
|
||||
formatTime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ${hours % 24}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||
return `${seconds}s`;
|
||||
},
|
||||
|
||||
// Calculate experience needed for level
|
||||
getExperienceForLevel(level) {
|
||||
return EXPERIENCE_TABLE[level] || 0;
|
||||
},
|
||||
|
||||
// Get item rarity by chance
|
||||
getItemRarity() {
|
||||
const roll = Math.random();
|
||||
let cumulative = 0;
|
||||
|
||||
for (const [rarity, data] of Object.entries(ITEM_RARITIES)) {
|
||||
cumulative += data.dropChance;
|
||||
if (roll <= cumulative) {
|
||||
return rarity;
|
||||
}
|
||||
}
|
||||
|
||||
return 'common';
|
||||
},
|
||||
|
||||
// Deep clone object
|
||||
deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
// Generate unique ID
|
||||
generateId() {
|
||||
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
GAME_CONFIG,
|
||||
PLAYER_DEFAULTS,
|
||||
EXPERIENCE_TABLE,
|
||||
ITEM_RARITIES,
|
||||
ENEMY_TEMPLATES,
|
||||
DUNGEON_CONFIGS,
|
||||
SKILL_DEFINITIONS,
|
||||
SHOP_ITEMS,
|
||||
ACHIEVEMENTS,
|
||||
GAME_MESSAGES,
|
||||
GameUtils
|
||||
};
|
||||
}
|
||||
716
Client/js/main.js
Normal file
@ -0,0 +1,716 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Main Entry Point
|
||||
* Initializes and starts the game
|
||||
*/
|
||||
|
||||
console.log('[MAIN] main.js script loaded');
|
||||
|
||||
// Wait for DOM to be loaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.domContentLoaded', {
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent,
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('DOM elements found', {
|
||||
loadingIndicator: !!loadingIndicator,
|
||||
loadingStatus: !!loadingStatus
|
||||
});
|
||||
|
||||
try {
|
||||
// Start debug logging
|
||||
if (window.debugLogger) {
|
||||
window.debugLogger.startStep('domLoad');
|
||||
window.debugLogger.logStep('DOM loaded, starting initialization');
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
if (loadingIndicator) loadingIndicator.classList.remove('hidden');
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Initializing application...';
|
||||
loadingStatus.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading indicator shown');
|
||||
|
||||
// Initialize title bar controls immediately (don't wait for DOMContentLoaded)
|
||||
console.log('[MAIN] Initializing title bar controls immediately');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Initializing title bar controls');
|
||||
initializeTitleBar();
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (debugLogger) debugLogger.startStep('main.secondDOMContentLoaded', {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
window.debugLogger.startStep('domLoad');
|
||||
window.debugLogger.logStep('DOM loaded, starting initialization');
|
||||
|
||||
// Auto-start local server for singleplayer mode
|
||||
console.log('[MAIN] Checking local server status...');
|
||||
if (window.localServerManager) {
|
||||
try {
|
||||
const serverResult = await window.localServerManager.autoStartIfSingleplayer();
|
||||
if (serverResult.success) {
|
||||
console.log('[MAIN] Local server started successfully:', serverResult);
|
||||
if (debugLogger) debugLogger.logStep('Local server auto-started', {
|
||||
port: serverResult.port,
|
||||
url: serverResult.url
|
||||
});
|
||||
} else {
|
||||
console.log('[MAIN] Local server not started:', serverResult.reason || serverResult.error);
|
||||
if (debugLogger) debugLogger.logStep('Local server not started', {
|
||||
reason: serverResult.reason || serverResult.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MAIN] Error starting local server:', error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, 'Local server startup');
|
||||
}
|
||||
} else {
|
||||
console.warn('[MAIN] LocalServerManager not available');
|
||||
}
|
||||
|
||||
// Title bar is already initialized, just log it
|
||||
console.log('[MAIN] DOM loaded - title bar should already be working');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('DOM loaded - title bar should be working');
|
||||
|
||||
// Show main menu instead of directly loading game
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Ready';
|
||||
loadingStatus.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading status updated to Ready');
|
||||
|
||||
// Hide loading screen and show main menu
|
||||
const loadingScreen = document.getElementById('loadingScreen');
|
||||
const mainMenu = document.getElementById('mainMenu');
|
||||
|
||||
if (loadingScreen) loadingScreen.classList.add('hidden');
|
||||
if (mainMenu) mainMenu.classList.remove('hidden');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading screen hidden, main menu shown', {
|
||||
loadingScreenFound: !!loadingScreen,
|
||||
mainMenuFound: !!mainMenu,
|
||||
liveMainMenuReady: !!window.liveMainMenu
|
||||
});
|
||||
|
||||
// The LiveMainMenu will initialize itself and handle authentication
|
||||
console.log('[MAIN] Main menu displayed - LiveMainMenu will handle authentication and server browsing');
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.secondDOMContentLoaded', {
|
||||
success: true,
|
||||
mainMenuDisplayed: !!mainMenu
|
||||
});
|
||||
});
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.domContentLoaded', {
|
||||
success: true,
|
||||
titleBarInitialized: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game:', error);
|
||||
|
||||
if (debugLogger) debugLogger.errorEvent('main.domContentLoaded', error, {
|
||||
phase: 'initialization',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (window.debugLogger) {
|
||||
window.debugLogger.log('CRITICAL ERROR: Initialization failed', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (loadingIndicator) loadingIndicator.classList.add('error');
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Failed to load application';
|
||||
loadingStatus.classList.add('error');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Error state displayed');
|
||||
|
||||
const logger = window.logger;
|
||||
if (logger) {
|
||||
await logger.errorEvent(error, 'Main.js Initialization');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.domContentLoaded', {
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize title bar controls
|
||||
function initializeTitleBar() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.initializeTitleBar', {
|
||||
timestamp: new Date().toISOString(),
|
||||
electronAPIAvailable: !!window.electronAPI
|
||||
});
|
||||
|
||||
console.log('[TITLE BAR] Starting title bar initialization');
|
||||
|
||||
// Wait for both electronAPI and DOM elements to be available
|
||||
const checkReady = () => {
|
||||
const hasElectronAPI = !!window.electronAPI;
|
||||
const hasMinimizeBtn = !!document.getElementById('minimizeBtn');
|
||||
const hasCloseBtn = !!document.getElementById('closeBtn');
|
||||
const hasFullscreenBtn = !!document.getElementById('fullscreenBtn');
|
||||
|
||||
const readyState = {
|
||||
hasElectronAPI,
|
||||
hasMinimizeBtn,
|
||||
hasCloseBtn,
|
||||
hasFullscreenBtn
|
||||
};
|
||||
|
||||
console.log(`[TITLE BAR] electronAPI: ${hasElectronAPI}, minimizeBtn: ${hasMinimizeBtn}, closeBtn: ${hasCloseBtn}, fullscreenBtn: ${hasFullscreenBtn}`);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Title bar readiness check', readyState);
|
||||
|
||||
if (hasElectronAPI && hasMinimizeBtn && hasCloseBtn && hasFullscreenBtn) {
|
||||
console.log('[TITLE BAR] All elements ready, setting up events');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('All title bar elements ready, setting up events');
|
||||
setupTitleBarEvents();
|
||||
|
||||
// Hide the "Initializing application..." text since title bar is now working
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
if (loadingStatus && loadingStatus.textContent === 'Initializing application...') {
|
||||
console.log('[TITLE BAR] Hiding initializing text');
|
||||
loadingStatus.classList.add('hidden');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Hiding initializing text');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.initializeTitleBar', {
|
||||
success: true,
|
||||
allElementsReady: true
|
||||
});
|
||||
} else {
|
||||
if (debugLogger) debugLogger.logStep('Not all elements ready, retrying in 50ms', {
|
||||
missingElements: Object.keys(readyState).filter(key => !readyState[key])
|
||||
});
|
||||
setTimeout(checkReady, 50);
|
||||
}
|
||||
};
|
||||
|
||||
checkReady();
|
||||
}
|
||||
|
||||
function setupTitleBarEvents() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.setupTitleBarEvents');
|
||||
|
||||
const minimizeBtn = document.getElementById('minimizeBtn');
|
||||
const closeBtn = document.getElementById('closeBtn');
|
||||
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
||||
|
||||
console.log('[TITLE BAR] Setting up event listeners');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Title bar buttons found', {
|
||||
minimizeBtn: !!minimizeBtn,
|
||||
closeBtn: !!closeBtn,
|
||||
fullscreenBtn: !!fullscreenBtn
|
||||
});
|
||||
|
||||
let eventsSetup = 0;
|
||||
|
||||
if (minimizeBtn) {
|
||||
console.log('[TITLE BAR] Adding minimize button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up minimize button listener');
|
||||
minimizeBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Minimize button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar minimize button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.minimize) {
|
||||
window.electronAPI.minimize();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Window minimized via electronAPI');
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when minimize clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for minimize');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Minimize button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Minimize button not found');
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
console.log('[TITLE BAR] Adding close button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up close button listener');
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Close button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar close button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
// ... rest of the code remains the same ...
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.closeWindow) {
|
||||
window.electronAPI.closeWindow();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Window closed via electronAPI');
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when close clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for close');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Close button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Close button not found');
|
||||
}
|
||||
|
||||
if (fullscreenBtn) {
|
||||
console.log('[TITLE BAR] Adding fullscreen button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up fullscreen button listener');
|
||||
fullscreenBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Fullscreen button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar fullscreen button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.toggleFullscreen) {
|
||||
window.electronAPI.toggleFullscreen();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen toggled via electronAPI');
|
||||
|
||||
// Toggle fullscreen class on body
|
||||
document.body.classList.toggle('fullscreen');
|
||||
document.body.classList.toggle('fullscreen');
|
||||
// Update icon
|
||||
const icon = fullscreenBtn.querySelector('i');
|
||||
if (document.body.classList.contains('fullscreen')) {
|
||||
icon.className = 'fas fa-compress';
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen mode activated, icon changed to compress');
|
||||
} else {
|
||||
icon.className = 'fas fa-expand';
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen mode deactivated, icon changed to expand');
|
||||
}
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when fullscreen clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for fullscreen');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Fullscreen button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Fullscreen button not found');
|
||||
}
|
||||
|
||||
console.log('[TITLE BAR] Event listeners setup complete');
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.setupTitleBarEvents', {
|
||||
eventsSetup: eventsSetup,
|
||||
totalExpectedEvents: 3
|
||||
});
|
||||
}
|
||||
|
||||
// Global utility functions for onclick handlers
|
||||
window.game = null;
|
||||
|
||||
// Error handling
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('Game error:', event.error);
|
||||
if (window.game) {
|
||||
window.game.showNotification('An error occurred. Please refresh the page.', 'error', 5000);
|
||||
}
|
||||
});
|
||||
|
||||
// Shutdown handler for debug logging
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
if (window.debugLogger) {
|
||||
try {
|
||||
await window.debugLogger.shutdown();
|
||||
} catch (error) {
|
||||
console.error('[MAIN] Failed to shutdown debug logger:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Performance monitoring
|
||||
if (window.performance && window.performance.memory) {
|
||||
setInterval(() => {
|
||||
if (window.game && window.game.isRunning) {
|
||||
const stats = window.game.getPerformanceStats();
|
||||
if (stats.memory && stats.memory.used / stats.memory.total > 0.9) {
|
||||
console.warn('High memory usage detected:', stats.memory);
|
||||
}
|
||||
}
|
||||
}, 30000); // Check every 30 seconds
|
||||
}
|
||||
|
||||
// Global console functions
|
||||
function toggleConsole() {
|
||||
console.log('[DEBUG] toggleConsole called');
|
||||
const consoleWindow = document.getElementById('consoleWindow');
|
||||
const consoleInput = document.getElementById('consoleInput');
|
||||
|
||||
console.log('[DEBUG] consoleWindow element:', consoleWindow);
|
||||
console.log('[DEBUG] consoleInput element:', consoleInput);
|
||||
|
||||
if (!consoleWindow) {
|
||||
console.error('[DEBUG] consoleWindow element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (consoleWindow.style.display === 'flex') {
|
||||
consoleWindow.style.display = 'none';
|
||||
console.log('[DEBUG] Console hidden');
|
||||
} else {
|
||||
consoleWindow.style.display = 'flex';
|
||||
console.log('[DEBUG] Console shown');
|
||||
if (consoleInput) {
|
||||
consoleInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleConsoleInput(event) {
|
||||
if (event.key === 'Enter') {
|
||||
const input = event.target;
|
||||
const command = input.value.trim();
|
||||
|
||||
if (command) {
|
||||
executeConsoleCommand(command);
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeConsoleCommand(command) {
|
||||
const output = document.getElementById('consoleOutput');
|
||||
const commandLine = document.createElement('div');
|
||||
commandLine.className = 'console-line';
|
||||
commandLine.textContent = `> ${command}`;
|
||||
output.appendChild(commandLine);
|
||||
|
||||
// Log command to file and browser console
|
||||
console.log(`[CONSOLE] Command: ${command}`);
|
||||
if (window.logger) {
|
||||
window.logger.playerAction('Console Command', { command: command });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = processCommand(command);
|
||||
const resultLine = document.createElement('div');
|
||||
resultLine.className = `console-line ${result.type || 'success'}`;
|
||||
// Convert line breaks to HTML for proper rendering
|
||||
resultLine.innerHTML = result.message.replace(/\n/g, '<br>');
|
||||
output.appendChild(resultLine);
|
||||
|
||||
// Log result to file and browser console
|
||||
const consoleMethod = result.type === 'error' ? console.error :
|
||||
result.type === 'info' ? console.info : console.log;
|
||||
consoleMethod(`[CONSOLE] Result (${result.type}): ${result.message.replace(/\n/g, ' ')}`);
|
||||
|
||||
if (window.logger) {
|
||||
window.logger.playerAction('Console Result', {
|
||||
command: command,
|
||||
result: result.type,
|
||||
message: result.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorLine = document.createElement('div');
|
||||
errorLine.className = 'console-line console-error';
|
||||
errorLine.textContent = `Error: ${error.message}`;
|
||||
output.appendChild(errorLine);
|
||||
|
||||
// Log error to file and browser console
|
||||
console.error(`[CONSOLE] Error: ${error.message}`);
|
||||
if (window.logger) {
|
||||
window.logger.errorEvent(error, 'Console Command', { command: command });
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function processCommand(command) {
|
||||
const parts = command.split(' ');
|
||||
const cmd = parts[0].toLowerCase();
|
||||
const args = parts.slice(1);
|
||||
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Available commands:\nhelp - Show this help message\nclear - Clear console output\ncoins <amount> - Add coins to player (e.g., "coins 1000")\ngems <amount> - Add gems to player (e.g., "gems 100")\nresearch <amount> - Add research points (e.g., "research 500")\ncraftingxp <amount> - Add crafting experience (e.g., "craftingxp 200")\ngiveitem <item_id> <quantity> - Add item to inventory (e.g., "giveitem iron_ore 10")\nhealth <amount> - Set current ship health (e.g., "health 150")\nlevel <level> - Set current ship level (e.g., "level 5")\nunlock <ship_id> - Unlock a ship (e.g., "unlock heavy_fighter")\nstats - Show current player stats\nships - List all ships\ncurrent - Show current ship info`
|
||||
};
|
||||
|
||||
case 'clear':
|
||||
const output = document.getElementById('consoleOutput');
|
||||
output.innerHTML = '';
|
||||
return { type: 'success', message: 'Console cleared' };
|
||||
|
||||
case 'coins':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: coins <amount>' };
|
||||
}
|
||||
const amount = parseInt(args[0]);
|
||||
if (isNaN(amount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.addCredits(amount, 'console');
|
||||
window.game.systems.economy.updateUI();
|
||||
return { type: 'success', message: `Added ${amount} credits` };
|
||||
}
|
||||
return { type: 'error', message: 'Economy system not available' };
|
||||
|
||||
case 'gems':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: gems <amount>' };
|
||||
}
|
||||
const gemAmount = parseInt(args[0]);
|
||||
if (isNaN(gemAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.addGems(gemAmount, 'console');
|
||||
window.game.systems.economy.updateUI();
|
||||
return { type: 'success', message: `Added ${gemAmount} gems` };
|
||||
}
|
||||
return { type: 'error', message: 'Economy system not available' };
|
||||
|
||||
case 'research':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: research <amount>' };
|
||||
}
|
||||
const researchAmount = parseInt(args[0]);
|
||||
if (isNaN(researchAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
const currentSkillPoints = window.game.systems.player.stats.skillPoints || 0;
|
||||
window.game.systems.player.stats.skillPoints = currentSkillPoints + researchAmount;
|
||||
window.game.systems.player.updateUI();
|
||||
|
||||
// Also update skill system UI
|
||||
if (window.game.systems.skillSystem) {
|
||||
window.game.systems.skillSystem.updateUI();
|
||||
}
|
||||
|
||||
return { type: 'success', message: `Added ${researchAmount} skill points (Total: ${window.game.systems.player.stats.skillPoints})` };
|
||||
}
|
||||
return { type: 'error', message: 'Player system not available' };
|
||||
|
||||
case 'craftingxp':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: craftingxp <amount>' };
|
||||
}
|
||||
const craftingXpAmount = parseInt(args[0]);
|
||||
if (isNaN(craftingXpAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.skillSystem) {
|
||||
window.game.systems.skillSystem.awardCraftingExperience(craftingXpAmount);
|
||||
const currentLevel = window.game.systems.skillSystem.getSkillLevel('crafting');
|
||||
const currentExp = window.game.systems.skillSystem.getSkillExperience('crafting');
|
||||
return { type: 'success', message: `Added ${craftingXpAmount} crafting experience (Level: ${currentLevel}, XP: ${currentExp})` };
|
||||
}
|
||||
return { type: 'error', message: 'Skill system not available' };
|
||||
|
||||
case 'giveitem':
|
||||
if (args.length < 2) {
|
||||
return { type: 'error', message: 'Usage: giveitem <item_id> <quantity>' };
|
||||
}
|
||||
const itemId = args[0];
|
||||
const quantity = parseInt(args[1]);
|
||||
if (isNaN(quantity)) {
|
||||
return { type: 'error', message: 'Invalid quantity' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.inventory) {
|
||||
window.game.systems.inventory.addItem(itemId, quantity);
|
||||
window.game.systems.ui.updateInventory();
|
||||
return { type: 'success', message: `Added ${quantity}x ${itemId} to inventory` };
|
||||
}
|
||||
return { type: 'error', message: 'Inventory system not available' };
|
||||
|
||||
case 'health':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: health <amount>' };
|
||||
}
|
||||
const healthAmount = parseInt(args[0]);
|
||||
if (isNaN(healthAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
if (currentShip) {
|
||||
currentShip.health = Math.min(healthAmount, currentShip.maxHealth);
|
||||
window.game.systems.ship.updateCurrentShipDisplay();
|
||||
return { type: 'success', message: `Set ship health to ${currentShip.health}/${currentShip.maxHealth}` };
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'level':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: level <level>' };
|
||||
}
|
||||
const levelAmount = parseInt(args[0]);
|
||||
if (isNaN(levelAmount)) {
|
||||
return { type: 'error', message: 'Invalid level' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
window.game.systems.player.stats.level = levelAmount;
|
||||
window.game.systems.player.updateTitle();
|
||||
window.game.systems.player.updateUI();
|
||||
return { type: 'success', message: `Set player level to ${levelAmount}` };
|
||||
}
|
||||
return { type: 'error', message: 'Player system not available' };
|
||||
|
||||
case 'unlock':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: unlock <ship_id>' };
|
||||
}
|
||||
const shipId = args[0];
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const ship = window.game.systems.ship.ships.find(s => s.id === shipId);
|
||||
if (ship) {
|
||||
ship.status = 'inactive';
|
||||
window.game.systems.ship.renderShips();
|
||||
return { type: 'success', message: `Unlocked ship: ${ship.name}` };
|
||||
} else {
|
||||
return { type: 'error', message: `Ship not found: ${shipId}` };
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'stats':
|
||||
if (window.game && window.game.systems && window.game.systems.player && window.game.systems.economy) {
|
||||
const player = window.game.systems.player;
|
||||
const economy = window.game.systems.economy;
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Player Stats:\nLevel: ${player.stats.level}\nExperience: ${player.stats.experience}/${player.stats.experienceToNext}\nCredits: ${economy.credits}\nGems: ${economy.gems}\nTotal Kills: ${player.stats.totalKills}\nDungeons Cleared: ${player.stats.dungeonsCleared}\nTitle: ${player.info.title}`
|
||||
};
|
||||
}
|
||||
return { type: 'error', message: 'Player or Economy system not available' };
|
||||
|
||||
case 'ships':
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const ships = window.game.systems.ship.ships;
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
const shipList = ships.map(ship =>
|
||||
`- ${ship.name} (${ship.id}) - Level ${ship.level} - ${ship.status} - ${ship.rarity}${currentShip.id === ship.id ? ' [CURRENT]' : ''}`
|
||||
).join('\n');
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Available Ships:\n${shipList}`
|
||||
};
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'current':
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
if (currentShip) {
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Current Ship:
|
||||
- Name: ${currentShip.name}
|
||||
- Class: ${currentShip.class}
|
||||
- Level: ${currentShip.level}
|
||||
- Health: ${currentShip.health}/${currentShip.maxHealth}
|
||||
- Attack: ${currentShip.attack}
|
||||
- Defense: ${currentShip.defense}
|
||||
- Speed: ${currentShip.speed}
|
||||
- Rarity: ${currentShip.rarity}`
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
default:
|
||||
return { type: 'error', message: `Unknown command: ${cmd}. Type 'help' for available commands.` };
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut listener
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[DEBUG] DOMContentLoaded event fired for keyboard shortcuts');
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Log all key combinations for debugging
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
||||
console.log('[DEBUG] Key pressed:', {
|
||||
key: event.key,
|
||||
ctrlKey: event.ctrlKey,
|
||||
altKey: event.altKey,
|
||||
shiftKey: event.shiftKey,
|
||||
code: event.code
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Alt+Shift+C to toggle console
|
||||
if (event.ctrlKey && event.altKey && event.shiftKey && event.key === 'C') {
|
||||
console.log('[DEBUG] Ctrl+Alt+Shift+C detected!');
|
||||
event.preventDefault();
|
||||
|
||||
// Check if toggleConsole function exists
|
||||
if (typeof toggleConsole === 'function') {
|
||||
console.log('[DEBUG] toggleConsole function exists, calling it');
|
||||
toggleConsole();
|
||||
} else {
|
||||
console.error('[DEBUG] toggleConsole function not found!');
|
||||
}
|
||||
}
|
||||
|
||||
// Escape to close console
|
||||
if (event.key === 'Escape') {
|
||||
const consoleWindow = document.getElementById('consoleWindow');
|
||||
if (consoleWindow && consoleWindow.style.display === 'flex') {
|
||||
consoleWindow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize console output with welcome message
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const output = document.getElementById('consoleOutput');
|
||||
if (output) {
|
||||
const welcomeLine = document.createElement('div');
|
||||
welcomeLine.className = 'console-line console-info';
|
||||
welcomeLine.textContent = 'Developer Console ready. Type "help" for available commands.';
|
||||
output.appendChild(welcomeLine);
|
||||
}
|
||||
});
|
||||
2173
Client/js/systems/BaseSystem.js
Normal file
651
Client/js/systems/CraftingSystem.js
Normal file
@ -0,0 +1,651 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Crafting System
|
||||
* Handles item crafting, recipes, and crafting skill progression
|
||||
*/
|
||||
|
||||
class CraftingSystem extends BaseSystem {
|
||||
constructor(gameEngine) {
|
||||
super(gameEngine);
|
||||
|
||||
this.recipes = new Map();
|
||||
this.currentCategory = 'weapons';
|
||||
this.selectedRecipe = null;
|
||||
|
||||
this.initializeRecipes();
|
||||
}
|
||||
|
||||
initializeRecipes() {
|
||||
// Weapon Recipes
|
||||
this.addRecipe('basic_blaster', {
|
||||
name: 'Basic Blaster',
|
||||
category: 'weapons',
|
||||
description: 'A simple energy blaster for beginners',
|
||||
requirements: {
|
||||
weapon_crafting: 1,
|
||||
crafting: 1
|
||||
},
|
||||
materials: [
|
||||
{ id: 'iron_ore', quantity: 5 },
|
||||
{ id: 'energy_crystal', quantity: 2 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'basic_blaster', quantity: 1 }
|
||||
],
|
||||
experience: 10,
|
||||
craftingTime: 3000 // 3 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('enhanced_blaster', {
|
||||
name: 'Enhanced Blaster',
|
||||
category: 'weapons',
|
||||
description: 'An improved blaster with better damage output',
|
||||
requirements: {
|
||||
weapon_crafting: 3,
|
||||
crafting: 5
|
||||
},
|
||||
materials: [
|
||||
{ id: 'iron_ore', quantity: 10 },
|
||||
{ id: 'energy_crystal', quantity: 5 },
|
||||
{ id: 'copper_wire', quantity: 3 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'enhanced_blaster', quantity: 1 }
|
||||
],
|
||||
experience: 25,
|
||||
craftingTime: 5000 // 5 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('laser_sniper_rifle', {
|
||||
name: 'Laser Sniper Rifle',
|
||||
category: 'weapons',
|
||||
description: 'A long-range precision laser weapon',
|
||||
requirements: {
|
||||
weapon_crafting: 3,
|
||||
crafting: 5
|
||||
},
|
||||
materials: [
|
||||
{ id: 'advanced_circuitboard', quantity: 2 },
|
||||
{ id: 'energy_crystal', quantity: 8 },
|
||||
{ id: 'steel_plate', quantity: 5 },
|
||||
{ id: 'copper_wire', quantity: 4 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'laser_sniper_rifle', quantity: 1 }
|
||||
],
|
||||
experience: 40,
|
||||
craftingTime: 8000 // 8 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('plasma_cannon', {
|
||||
name: 'Plasma Cannon',
|
||||
category: 'weapons',
|
||||
description: 'A devastating plasma-based weapon',
|
||||
requirements: {
|
||||
weapon_crafting: 5,
|
||||
crafting: 7
|
||||
},
|
||||
materials: [
|
||||
{ id: 'advanced_components', quantity: 3 },
|
||||
{ id: 'energy_crystal', quantity: 12 },
|
||||
{ id: 'steel_plate', quantity: 8 },
|
||||
{ id: 'battery', quantity: 3 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'plasma_cannon', quantity: 1 }
|
||||
],
|
||||
experience: 60,
|
||||
craftingTime: 12000 // 12 seconds
|
||||
});
|
||||
|
||||
// Armor Recipes
|
||||
this.addRecipe('basic_armor', {
|
||||
name: 'Basic Armor',
|
||||
category: 'armor',
|
||||
description: 'Light armor providing basic protection',
|
||||
requirements: {
|
||||
armor_forging: 1,
|
||||
crafting: 1
|
||||
},
|
||||
materials: [
|
||||
{ id: 'iron_ore', quantity: 8 },
|
||||
{ id: 'leather', quantity: 3 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'basic_armor', quantity: 1 }
|
||||
],
|
||||
experience: 15,
|
||||
craftingTime: 4000 // 4 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('reinforced_armor', {
|
||||
name: 'Reinforced Armor',
|
||||
category: 'armor',
|
||||
description: 'Heavy armor with enhanced protection',
|
||||
requirements: {
|
||||
armor_forging: 4,
|
||||
crafting: 6
|
||||
},
|
||||
materials: [
|
||||
{ id: 'iron_ore', quantity: 15 },
|
||||
{ id: 'steel_plate', quantity: 5 },
|
||||
{ id: 'leather', quantity: 5 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'reinforced_armor', quantity: 1 }
|
||||
],
|
||||
experience: 35,
|
||||
craftingTime: 6000 // 6 seconds
|
||||
});
|
||||
|
||||
// Item Recipes
|
||||
this.addRecipe('health_kit', {
|
||||
name: 'Health Kit',
|
||||
category: 'items',
|
||||
description: 'A medical kit that restores health',
|
||||
requirements: {
|
||||
crafting: 2
|
||||
},
|
||||
materials: [
|
||||
{ id: 'herbs', quantity: 3 },
|
||||
{ id: 'bandages', quantity: 2 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'health_kit', quantity: 3 }
|
||||
],
|
||||
experience: 5,
|
||||
craftingTime: 2000 // 2 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('basic_circuit', {
|
||||
name: 'Basic Circuit',
|
||||
category: 'items',
|
||||
description: 'Create a basic electronic circuit',
|
||||
requirements: {
|
||||
crafting: 3
|
||||
},
|
||||
materials: [
|
||||
{ id: 'basic_circuitboard', quantity: 1 },
|
||||
{ id: 'copper_wire', quantity: 2 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'basic_circuit', quantity: 1 }
|
||||
],
|
||||
experience: 8,
|
||||
craftingTime: 2500 // 2.5 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('advanced_circuit', {
|
||||
name: 'Advanced Circuit',
|
||||
category: 'items',
|
||||
description: 'Create an advanced electronic circuit',
|
||||
requirements: {
|
||||
crafting: 5
|
||||
},
|
||||
materials: [
|
||||
{ id: 'advanced_circuitboard', quantity: 1 },
|
||||
{ id: 'energy_crystal', quantity: 2 },
|
||||
{ id: 'copper_wire', quantity: 3 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'advanced_circuit', quantity: 1 }
|
||||
],
|
||||
experience: 15,
|
||||
craftingTime: 4000 // 4 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('electronic_device', {
|
||||
name: 'Electronic Device',
|
||||
category: 'items',
|
||||
description: 'Create a complex electronic device',
|
||||
requirements: {
|
||||
crafting: 7
|
||||
},
|
||||
materials: [
|
||||
{ id: 'advanced_components', quantity: 1 },
|
||||
{ id: 'battery', quantity: 2 },
|
||||
{ id: 'common_circuitboard', quantity: 1 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'electronic_device', quantity: 1 }
|
||||
],
|
||||
experience: 20,
|
||||
craftingTime: 5000 // 5 seconds
|
||||
});
|
||||
|
||||
// Ship Component Recipes
|
||||
this.addRecipe('shield_generator', {
|
||||
name: 'Shield Generator',
|
||||
category: 'ships',
|
||||
description: 'A basic shield generator for ship protection',
|
||||
requirements: {
|
||||
engineering: 2,
|
||||
crafting: 4
|
||||
},
|
||||
materials: [
|
||||
{ id: 'energy_crystal', quantity: 8 },
|
||||
{ id: 'steel_plate', quantity: 5 },
|
||||
{ id: 'copper_wire', quantity: 4 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'shield_generator', quantity: 1 }
|
||||
],
|
||||
experience: 30,
|
||||
craftingTime: 8000 // 8 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('engine_upgrade', {
|
||||
name: 'Engine Upgrade',
|
||||
category: 'ships',
|
||||
description: 'An upgrade that improves ship engine performance',
|
||||
requirements: {
|
||||
engineering: 5,
|
||||
crafting: 7
|
||||
},
|
||||
materials: [
|
||||
{ id: 'steel_plate', quantity: 10 },
|
||||
{ id: 'energy_crystal', quantity: 6 },
|
||||
{ id: 'rare_metal', quantity: 2 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'engine_upgrade', quantity: 1 }
|
||||
],
|
||||
experience: 50,
|
||||
craftingTime: 10000 // 10 seconds
|
||||
});
|
||||
|
||||
this.addRecipe('quantum_computer', {
|
||||
name: 'Quantum Computer',
|
||||
category: 'ships',
|
||||
description: 'Advanced quantum computer for high-end ship systems',
|
||||
requirements: {
|
||||
engineering: 8,
|
||||
crafting: 10
|
||||
},
|
||||
materials: [
|
||||
{ id: 'advanced_components', quantity: 3 },
|
||||
{ id: 'energy_crystal', quantity: 8 },
|
||||
{ id: 'rare_metal', quantity: 4 },
|
||||
{ id: 'copper_wire', quantity: 6 }
|
||||
],
|
||||
results: [
|
||||
{ id: 'quantum_computer', quantity: 1 }
|
||||
],
|
||||
experience: 100,
|
||||
craftingTime: 15000 // 15 seconds
|
||||
});
|
||||
}
|
||||
|
||||
addRecipe(id, recipe) {
|
||||
recipe.id = id;
|
||||
recipe.unlocked = false;
|
||||
this.recipes.set(id, recipe);
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Check for newly unlocked recipes
|
||||
this.checkRecipeUnlocks();
|
||||
}
|
||||
|
||||
checkRecipeUnlocks() {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return;
|
||||
|
||||
for (const [id, recipe] of this.recipes) {
|
||||
if (!recipe.unlocked) {
|
||||
let canCraft = true;
|
||||
|
||||
// Check skill requirements
|
||||
if (recipe.requirements) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
||||
if (skillLevel < requiredLevel) {
|
||||
canCraft = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canCraft) {
|
||||
recipe.unlocked = true;
|
||||
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRecipesByCategory(category) {
|
||||
return Array.from(this.recipes.values())
|
||||
.filter(recipe => recipe.category === category);
|
||||
}
|
||||
|
||||
canCraftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe) return false;
|
||||
|
||||
// Check skill requirements
|
||||
if (recipe.requirements) {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return false;
|
||||
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
||||
if (skillLevel < requiredLevel) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check materials
|
||||
if (recipe.materials) {
|
||||
for (const material of recipe.materials) {
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!inventory || !inventory.hasItem(material.id, material.quantity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMissingMaterials(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe || !recipe.materials) return [];
|
||||
|
||||
const missing = [];
|
||||
const inventory = this.game.systems.inventory;
|
||||
|
||||
console.log(`[CRAFTING DEBUG] Checking materials for recipe: ${recipe.name}`);
|
||||
console.log(`[CRAFTING DEBUG] Inventory system:`, inventory);
|
||||
|
||||
for (const material of recipe.materials) {
|
||||
let currentCount = 0;
|
||||
|
||||
// Safely get current material count
|
||||
if (inventory && typeof inventory.getItemCount === 'function') {
|
||||
try {
|
||||
currentCount = inventory.getItemCount(material.id);
|
||||
// Ensure we have a valid number
|
||||
currentCount = typeof currentCount === 'number' && !isNaN(currentCount) ? currentCount : 0;
|
||||
} catch (error) {
|
||||
console.log(`[CRAFTING DEBUG] Error getting count for ${material.id}:`, error);
|
||||
currentCount = 0;
|
||||
}
|
||||
console.log(`[CRAFTING DEBUG] Material ${material.id}: current=${currentCount}, required=${material.quantity}`);
|
||||
} else {
|
||||
console.log(`[CRAFTING DEBUG] Inventory or getItemCount not available for ${material.id}`);
|
||||
currentCount = 0;
|
||||
}
|
||||
|
||||
// Ensure required quantity is also a valid number
|
||||
const requiredQuantity = typeof material.quantity === 'number' && !isNaN(material.quantity) ? material.quantity : 0;
|
||||
|
||||
// Check if we have enough materials
|
||||
if (currentCount < requiredQuantity) {
|
||||
missing.push({
|
||||
id: material.id,
|
||||
required: requiredQuantity,
|
||||
current: currentCount,
|
||||
missing: Math.max(0, requiredQuantity - currentCount)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING DEBUG] Missing materials:`, missing);
|
||||
return missing;
|
||||
}
|
||||
|
||||
async craftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe) {
|
||||
console.error(`[CRAFTING] Recipe not found: ${recipeId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.canCraftRecipe(recipeId)) {
|
||||
console.log(`[CRAFTING] Cannot craft recipe: ${recipe.name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING] Starting to craft: ${recipe.name}`);
|
||||
|
||||
// Remove materials
|
||||
if (recipe.materials) {
|
||||
for (const material of recipe.materials) {
|
||||
this.game.systems.inventory.removeItem(material.id, material.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
// Add crafting experience
|
||||
if (recipe.experience) {
|
||||
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
|
||||
}
|
||||
|
||||
// Wait for crafting time
|
||||
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime));
|
||||
|
||||
// Add results to inventory
|
||||
if (recipe.results) {
|
||||
for (const result of recipe.results) {
|
||||
this.game.systems.inventory.addItem(result.id, result.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
// Update quest progress
|
||||
if (this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.onItemCrafted();
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING] Successfully crafted: ${recipe.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
selectRecipe(recipeId) {
|
||||
this.selectedRecipe = this.recipes.get(recipeId);
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
|
||||
getSelectedRecipe() {
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
this.updateRecipeList();
|
||||
this.updateCraftingDetails();
|
||||
this.updateCraftingInfo();
|
||||
}
|
||||
|
||||
updateRecipeList() {
|
||||
const recipeListElement = document.getElementById('recipeList');
|
||||
if (!recipeListElement) return;
|
||||
|
||||
const recipes = this.getRecipesByCategory(this.currentCategory);
|
||||
|
||||
recipeListElement.innerHTML = '';
|
||||
|
||||
if (recipes.length === 0) {
|
||||
recipeListElement.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
const recipeElement = document.createElement('div');
|
||||
recipeElement.className = 'recipe-item';
|
||||
recipeElement.dataset.recipeId = recipe.id;
|
||||
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
const missingMaterials = this.getMissingMaterials(recipe.id);
|
||||
|
||||
// Check if recipe is unlocked (skill requirements met)
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
let skillRequirementsMet = true;
|
||||
if (recipe.requirements && skillSystem) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
||||
if (skillLevel < requiredLevel) {
|
||||
skillRequirementsMet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply styling based on status
|
||||
if (!skillRequirementsMet) {
|
||||
recipeElement.classList.add('locked');
|
||||
} else if (!canCraft) {
|
||||
recipeElement.classList.add('missing-materials');
|
||||
} else {
|
||||
recipeElement.classList.add('can-craft');
|
||||
}
|
||||
|
||||
// Generate requirements text
|
||||
const requirementsText = recipe.requirements ?
|
||||
Object.entries(recipe.requirements).map(([skill, level]) => `${skill}: ${level}`).join(', ') : 'None';
|
||||
|
||||
// Generate materials with missing status
|
||||
const materialsHtml = recipe.materials ? recipe.materials.map(mat => {
|
||||
const missing = missingMaterials.find(m => m.id === mat.id);
|
||||
const currentCount = missing ? missing.current : 0;
|
||||
const requiredCount = mat.quantity || 0;
|
||||
|
||||
if (missing) {
|
||||
return `<div class="material-item missing">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-quantity">${currentCount}/${requiredCount}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="material-item">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-quantity">${currentCount}/${requiredCount}</span>
|
||||
</div>`;
|
||||
}
|
||||
}).join('') : '';
|
||||
|
||||
recipeElement.innerHTML = `
|
||||
<div class="recipe-header">
|
||||
<h4>${recipe.name}</h4>
|
||||
<span class="recipe-level">Level ${requirementsText}</span>
|
||||
</div>
|
||||
<div class="recipe-description">${recipe.description}</div>
|
||||
<div class="recipe-materials">
|
||||
${materialsHtml}
|
||||
</div>
|
||||
${missingMaterials.length > 0 ? `
|
||||
<div class="missing-materials-text">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Missing: ${missingMaterials.map(m => `${m.missing}x ${m.id}`).join(', ')}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="recipe-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${recipe.craftingTime / 1000}s</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
recipeElement.addEventListener('click', () => {
|
||||
this.selectRecipe(recipe.id);
|
||||
this.updateCraftingDetails();
|
||||
});
|
||||
|
||||
recipeListElement.appendChild(recipeElement);
|
||||
});
|
||||
}
|
||||
|
||||
updateCraftingDetails() {
|
||||
const detailsElement = document.getElementById('craftingDetails');
|
||||
if (!detailsElement) return;
|
||||
|
||||
if (!this.selectedRecipe) {
|
||||
detailsElement.innerHTML = `
|
||||
<div class="selected-recipe">
|
||||
<h3>Select a Recipe</h3>
|
||||
<p>Choose a recipe from the list to see details and craft items.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const recipe = this.selectedRecipe;
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
|
||||
detailsElement.innerHTML = `
|
||||
<div class="selected-recipe">
|
||||
<h3>${recipe.name}</h3>
|
||||
<p class="recipe-description">${recipe.description}</p>
|
||||
|
||||
<div class="recipe-requirements">
|
||||
<h4>Requirements:</h4>
|
||||
${recipe.requirements ? Object.entries(recipe.requirements).map(([skill, level]) =>
|
||||
`<div class="requirement-item">
|
||||
<span class="skill-name">${skill}</span>
|
||||
<span class="skill-level">Level ${level}</span>
|
||||
</div>`
|
||||
).join('') : '<p>No special requirements</p>'}
|
||||
</div>
|
||||
|
||||
<div class="recipe-materials-needed">
|
||||
<h4>Materials Needed:</h4>
|
||||
${recipe.materials ? recipe.materials.map(mat =>
|
||||
`<div class="material-needed">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-needed">x${mat.quantity}</span>
|
||||
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
|
||||
</div>`
|
||||
).join('') : '<p>No materials needed</p>'}
|
||||
</div>
|
||||
|
||||
<div class="recipe-results">
|
||||
<h4>Results:</h4>
|
||||
${recipe.results ? recipe.results.map(result =>
|
||||
`<div class="result-item">
|
||||
<span class="result-name">${result.id}</span>
|
||||
<span class="result-quantity">x${result.quantity}</span>
|
||||
</div>`
|
||||
).join('') : ''}
|
||||
</div>
|
||||
|
||||
<div class="recipe-info">
|
||||
<div class="experience-reward">
|
||||
<i class="fas fa-star"></i>
|
||||
<span>${recipe.experience} XP</span>
|
||||
</div>
|
||||
<div class="crafting-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${recipe.craftingTime / 1000} seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
|
||||
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
|
||||
${canCraft ? 'Craft Item' : 'Cannot Craft'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateCraftingInfo() {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return;
|
||||
|
||||
const craftingLevel = skillSystem.getSkillLevel('crafting');
|
||||
const craftingExp = skillSystem.getSkillExperience('crafting');
|
||||
const expNeeded = skillSystem.getExperienceNeeded('crafting');
|
||||
|
||||
const levelElement = document.getElementById('craftingLevel');
|
||||
const expElement = document.getElementById('craftingExp');
|
||||
|
||||
if (levelElement) levelElement.textContent = craftingLevel;
|
||||
if (expElement) expElement.textContent = `${craftingExp}/${expNeeded}`;
|
||||
}
|
||||
|
||||
switchCategory(category) {
|
||||
this.currentCategory = category;
|
||||
this.selectedRecipe = null;
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in GameEngine
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CraftingSystem;
|
||||
}
|
||||
1977
Client/js/systems/DungeonSystem.js
Normal file
357
Client/js/systems/IdleSystem.js
Normal file
@ -0,0 +1,357 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Idle System
|
||||
* Manages offline progression and idle mechanics
|
||||
*/
|
||||
|
||||
class IdleSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Idle settings
|
||||
this.maxOfflineTime = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
||||
this.lastActiveTime = Date.now();
|
||||
this.accumulatedTime = 0; // Track time for resource generation
|
||||
|
||||
// Idle production rates
|
||||
this.productionRates = {
|
||||
credits: 10, // credits per second (increased for better gameplay)
|
||||
experience: 1, // experience per second (increased for better progression)
|
||||
energy: 0.5 // energy regeneration per second
|
||||
};
|
||||
|
||||
// Offline rewards
|
||||
this.offlineRewards = {
|
||||
credits: 0,
|
||||
experience: 0,
|
||||
energy: 0,
|
||||
items: []
|
||||
};
|
||||
|
||||
// Idle bonuses
|
||||
this.bonuses = {
|
||||
premium: 1.0,
|
||||
guild: 1.0,
|
||||
research: 1.0
|
||||
};
|
||||
|
||||
// Idle achievements
|
||||
this.achievements = {
|
||||
totalOfflineTime: 0,
|
||||
maxOfflineSession: 0,
|
||||
totalIdleCredits: 0,
|
||||
totalIdleExperience: 0
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Calculate offline progress if returning
|
||||
this.calculateOfflineProgress();
|
||||
}
|
||||
|
||||
calculateOfflineProgress(offlineTime = null) {
|
||||
const currentTime = Date.now();
|
||||
const actualOfflineTime = offlineTime || (currentTime - this.lastActiveTime);
|
||||
|
||||
// Cap offline time to maximum
|
||||
const cappedOfflineTime = Math.min(actualOfflineTime, this.maxOfflineTime);
|
||||
|
||||
if (cappedOfflineTime < 60000) { // Less than 1 minute
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate production
|
||||
const totalBonus = this.getTotalBonus();
|
||||
const productionSeconds = cappedOfflineTime / 1000;
|
||||
|
||||
this.offlineRewards = {
|
||||
credits: Math.floor(this.productionRates.credits * productionSeconds * totalBonus),
|
||||
experience: Math.floor(this.productionRates.experience * productionSeconds * totalBonus),
|
||||
energy: Math.min(
|
||||
this.game.systems.player.attributes.maxEnergy,
|
||||
Math.floor(this.productionRates.energy * productionSeconds)
|
||||
),
|
||||
items: this.generateIdleItems(cappedOfflineTime)
|
||||
};
|
||||
|
||||
// Update achievements
|
||||
this.achievements.totalOfflineTime += cappedOfflineTime;
|
||||
this.achievements.maxOfflineSession = Math.max(this.achievements.maxOfflineSession, cappedOfflineTime);
|
||||
this.achievements.totalIdleCredits += this.offlineRewards.credits;
|
||||
this.achievements.totalIdleExperience += this.offlineRewards.experience;
|
||||
|
||||
// Show offline rewards notification
|
||||
this.showOfflineRewards(cappedOfflineTime);
|
||||
}
|
||||
|
||||
getTotalBonus() {
|
||||
return this.bonuses.premium * this.bonuses.guild * this.bonuses.research;
|
||||
}
|
||||
|
||||
generateIdleItems(offlineTime) {
|
||||
const items = [];
|
||||
const hours = offlineTime / (1000 * 60 * 60);
|
||||
|
||||
// Chance to find items based on offline time
|
||||
const itemChance = Math.min(0.5, hours * 0.05);
|
||||
|
||||
if (Math.random() < itemChance) {
|
||||
const itemCount = Math.floor(hours / 2) + 1;
|
||||
|
||||
for (let i = 0; i < itemCount; i++) {
|
||||
const rarity = this.getRandomItemRarity();
|
||||
const item = this.game.systems.inventory.generateItem('consumable', rarity);
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getRandomItemRarity() {
|
||||
const roll = Math.random();
|
||||
if (roll < 0.05) return 'legendary';
|
||||
if (roll < 0.15) return 'epic';
|
||||
if (roll < 0.35) return 'rare';
|
||||
if (roll < 0.65) return 'uncommon';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
showOfflineRewards(offlineTime) {
|
||||
const timeString = this.game.formatTime(offlineTime);
|
||||
|
||||
this.game.showNotification(
|
||||
`Welcome back! You were offline for ${timeString}`,
|
||||
'info',
|
||||
5000
|
||||
);
|
||||
|
||||
// Format rewards message
|
||||
let rewardsMessage = 'Offline Rewards:\n';
|
||||
if (this.offlineRewards.credits > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.credits)} credits\n`;
|
||||
}
|
||||
if (this.offlineRewards.experience > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.experience)} XP\n`;
|
||||
}
|
||||
if (this.offlineRewards.energy > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.energy)} energy\n`;
|
||||
}
|
||||
if (this.offlineRewards.items.length > 0) {
|
||||
rewardsMessage += `+${this.offlineRewards.items.length} items\n`;
|
||||
}
|
||||
|
||||
this.game.showNotification(rewardsMessage, 'success', 5000);
|
||||
}
|
||||
|
||||
claimOfflineRewards() {
|
||||
if (this.offlineRewards.credits === 0 &&
|
||||
this.offlineRewards.experience === 0 &&
|
||||
this.offlineRewards.items.length === 0) {
|
||||
this.game.showNotification('No offline rewards to claim', 'info', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Give rewards
|
||||
if (this.offlineRewards.credits > 0) {
|
||||
this.game.systems.economy.addCredits(this.offlineRewards.credits, 'offline');
|
||||
}
|
||||
|
||||
if (this.offlineRewards.experience > 0) {
|
||||
this.game.systems.player.addExperience(this.offlineRewards.experience);
|
||||
}
|
||||
|
||||
if (this.offlineRewards.energy > 0) {
|
||||
this.game.systems.player.restoreEnergy(this.offlineRewards.energy);
|
||||
}
|
||||
|
||||
// Add items to inventory
|
||||
if (this.offlineRewards.items.length > 0) {
|
||||
const inventory = this.game.systems.inventory;
|
||||
this.offlineRewards.items.forEach(item => {
|
||||
inventory.addItem(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset offline rewards
|
||||
this.offlineRewards = {
|
||||
credits: 0,
|
||||
experience: 0,
|
||||
energy: 0,
|
||||
items: []
|
||||
};
|
||||
|
||||
this.game.showNotification('Offline rewards claimed!', 'success', 3000);
|
||||
}
|
||||
|
||||
// Active idle production
|
||||
update(deltaTime) {
|
||||
if (this.game.state.paused) return;
|
||||
|
||||
// Use real computer time delta
|
||||
const seconds = deltaTime / 1000;
|
||||
const totalBonus = this.getTotalBonus();
|
||||
|
||||
// Only add resources once per second, not every frame
|
||||
this.accumulatedTime += seconds;
|
||||
|
||||
if (this.accumulatedTime >= 1.0) {
|
||||
// Calculate active production
|
||||
const activeCredits = Math.floor(this.productionRates.credits * totalBonus);
|
||||
const activeExperience = Math.floor(this.productionRates.experience * totalBonus);
|
||||
// const activeEnergy = this.productionRates.energy * totalBonus * 0.1; // Energy is handled differently
|
||||
|
||||
// Add resources
|
||||
if (activeCredits > 0) {
|
||||
this.game.systems.economy.addCredits(activeCredits, 'idle');
|
||||
}
|
||||
if (activeExperience > 0) {
|
||||
this.game.systems.player.addExperience(activeExperience);
|
||||
}
|
||||
|
||||
// Regenerate energy
|
||||
this.game.systems.player.restoreEnergy(this.productionRates.energy);
|
||||
|
||||
// Reset accumulated time, keeping any remainder
|
||||
this.accumulatedTime -= 1.0;
|
||||
|
||||
// Debugging: Log when resources are added
|
||||
// console.debug(`[IDLE] Added ${activeCredits} credits and ${activeExperience} XP. Accumulated time: ${this.accumulatedTime.toFixed(2)}s`);
|
||||
}
|
||||
|
||||
// Update last active time for offline calculations
|
||||
this.lastActiveTime = Date.now();
|
||||
}
|
||||
|
||||
// Upgrade production rates
|
||||
upgradeProduction(type) {
|
||||
const upgradeCosts = {
|
||||
credits: 100,
|
||||
experience: 150,
|
||||
energy: 80
|
||||
};
|
||||
|
||||
const cost = upgradeCosts[type];
|
||||
if (!cost || this.game.systems.economy.credits < cost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.game.systems.economy.removeCredits(cost);
|
||||
|
||||
switch (type) {
|
||||
case 'credits':
|
||||
this.productionRates.credits += 2;
|
||||
break;
|
||||
case 'experience':
|
||||
this.productionRates.experience += 1;
|
||||
break;
|
||||
case 'energy':
|
||||
this.productionRates.energy += 0.2;
|
||||
break;
|
||||
}
|
||||
|
||||
this.game.showNotification(`Production upgraded: ${type}!`, 'success', 3000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bonus management
|
||||
setBonus(type, value) {
|
||||
if (this.bonuses[type] !== undefined) {
|
||||
this.bonuses[type] = value;
|
||||
this.game.showNotification(`${type} bonus set to ${value}x`, 'info', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Achievement checking
|
||||
checkAchievements() {
|
||||
const achievements = [
|
||||
{
|
||||
id: 'idle_warrior',
|
||||
name: 'Idle Warrior',
|
||||
description: 'Earn 1,000,000 credits from idle',
|
||||
condition: () => this.achievements.totalIdleCredits >= 1000000,
|
||||
reward: { gems: 50, experience: 1000 }
|
||||
},
|
||||
{
|
||||
id: 'time_master',
|
||||
name: 'Time Master',
|
||||
description: 'Accumulate 24 hours of offline time',
|
||||
condition: () => this.achievements.totalOfflineTime >= 24 * 60 * 60 * 1000,
|
||||
reward: { gems: 25, experience: 500 }
|
||||
},
|
||||
{
|
||||
id: 'marathon_idle',
|
||||
name: 'Marathon Idle',
|
||||
description: 'Be offline for more than 12 hours at once',
|
||||
condition: () => this.achievements.maxOfflineSession >= 12 * 60 * 60 * 1000,
|
||||
reward: { gems: 100, experience: 2000 }
|
||||
}
|
||||
];
|
||||
|
||||
achievements.forEach(achievement => {
|
||||
if (achievement.condition()) {
|
||||
this.unlockAchievement(achievement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unlockAchievement(achievement) {
|
||||
this.game.showNotification(`Achievement Unlocked: ${achievement.name}!`, 'success', 5000);
|
||||
this.game.showNotification(achievement.description, 'info', 3000);
|
||||
|
||||
// Give rewards
|
||||
if (achievement.reward.gems) {
|
||||
this.game.systems.economy.addGems(achievement.reward.gems, 'achievement');
|
||||
}
|
||||
|
||||
if (achievement.reward.experience) {
|
||||
this.game.systems.player.addExperience(achievement.reward.experience);
|
||||
}
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
const offlineTimeElement = document.getElementById('offlineTime');
|
||||
const offlineResourcesElement = document.getElementById('offlineResources');
|
||||
const claimOfflineBtn = document.getElementById('claimOfflineBtn');
|
||||
|
||||
if (offlineTimeElement) {
|
||||
const totalRewards = this.offlineRewards.credits +
|
||||
this.offlineRewards.experience +
|
||||
(this.offlineRewards.items.length * 100);
|
||||
offlineTimeElement.textContent = totalRewards > 0 ? 'Available' : 'None';
|
||||
}
|
||||
|
||||
if (offlineResourcesElement) {
|
||||
const totalRewards = this.offlineRewards.credits +
|
||||
this.offlineRewards.experience +
|
||||
(this.offlineRewards.items.length * 100);
|
||||
offlineResourcesElement.textContent = this.game.formatNumber(totalRewards);
|
||||
}
|
||||
|
||||
if (claimOfflineBtn) {
|
||||
const hasRewards = this.offlineRewards.credits > 0 ||
|
||||
this.offlineRewards.experience > 0 ||
|
||||
this.offlineRewards.items.length > 0;
|
||||
claimOfflineBtn.disabled = !hasRewards;
|
||||
}
|
||||
}
|
||||
|
||||
// Save/Load
|
||||
save() {
|
||||
return {
|
||||
lastActiveTime: this.lastActiveTime,
|
||||
productionRates: this.productionRates,
|
||||
bonuses: this.bonuses,
|
||||
achievements: this.achievements,
|
||||
offlineRewards: this.offlineRewards
|
||||
};
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.lastActiveTime) this.lastActiveTime = data.lastActiveTime;
|
||||
if (data.productionRates) this.productionRates = { ...this.productionRates, ...data.productionRates };
|
||||
if (data.bonuses) this.bonuses = { ...this.bonuses, ...data.bonuses };
|
||||
if (data.achievements) this.achievements = { ...this.achievements, ...data.achievements };
|
||||
if (data.offlineRewards) this.offlineRewards = data.offlineRewards;
|
||||
}
|
||||
}
|
||||
2725
Client/js/systems/QuestSystem.js
Normal file
223
Client/js/systems/ShipSystem.js
Normal file
@ -0,0 +1,223 @@
|
||||
class ShipSystem {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.ships = [];
|
||||
this.currentShip = null;
|
||||
this.initializeShips();
|
||||
}
|
||||
|
||||
initializeShips() {
|
||||
// Initialize with player's current ship instead of static data
|
||||
this.ships = [];
|
||||
|
||||
// Wait for game systems to be ready, then sync with player ship
|
||||
setTimeout(() => {
|
||||
this.syncWithPlayerShip();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
syncWithPlayerShip() {
|
||||
const player = this.game.systems.player;
|
||||
if (!player || !player.ship) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player && player.ship) {
|
||||
// Create ship object from player's current ship
|
||||
const playerShip = {
|
||||
id: 'current_ship',
|
||||
name: player.ship.name || 'Starter Cruiser',
|
||||
class: player.ship.class || 'Cruiser',
|
||||
level: player.ship.level || 1,
|
||||
health: player.ship.health || player.ship.maxHealth || 100,
|
||||
maxHealth: player.ship.maxHealth || 100,
|
||||
attack: player.ship.attack || player.attributes.attack || 10,
|
||||
defense: player.ship.defence || player.attributes.defense || 5,
|
||||
speed: player.ship.speed || player.attributes.speed || 10,
|
||||
image: player.ship.texture || 'assets/textures/ships/starter_cruiser.png',
|
||||
status: 'active',
|
||||
experience: 0,
|
||||
requiredExp: 100,
|
||||
rarity: 'Common'
|
||||
};
|
||||
|
||||
this.ships = [playerShip];
|
||||
this.currentShip = playerShip;
|
||||
|
||||
// Update the display immediately
|
||||
this.updateCurrentShipDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
renderShips() {
|
||||
const shipGrid = document.getElementById('shipGrid');
|
||||
if (!shipGrid) return;
|
||||
|
||||
shipGrid.innerHTML = '';
|
||||
|
||||
this.ships.forEach(ship => {
|
||||
const shipCard = this.createShipCard(ship);
|
||||
shipGrid.appendChild(shipCard);
|
||||
});
|
||||
}
|
||||
|
||||
createShipCard(ship) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `ship-card ${ship.status === 'active' ? 'active' : ''}`;
|
||||
card.dataset.shipId = ship.id;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="ship-card-header">
|
||||
<img src="${ship.image}" alt="${ship.name}" class="ship-card-image">
|
||||
<div class="ship-card-info">
|
||||
<div class="ship-card-rarity ${ship.rarity.toLowerCase()}">${ship.rarity}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ship-card-actions">
|
||||
<button class="btn-action btn-switch" onclick="game.systems.ship.switchShip('${ship.id}')"
|
||||
${ship.status === 'active' ? 'disabled' : ''}>
|
||||
${ship.status === 'active' ? 'ACTIVE' : 'SWITCH'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
updateCurrentShipDisplay() {
|
||||
// Use player's ship data instead of this.currentShip
|
||||
const player = this.game.systems.player;
|
||||
if (!player || !player.ship) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = {
|
||||
currentShipImage: document.getElementById('currentShipImage'),
|
||||
currentShipName: document.getElementById('currentShipName'),
|
||||
currentShipClass: document.getElementById('currentShipClass'),
|
||||
currentShipLevel: document.getElementById('currentShipLevel'),
|
||||
currentShipHealth: document.getElementById('currentShipHealth'),
|
||||
currentShipAttack: document.getElementById('currentShipAttack'),
|
||||
currentShipDefense: document.getElementById('currentShipDefense'),
|
||||
currentShipSpeed: document.getElementById('currentShipSpeed')
|
||||
};
|
||||
|
||||
// Use player's ship data
|
||||
const ship = player.ship;
|
||||
|
||||
if (elements.currentShipImage) {
|
||||
// Use the ship's texture if available, otherwise fallback
|
||||
const imagePath = ship.texture || `assets/textures/ships/starter_cruiser.png`;
|
||||
elements.currentShipImage.src = imagePath;
|
||||
elements.currentShipImage.alt = ship.name;
|
||||
}
|
||||
if (elements.currentShipName) elements.currentShipName.textContent = ship.name;
|
||||
if (elements.currentShipClass) elements.currentShipClass.textContent = ship.class || 'Unknown';
|
||||
if (elements.currentShipLevel) elements.currentShipLevel.textContent = ship.level || 1;
|
||||
if (elements.currentShipHealth) elements.currentShipHealth.textContent = `${ship.health}/${ship.maxHealth}`;
|
||||
if (elements.currentShipAttack) elements.currentShipAttack.textContent = ship.attack || 0;
|
||||
if (elements.currentShipDefense) elements.currentShipDefense.textContent = ship.defence || ship.defense || 0;
|
||||
if (elements.currentShipSpeed) elements.currentShipSpeed.textContent = ship.speed || 0;
|
||||
}
|
||||
|
||||
switchShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship || ship.status === 'active') return;
|
||||
|
||||
// Deactivate current ship
|
||||
if (this.currentShip) {
|
||||
this.currentShip.status = 'inactive';
|
||||
}
|
||||
|
||||
// Activate new ship
|
||||
ship.status = 'active';
|
||||
this.currentShip = ship;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
// Show notification
|
||||
this.game.showNotification(`Switched to ${ship.name}!`, 'success', 3000);
|
||||
}
|
||||
|
||||
upgradeShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship) return;
|
||||
|
||||
const upgradeCost = ship.level * 1000;
|
||||
|
||||
if (this.game.systems.economy.getCredits() < upgradeCost) {
|
||||
this.game.showNotification(`Not enough credits! Need ${upgradeCost} credits.`, 'error', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Upgrade ship
|
||||
this.game.systems.economy.removeCredits(upgradeCost);
|
||||
ship.level++;
|
||||
ship.maxHealth += 10;
|
||||
ship.health = ship.maxHealth; // Full heal on upgrade
|
||||
ship.attack += 2;
|
||||
ship.defense += 1;
|
||||
ship.speed += 1;
|
||||
ship.requiredExp = ship.level * 100;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
this.game.showNotification(`${ship.name} upgraded to level ${ship.level}!`, 'success', 3000);
|
||||
}
|
||||
|
||||
repairShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship || ship.health >= ship.maxHealth) return;
|
||||
|
||||
const repairCost = Math.floor((ship.maxHealth - ship.health) * 0.5);
|
||||
|
||||
if (this.game.systems.economy.getCredits() < repairCost) {
|
||||
this.game.showNotification(`Not enough credits! Need ${repairCost} credits.`, 'error', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Repair ship
|
||||
this.game.systems.economy.removeCredits(repairCost);
|
||||
ship.health = ship.maxHealth;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
this.game.showNotification(`${ship.name} fully repaired!`, 'success', 3000);
|
||||
}
|
||||
|
||||
addExperience(shipId, amount) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship) return;
|
||||
|
||||
ship.experience += amount;
|
||||
|
||||
// Check for level up
|
||||
while (ship.experience >= ship.requiredExp) {
|
||||
ship.experience -= ship.requiredExp;
|
||||
this.upgradeShip(shipId);
|
||||
}
|
||||
|
||||
this.renderShips();
|
||||
if (this.currentShip && this.currentShip.id === shipId) {
|
||||
this.updateCurrentShipDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
getShip(shipId) {
|
||||
return this.ships.find(s => s.id === shipId);
|
||||
}
|
||||
|
||||
getCurrentShip() {
|
||||
return this.currentShip;
|
||||
}
|
||||
|
||||
getAllShips() {
|
||||
return this.ships;
|
||||
}
|
||||
}
|
||||
588
Client/js/systems/SkillSystem.js
Normal file
@ -0,0 +1,588 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Skill System
|
||||
* Manages skills, progression, and specialization
|
||||
*/
|
||||
|
||||
class SkillSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Skill categories
|
||||
this.categories = {
|
||||
combat: 'Combat',
|
||||
science: 'Science',
|
||||
crafting: 'Crafting'
|
||||
};
|
||||
|
||||
// Skill definitions
|
||||
this.skills = {
|
||||
combat: {
|
||||
weapons_mastery: {
|
||||
name: 'Weapons Mastery',
|
||||
description: 'Increases weapon damage and critical chance',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
attack: 2,
|
||||
criticalChance: 0.01
|
||||
},
|
||||
icon: 'fa-sword',
|
||||
unlocked: true
|
||||
},
|
||||
shield_techniques: {
|
||||
name: 'Shield Techniques',
|
||||
description: 'Improves defense and energy efficiency',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
defense: 2,
|
||||
maxEnergy: 5
|
||||
},
|
||||
icon: 'fa-shield-alt',
|
||||
unlocked: true
|
||||
},
|
||||
piloting: {
|
||||
name: 'Piloting',
|
||||
description: 'Enhances speed and evasion',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
speed: 2,
|
||||
criticalChance: 0.005
|
||||
},
|
||||
icon: 'fa-rocket',
|
||||
unlocked: true
|
||||
},
|
||||
tactical_analysis: {
|
||||
name: 'Tactical Analysis',
|
||||
description: 'Reveals enemy weaknesses and improves accuracy',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
criticalDamage: 0.05,
|
||||
attack: 1
|
||||
},
|
||||
icon: 'fa-brain',
|
||||
unlocked: false,
|
||||
requiredLevel: 5
|
||||
}
|
||||
},
|
||||
science: {
|
||||
engineering: {
|
||||
name: 'Engineering',
|
||||
description: 'Technical skills for ship components and machinery',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.08,
|
||||
shipStats: 0.03
|
||||
},
|
||||
icon: 'fa-wrench',
|
||||
unlocked: true
|
||||
},
|
||||
energy_manipulation: {
|
||||
name: 'Energy Manipulation',
|
||||
description: 'Better energy control and regeneration',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
maxEnergy: 10,
|
||||
energyRegeneration: 0.1
|
||||
},
|
||||
icon: 'fa-bolt',
|
||||
unlocked: true
|
||||
},
|
||||
alien_technology: {
|
||||
name: 'Alien Technology',
|
||||
description: 'Understanding and using alien artifacts',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
findRarity: 0.05,
|
||||
itemValue: 0.1
|
||||
},
|
||||
icon: 'fa-atom',
|
||||
unlocked: false,
|
||||
requiredLevel: 3
|
||||
},
|
||||
quantum_physics: {
|
||||
name: 'Quantum Physics',
|
||||
description: 'Advanced quantum mechanics for better equipment',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
criticalDamage: 0.1,
|
||||
attack: 3
|
||||
},
|
||||
icon: 'fa-microscope',
|
||||
unlocked: false,
|
||||
requiredLevel: 8
|
||||
},
|
||||
bio_engineering: {
|
||||
name: 'Bio-Engineering',
|
||||
description: 'Biological enhancements and healing',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
maxHealth: 15,
|
||||
healthRegeneration: 0.05
|
||||
},
|
||||
icon: 'fa-dna',
|
||||
unlocked: false,
|
||||
requiredLevel: 6
|
||||
}
|
||||
},
|
||||
crafting: {
|
||||
crafting: {
|
||||
name: 'General Crafting',
|
||||
description: 'Basic crafting skills for all items',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.05
|
||||
},
|
||||
icon: 'fa-hammer',
|
||||
unlocked: true
|
||||
},
|
||||
weapon_crafting: {
|
||||
name: 'Weapon Crafting',
|
||||
description: 'Create and upgrade weapons',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.1,
|
||||
weaponStats: 0.05
|
||||
},
|
||||
icon: 'fa-hammer',
|
||||
unlocked: true
|
||||
},
|
||||
armor_forging: {
|
||||
name: 'Armor Forging',
|
||||
description: 'Forge protective armor and shields',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
craftingBonus: 0.1,
|
||||
armorStats: 0.05
|
||||
},
|
||||
icon: 'fa-anvil',
|
||||
unlocked: true
|
||||
},
|
||||
resource_extraction: {
|
||||
name: 'Resource Extraction',
|
||||
description: 'Better resource gathering and efficiency',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
resourceBonus: 0.15,
|
||||
findResources: 0.1
|
||||
},
|
||||
icon: 'fa-gem',
|
||||
unlocked: false,
|
||||
requiredLevel: 4
|
||||
},
|
||||
engineering: {
|
||||
name: 'Engineering',
|
||||
description: 'Advanced ship modifications and systems',
|
||||
maxLevel: 10,
|
||||
currentLevel: 0,
|
||||
experience: 0,
|
||||
experienceToNext: 100,
|
||||
effects: {
|
||||
shipUpgrades: 0.2,
|
||||
systemEfficiency: 0.1
|
||||
},
|
||||
icon: 'fa-cogs',
|
||||
unlocked: false,
|
||||
requiredLevel: 7
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Skill experience rates
|
||||
this.experienceRates = {
|
||||
combat: 1.0,
|
||||
science: 0.8,
|
||||
crafting: 0.6
|
||||
};
|
||||
|
||||
// Active buffs from skills
|
||||
this.activeBuffs = {};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
}
|
||||
|
||||
// Skill management
|
||||
addSkillExperience(category, skillId, amount) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
if (!skill || skill.currentLevel >= skill.maxLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
skill.experience += amount;
|
||||
|
||||
// Check for level up
|
||||
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
|
||||
this.levelUpSkill(category, skillId);
|
||||
}
|
||||
|
||||
this.applySkillEffects();
|
||||
return true;
|
||||
}
|
||||
|
||||
levelUpSkill(category, skillId) {
|
||||
const skill = this.skills[category][skillId];
|
||||
|
||||
// Handle excess experience
|
||||
const excessExperience = skill.experience - skill.experienceToNext;
|
||||
|
||||
skill.currentLevel++;
|
||||
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
|
||||
|
||||
// Set experience to excess (minimum 0)
|
||||
skill.experience = Math.max(0, excessExperience);
|
||||
|
||||
// Apply skill effects
|
||||
this.applySkillEffects();
|
||||
|
||||
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
|
||||
this.game.showNotification('Skill effects applied!', 'info', 3000);
|
||||
}
|
||||
|
||||
upgradeSkill(category, skillId) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
const player = this.game.systems.player;
|
||||
|
||||
if (!skill) {
|
||||
this.game.showNotification('Skill not found', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skill.unlocked) {
|
||||
this.game.showNotification('Skill is locked', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skill.currentLevel >= skill.maxLevel) {
|
||||
this.game.showNotification('Skill is at maximum level', 'warning', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player.stats.skillPoints < 1) {
|
||||
this.game.showNotification('Not enough skill points', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use skill point and level up
|
||||
player.stats.skillPoints--;
|
||||
this.levelUpSkill(category, skillId);
|
||||
|
||||
// Update UI to refresh skill points display
|
||||
this.updateUI();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unlockSkill(category, skillId) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
const player = this.game.systems.player;
|
||||
|
||||
if (!skill) {
|
||||
this.game.showNotification('Skill not found', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skill.unlocked) {
|
||||
this.game.showNotification('Skill is already unlocked', 'warning', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
|
||||
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player.stats.skillPoints < 2) {
|
||||
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unlock skill
|
||||
player.stats.skillPoints -= 2;
|
||||
skill.unlocked = true;
|
||||
skill.currentLevel = 1;
|
||||
|
||||
this.applySkillEffects();
|
||||
|
||||
// Update UI to refresh skill points display
|
||||
this.updateUI();
|
||||
|
||||
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
|
||||
return true;
|
||||
}
|
||||
|
||||
applySkillEffects() {
|
||||
const player = this.game.systems.player;
|
||||
|
||||
// Reset to base stats first
|
||||
this.resetToBaseStats();
|
||||
|
||||
// Apply all skill effects
|
||||
Object.values(this.skills).forEach(skill => {
|
||||
if (skill.level > 0) {
|
||||
const skillData = this.skillData[skill.id];
|
||||
if (skillData && skillData.effects) {
|
||||
Object.entries(skillData.effects).forEach(([effect, value]) => {
|
||||
const totalEffect = value * skill.level;
|
||||
|
||||
switch (effect) {
|
||||
case 'attack':
|
||||
player.attributes.attack += totalEffect;
|
||||
break;
|
||||
case 'defense':
|
||||
player.attributes.defense += totalEffect;
|
||||
break;
|
||||
case 'speed':
|
||||
player.attributes.speed += totalEffect;
|
||||
break;
|
||||
case 'maxHealth':
|
||||
player.attributes.maxHealth += totalEffect;
|
||||
break;
|
||||
case 'maxEnergy':
|
||||
player.attributes.maxEnergy += totalEffect;
|
||||
break;
|
||||
case 'criticalChance':
|
||||
player.attributes.criticalChance += totalEffect;
|
||||
break;
|
||||
case 'criticalDamage':
|
||||
player.attributes.criticalDamage += totalEffect;
|
||||
break;
|
||||
case 'energyRegeneration':
|
||||
case 'healthRegeneration':
|
||||
case 'craftingBonus':
|
||||
case 'weaponStats':
|
||||
case 'armorStats':
|
||||
case 'resourceBonus':
|
||||
case 'shipUpgrades':
|
||||
case 'systemEfficiency':
|
||||
case 'findRarity':
|
||||
case 'itemValue':
|
||||
case 'findResources':
|
||||
// Store these for other systems to use
|
||||
if (!this.activeBuffs[effect]) {
|
||||
this.activeBuffs[effect] = 0;
|
||||
}
|
||||
this.activeBuffs[effect] += totalEffect;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
player.updateUI();
|
||||
}
|
||||
|
||||
resetToBaseStats() {
|
||||
const player = this.game.systems.player;
|
||||
|
||||
// Reset to base values (would need to store base stats separately)
|
||||
// For now, we'll use initial values
|
||||
const baseStats = {
|
||||
attack: 10 + (player.stats.level - 1) * 2,
|
||||
defense: 5 + (player.stats.level - 1) * 1,
|
||||
speed: 10,
|
||||
maxHealth: 100 + (player.stats.level - 1) * 10,
|
||||
maxEnergy: 100 + (player.stats.level - 1) * 5,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
Object.assign(player.attributes, baseStats);
|
||||
this.activeBuffs = {};
|
||||
}
|
||||
|
||||
// Skill experience from actions
|
||||
awardCombatExperience(amount) {
|
||||
this.addSkillExperience('combat', 'weapons_mastery', amount);
|
||||
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
|
||||
}
|
||||
|
||||
awardScienceExperience(amount) {
|
||||
this.addSkillExperience('science', 'energy_manipulation', amount);
|
||||
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
|
||||
}
|
||||
|
||||
awardCraftingExperience(amount) {
|
||||
this.addSkillExperience('crafting', 'weapon_crafting', amount);
|
||||
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
|
||||
}
|
||||
|
||||
// Skill checks
|
||||
getSkillLevel(category, skillId) {
|
||||
return this.skills[category]?.[skillId]?.currentLevel || 0;
|
||||
}
|
||||
|
||||
hasSkill(category, skillId, minimumLevel = 1) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
|
||||
}
|
||||
|
||||
getSkillBonus(effect) {
|
||||
return this.activeBuffs[effect] || 0;
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
this.updateSkillsGrid();
|
||||
this.updateSkillPointsDisplay();
|
||||
}
|
||||
|
||||
updateSkillsGrid() {
|
||||
const skillsGridElement = document.getElementById('skillsGrid');
|
||||
if (!skillsGridElement) return;
|
||||
|
||||
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
|
||||
const skills = this.skills[activeCategory] || {};
|
||||
|
||||
skillsGridElement.innerHTML = '';
|
||||
|
||||
Object.entries(skills).forEach(([skillId, skill]) => {
|
||||
const skillElement = document.createElement('div');
|
||||
skillElement.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
||||
|
||||
const progressPercent = skill.currentLevel > 0 ?
|
||||
(skill.experience / skill.experienceToNext) * 100 : 0;
|
||||
|
||||
// Use texture manager for icon fallback
|
||||
const iconClass = this.game.systems.textureManager ?
|
||||
this.game.systems.textureManager.getIcon(skill.icon) :
|
||||
(skill.icon || 'fa-question');
|
||||
|
||||
skillElement.innerHTML = `
|
||||
<div class="skill-header">
|
||||
<div class="skill-icon">
|
||||
<i class="fas ${iconClass}"></i>
|
||||
</div>
|
||||
<div class="skill-info">
|
||||
<div class="skill-name">${skill.name}</div>
|
||||
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-description">${skill.description}</div>
|
||||
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
|
||||
<div class="skill-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
<span>${skill.experience}/${skill.experienceToNext} XP</span>
|
||||
</div>
|
||||
` : skill.currentLevel >= skill.maxLevel ? `
|
||||
<div class="skill-max-level">
|
||||
<span>MAX LEVEL</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="skill-actions">
|
||||
${!skill.unlocked ? `
|
||||
<button class="btn btn-warning" onclick="if(window.game && window.game.systems) window.game.systems.skillSystem.unlockSkill('${activeCategory}', '${skillId}')">
|
||||
Unlock (2 Points)
|
||||
</button>
|
||||
` : skill.currentLevel < skill.maxLevel ? `
|
||||
<button class="btn btn-primary" onclick="if(window.game && window.game.systems) window.game.systems.skillSystem.upgradeSkill('${activeCategory}', '${skillId}')">
|
||||
Upgrade (1 Point)
|
||||
</button>
|
||||
` : `
|
||||
<span class="max-level">MAX LEVEL</span>
|
||||
`}
|
||||
</div>
|
||||
${skill.requiredLevel && !skill.unlocked ? `
|
||||
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
skillsGridElement.appendChild(skillElement);
|
||||
});
|
||||
}
|
||||
|
||||
updateSkillPointsDisplay() {
|
||||
const player = this.game.systems.player;
|
||||
// Update skill points display if element exists
|
||||
const skillPointsElements = document.querySelectorAll('.skill-points');
|
||||
skillPointsElements.forEach(element => {
|
||||
element.textContent = `Skill Points: ${player.stats.skillPoints}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Save/Load
|
||||
save() {
|
||||
return {
|
||||
skills: this.skills,
|
||||
activeBuffs: this.activeBuffs
|
||||
};
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.skills) {
|
||||
// Deep merge to preserve structure
|
||||
for (const [category, skills] of Object.entries(data.skills)) {
|
||||
if (this.skills[category]) {
|
||||
for (const [skillId, skillData] of Object.entries(skills)) {
|
||||
if (this.skills[category][skillId]) {
|
||||
Object.assign(this.skills[category][skillId], skillData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.activeBuffs) {
|
||||
this.activeBuffs = data.activeBuffs;
|
||||
}
|
||||
|
||||
this.applySkillEffects();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.skillPoints = 0;
|
||||
this.unlockedSkills = [];
|
||||
this.activeBuffs = [];
|
||||
// Skills are already defined in constructor, just reset levels
|
||||
Object.values(this.skills).forEach(category => {
|
||||
Object.values(category).forEach(skill => {
|
||||
skill.currentLevel = 0;
|
||||
skill.experience = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
1433
Client/js/ui/LiveMainMenu.js
Normal file
1251
Client/js/ui/MainMenu.js
Normal file
2321
Client/js/ui/UIManager.js
Normal file
4688
Client/package-lock.json
generated
Normal file
127
Client/package.json
Normal file
@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "galaxystrikeonline",
|
||||
"version": "1.0.0",
|
||||
"description": "Galaxy Strike Online - Space Idle MMORPG",
|
||||
"license": "MIT",
|
||||
"author": "Korvarix Studios",
|
||||
"type": "commonjs",
|
||||
"main": "electron-main.js",
|
||||
"homepage": "./",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev": "electron . --dev",
|
||||
"debug": "cross-env DEBUG=* electron .",
|
||||
"debug-verbose": "cross-env DEBUG=* VERBOSE=true electron .",
|
||||
"debug-boot": "cross-env DEBUG=boot* electron .",
|
||||
"debug-renderer": "cross-env DEBUG=renderer* electron .",
|
||||
"debug-main": "cross-env DEBUG=main* electron .",
|
||||
"debug-windows": "set DEBUG=boot* && electron .",
|
||||
"debug-windows-verbose": "set DEBUG=* && set VERBOSE=true && electron .",
|
||||
"build": "electron-builder",
|
||||
"build-win": "electron-builder --win",
|
||||
"build-mac": "electron-builder --mac",
|
||||
"build-linux": "electron-builder --linux",
|
||||
"dist": "npm run build",
|
||||
"pack": "electron-builder --dir",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"keywords": [
|
||||
"game",
|
||||
"space",
|
||||
"mmorpg",
|
||||
"idle",
|
||||
"electron"
|
||||
],
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"express": "^4.22.1",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^40.0.0",
|
||||
"electron-builder": "^23.0.6"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.korvarixstudios.galaxystrikeonline",
|
||||
"productName": "Galaxy Strike Online",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"**/*",
|
||||
"!node_modules",
|
||||
"!dist",
|
||||
"!*.md"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "assets",
|
||||
"to": "assets"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.ico"
|
||||
},
|
||||
"mac": {
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.icns",
|
||||
"category": "public.app-category.games"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "deb",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.png",
|
||||
"category": "Game"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Client/preload.js
Normal file
@ -0,0 +1,33 @@
|
||||
console.log('[PRELOAD] Preload script starting');
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
console.log('[PRELOAD] Electron modules imported successfully');
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Window controls
|
||||
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
||||
closeWindow: () => ipcRenderer.send('close-window'),
|
||||
toggleFullscreen: () => ipcRenderer.send('toggle-fullscreen'),
|
||||
|
||||
// Logging
|
||||
log: (level, message, data) => ipcRenderer.send('log-message', { level, message, data }),
|
||||
|
||||
// Save operations
|
||||
createSaveFolders: (saveSlots) => ipcRenderer.invoke('create-save-folders', saveSlots),
|
||||
testFileAccess: (slotPath) => ipcRenderer.invoke('test-file-access', slotPath),
|
||||
saveGame: (slot, saveData) => ipcRenderer.invoke('save-game', slot, saveData),
|
||||
loadGame: (slot) => ipcRenderer.invoke('load-game', slot),
|
||||
|
||||
// System operations
|
||||
getPath: (name) => ipcRenderer.invoke('get-path', name)
|
||||
});
|
||||
|
||||
console.log('[PRELOAD] electronAPI exposed via contextBridge successfully');
|
||||
} catch (error) {
|
||||
console.error('[PRELOAD] Failed to expose electronAPI:', error);
|
||||
console.error('[PRELOAD] Error stack:', error.stack);
|
||||
}
|
||||
1677
Client/styles/components.css
Normal file
2544
Client/styles/main.css
Normal file
842
Client/styles/tables.css
Normal file
@ -0,0 +1,842 @@
|
||||
/* Table Styles for Galaxy Strike Online */
|
||||
|
||||
/* Base Table Styles */
|
||||
.dungeon-table,
|
||||
.skills-table,
|
||||
.base-rooms-table,
|
||||
.base-upgrades-table,
|
||||
.ship-gallery-table,
|
||||
.starbase-management-table,
|
||||
.starbase-shop-table,
|
||||
.quests-table,
|
||||
.inventory-table,
|
||||
.shop-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.dungeon-table th,
|
||||
.skills-table th,
|
||||
.base-rooms-table th,
|
||||
.base-upgrades-table th,
|
||||
.ship-gallery-table th,
|
||||
.starbase-management-table th,
|
||||
.starbase-shop-table th,
|
||||
.quests-table th,
|
||||
.inventory-table th,
|
||||
.shop-table th {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dungeon-table td,
|
||||
.skills-table td,
|
||||
.base-rooms-table td,
|
||||
.base-upgrades-table td,
|
||||
.ship-gallery-table td,
|
||||
.starbase-management-table td,
|
||||
.starbase-shop-table td,
|
||||
.quests-table td,
|
||||
.inventory-table td,
|
||||
.shop-table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dungeon-table tr:hover,
|
||||
.skills-table tr:hover,
|
||||
.base-rooms-table tr:hover,
|
||||
.base-upgrades-table tr:hover,
|
||||
.ship-gallery-table tr:hover,
|
||||
.starbase-management-table tr:hover,
|
||||
.starbase-shop-table tr:hover,
|
||||
.quests-table tr:hover,
|
||||
.inventory-table tr:hover,
|
||||
.shop-table tr:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dungeon Table Specific */
|
||||
.dungeon-table .difficulty-easy { color: #00ff00; }
|
||||
.dungeon-table .difficulty-medium { color: #ffff00; }
|
||||
.dungeon-table .difficulty-hard { color: #ff9900; }
|
||||
.dungeon-table .difficulty-extreme { color: #ff0000; }
|
||||
|
||||
/* Skills Table Specific */
|
||||
.skills-table .skill-level {
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.skills-table .skill-progress {
|
||||
width: 100px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skills-table .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Base Tables Specific */
|
||||
.base-rooms-table .room-status-active { color: #00ff00; }
|
||||
.base-rooms-table .room-status-inactive { color: #ff0000; }
|
||||
.base-rooms-table .room-status-upgrading { color: #ffff00; }
|
||||
|
||||
.base-upgrades-table .upgrade-level {
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* Ship Gallery Grid Specific */
|
||||
.ship-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.ship-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
border: 2px solid var(--primary-color);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ship-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 8px 30px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
|
||||
.ship-card.active {
|
||||
border-color: var(--success-color);
|
||||
box-shadow: 0 8px 30px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
.ship-card.active::before {
|
||||
content: "ACTIVE";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: var(--gradient-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ship-card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.ship-card-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--primary-color);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ship-card-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ship-card-rarity {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--hover-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.ship-card-rarity.common {
|
||||
color: #888;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.ship-card-rarity.rare {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ship-card-rarity.epic {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.ship-card-rarity.legendary {
|
||||
color: var(--warning-color);
|
||||
border-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.ship-card-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.ship-card-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-value {
|
||||
color: var(--text-secondary);
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-value.health {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-value.attack {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-value.defense {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.ship-card-stat .stat-value.speed {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.ship-card-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ship-card-actions .btn-action {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-action.btn-switch {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-action.btn-switch:hover {
|
||||
background: var(--gradient-secondary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-action.btn-switch:disabled {
|
||||
background: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-action.btn-upgrade {
|
||||
background: var(--gradient-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-action.btn-upgrade:hover {
|
||||
background: var(--gradient-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-action.btn-repair {
|
||||
background: var(--gradient-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-action.btn-repair:hover {
|
||||
background: var(--gradient-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Ship Gallery Layout */
|
||||
.ship-layout {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.current-ship-section {
|
||||
flex: 0 0 400px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.ship-grid-section {
|
||||
flex: 1;
|
||||
min-width: 0; /* Prevent flex item from overflowing */
|
||||
}
|
||||
|
||||
.ship-grid-section h4 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.current-ship-section h4 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Current Ship Display */
|
||||
.current-ship-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current-ship-image {
|
||||
flex-shrink: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.current-ship-image img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--primary-color);
|
||||
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.current-ship-details {
|
||||
flex: 1;
|
||||
order: 2;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current-ship-details h5 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current-ship-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ship-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--hover-bg);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.ship-stat .stat-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ship-stat .stat-value {
|
||||
color: var(--text-secondary);
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ship-stat .stat-value.health {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.ship-stat .stat-value.attack {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.ship-stat .stat-value.defense {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.ship-stat .stat-value.speed {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.ship-table-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.ship-table-section h4 {
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.ship-layout {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.current-ship-section {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-ship-display {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.current-ship-image img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.current-ship-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ship-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.ship-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.ship-card-header {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ship-card-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.ship-card-stats {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ship-card-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ship-card-actions .btn-action {
|
||||
padding: 10px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ship-layout {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.current-ship-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.current-ship-display {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.current-ship-image {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.current-ship-details {
|
||||
order: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ship-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.ship-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ship-card-header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ship-card-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ship-card-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ship-card-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ship-card-class {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Console Window Styles */
|
||||
.console-window {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.8);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.console-header {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.console-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.console-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.console-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.console-output {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.console-output .console-line {
|
||||
margin-bottom: 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.console-output .console-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.console-output .console-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.console-output .console-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.console-output .console-info {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.console-input-container {
|
||||
padding: 10px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.console-input {
|
||||
width: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 8px 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.console-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Starbase Tables Specific */
|
||||
.starbase-management-table .starbase-level {
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.starbase-shop-table .starbase-cost {
|
||||
font-weight: bold;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* Quests Table Specific */
|
||||
.quests-table .quest-type-main { color: #667eea; }
|
||||
.quests-table .quest-type-daily { color: #00ff00; }
|
||||
.quests-table .quest-type-procedural { color: #ff9900; }
|
||||
.quests-table .quest-type-completed { color: #888888; }
|
||||
.quests-table .quest-type-failed { color: #ff0000; }
|
||||
|
||||
.quests-table .quest-progress {
|
||||
width: 100px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quests-table .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00ff00, #00cc00);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Inventory Table Specific */
|
||||
.inventory-table .item-rarity-common { color: #888888; }
|
||||
.inventory-table .item-rarity-uncommon { color: #00ff00; }
|
||||
.inventory-table .item-rarity-rare { color: #0088ff; }
|
||||
.inventory-table .item-rarity-epic { color: #8833ff; }
|
||||
.inventory-table .item-rarity-legendary { color: #ff8800; }
|
||||
|
||||
.inventory-table .item-stats {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
/* Shop Table Specific */
|
||||
.shop-table .item-price {
|
||||
font-weight: bold;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.shop-table .item-description {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.dungeon-table .btn-action,
|
||||
.skills-table .btn-action,
|
||||
.base-rooms-table .btn-action,
|
||||
.base-upgrades-table .btn-action,
|
||||
.ship-gallery-table .btn-action,
|
||||
.starbase-management-table .btn-action,
|
||||
.starbase-shop-table .btn-action,
|
||||
.quests-table .btn-action,
|
||||
.inventory-table .btn-action,
|
||||
.shop-table .btn-action {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-action.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-action.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-action.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-action.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-action.btn-success {
|
||||
background: linear-gradient(135deg, #00ff00 0%, #00cc00 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-action.btn-success:hover {
|
||||
background: linear-gradient(135deg, #00cc00 0%, #00ff00 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-action.btn-danger {
|
||||
background: linear-gradient(135deg, #ff0000 0%, #cc0000 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-action.btn-danger:hover {
|
||||
background: linear-gradient(135deg, #cc0000 0%, #ff0000 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.dungeon-table,
|
||||
.skills-table,
|
||||
.base-rooms-table,
|
||||
.base-upgrades-table,
|
||||
.ship-gallery-table,
|
||||
.starbase-management-table,
|
||||
.starbase-shop-table,
|
||||
.quests-table,
|
||||
.inventory-table,
|
||||
.shop-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dungeon-table th,
|
||||
.skills-table th,
|
||||
.base-rooms-table th,
|
||||
.base-upgrades-table th,
|
||||
.ship-gallery-table th,
|
||||
.starbase-management-table th,
|
||||
.starbase-shop-table th,
|
||||
.quests-table th,
|
||||
.inventory-table th,
|
||||
.shop-table th {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dungeon-table td,
|
||||
.skills-table td,
|
||||
.base-rooms-table td,
|
||||
.base-upgrades-table td,
|
||||
.ship-gallery-table td,
|
||||
.starbase-management-table td,
|
||||
.starbase-shop-table td,
|
||||
.quests-table td,
|
||||
.inventory-table td,
|
||||
.shop-table td {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
175
GameServer/config/LocalDatabase.js
Normal file
@ -0,0 +1,175 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class LocalDatabase {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.dbPath = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Create data directory if it doesn't exist
|
||||
const dataDir = path.join(__dirname, '../../data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
logger.info(`[LOCAL DB] Created data directory: ${dataDir}`);
|
||||
}
|
||||
|
||||
this.dbPath = path.join(dataDir, 'mods.db');
|
||||
|
||||
logger.info(`[LOCAL DB] Initializing database at: ${this.dbPath}`);
|
||||
|
||||
// Create database connection
|
||||
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] Error opening database:', err.message);
|
||||
throw err;
|
||||
} else {
|
||||
logger.info('[LOCAL DB] Database connected successfully');
|
||||
}
|
||||
});
|
||||
|
||||
// Enable foreign keys
|
||||
await this.run('PRAGMA foreign_keys = ON');
|
||||
|
||||
// Create tables
|
||||
await this.createTables();
|
||||
|
||||
logger.info('[LOCAL DB] Database initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('[LOCAL DB] Failed to initialize database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createTables() {
|
||||
const tables = [
|
||||
// Mods table
|
||||
`CREATE TABLE IF NOT EXISTS mods (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
version TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
file_path TEXT NOT NULL,
|
||||
checksum TEXT,
|
||||
dependencies TEXT,
|
||||
config TEXT
|
||||
)`,
|
||||
|
||||
// Mod assets table
|
||||
`CREATE TABLE IF NOT EXISTS mod_assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mod_id INTEGER NOT NULL,
|
||||
asset_type TEXT NOT NULL, -- 'ship', 'item', 'quest', etc.
|
||||
asset_id TEXT NOT NULL,
|
||||
asset_data TEXT NOT NULL, -- JSON data
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mod_id) REFERENCES mods (id) ON DELETE CASCADE,
|
||||
UNIQUE(mod_id, asset_type, asset_id)
|
||||
)`,
|
||||
|
||||
// Server mod preferences table
|
||||
`CREATE TABLE IF NOT EXISTS server_mod_preferences (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// Mod load order table
|
||||
`CREATE TABLE IF NOT EXISTS mod_load_order (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mod_id INTEGER NOT NULL,
|
||||
load_order INTEGER NOT NULL,
|
||||
FOREIGN KEY (mod_id) REFERENCES mods (id) ON DELETE CASCADE,
|
||||
UNIQUE(mod_id)
|
||||
)`
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await this.run(table);
|
||||
}
|
||||
|
||||
logger.info('[LOCAL DB] All tables created successfully');
|
||||
}
|
||||
|
||||
// Helper method to run SQL commands
|
||||
run(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, params, function(err) {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ id: this.lastID, changes: this.changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to get single row
|
||||
get(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(sql, params, (err, row) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to get multiple rows
|
||||
all(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(sql, params, (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
close() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.db) {
|
||||
this.db.close((err) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] Error closing database:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
logger.info('[LOCAL DB] Database closed');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get database instance
|
||||
getDatabase() {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
// Get database path
|
||||
getDatabasePath() {
|
||||
return this.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LocalDatabase();
|
||||
14
GameServer/config/database.js
Normal file
@ -0,0 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
const conn = await mongoose.connect(process.env.MONGODB_URI);
|
||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||
} catch (error) {
|
||||
logger.error('Database connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
BIN
GameServer/data/mods.db
Normal file
40
GameServer/middleware/errorHandler.js
Normal file
@ -0,0 +1,40 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
logger.error(err);
|
||||
|
||||
// Mongoose bad ObjectId
|
||||
if (err.name === 'CastError') {
|
||||
const message = 'Resource not found';
|
||||
error = { message, statusCode: 404 };
|
||||
}
|
||||
|
||||
// Mongoose duplicate key
|
||||
if (err.code === 11000) {
|
||||
const message = 'Duplicate field value entered';
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message);
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
res.status(error.statusCode || 500).json({
|
||||
success: false,
|
||||
error: error.message || 'Server Error'
|
||||
});
|
||||
};
|
||||
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new Error(`Not found - ${req.originalUrl}`);
|
||||
res.status(404);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = { errorHandler, notFound };
|
||||
264
GameServer/models/ModModels.js
Normal file
@ -0,0 +1,264 @@
|
||||
const LocalDatabase = require('../config/LocalDatabase');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Mod Model - Handles mod data in local database
|
||||
*/
|
||||
class ModModel {
|
||||
static async create(modData) {
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
filePath,
|
||||
checksum,
|
||||
dependencies,
|
||||
config
|
||||
} = modData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO mods (name, version, author, description, file_path, checksum, dependencies, config)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description || null,
|
||||
filePath,
|
||||
checksum || null,
|
||||
dependencies ? JSON.stringify(dependencies) : null,
|
||||
config ? JSON.stringify(config) : null
|
||||
]);
|
||||
|
||||
return await this.findById(result.id);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM mods WHERE id = ?';
|
||||
const mod = await LocalDatabase.get(sql, [id]);
|
||||
return this.parseMod(mod);
|
||||
}
|
||||
|
||||
static async findByName(name) {
|
||||
const sql = 'SELECT * FROM mods WHERE name = ?';
|
||||
const mod = await LocalDatabase.get(sql, [name]);
|
||||
return this.parseMod(mod);
|
||||
}
|
||||
|
||||
static async findAll(enabledOnly = false) {
|
||||
const sql = enabledOnly
|
||||
? 'SELECT * FROM mods WHERE enabled = 1 ORDER BY name'
|
||||
: 'SELECT * FROM mods ORDER BY name';
|
||||
|
||||
const mods = await LocalDatabase.all(sql);
|
||||
return mods.map(mod => this.parseMod(mod));
|
||||
}
|
||||
|
||||
static async update(id, updates) {
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
if (key === 'dependencies' || key === 'config') {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(JSON.stringify(updates[key]));
|
||||
} else {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(updates[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length === 0) return false;
|
||||
|
||||
fields.push('updated_at = CURRENT_TIMESTAMP');
|
||||
values.push(id);
|
||||
|
||||
const sql = `UPDATE mods SET ${fields.join(', ')} WHERE id = ?`;
|
||||
const result = await LocalDatabase.run(sql, values);
|
||||
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
const sql = 'DELETE FROM mods WHERE id = ?';
|
||||
const result = await LocalDatabase.run(sql, [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async enable(id) {
|
||||
return await this.update(id, { enabled: 1 });
|
||||
}
|
||||
|
||||
static async disable(id) {
|
||||
return await this.update(id, { enabled: 0 });
|
||||
}
|
||||
|
||||
static async getLoadOrder() {
|
||||
const sql = `
|
||||
SELECT m.*, ml.load_order
|
||||
FROM mods m
|
||||
LEFT JOIN mod_load_order ml ON m.id = ml.mod_id
|
||||
WHERE m.enabled = 1
|
||||
ORDER BY ml.load_order ASC, m.name ASC
|
||||
`;
|
||||
|
||||
const mods = await LocalDatabase.all(sql);
|
||||
return mods.map(mod => this.parseMod(mod));
|
||||
}
|
||||
|
||||
static async setLoadOrder(modIds) {
|
||||
await LocalDatabase.run('DELETE FROM mod_load_order');
|
||||
|
||||
for (let i = 0; i < modIds.length; i++) {
|
||||
await LocalDatabase.run(
|
||||
'INSERT INTO mod_load_order (mod_id, load_order) VALUES (?, ?)',
|
||||
[modIds[i], i + 1]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static parseMod(mod) {
|
||||
if (!mod) return null;
|
||||
|
||||
return {
|
||||
...mod,
|
||||
enabled: Boolean(mod.enabled),
|
||||
dependencies: mod.dependencies ? JSON.parse(mod.dependencies) : null,
|
||||
config: mod.config ? JSON.parse(mod.config) : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ModAsset Model - Handles mod assets in local database
|
||||
*/
|
||||
class ModAssetModel {
|
||||
static async create(assetData) {
|
||||
const { modId, assetType, assetId, assetData: data } = assetData;
|
||||
|
||||
const sql = `
|
||||
INSERT OR REPLACE INTO mod_assets (mod_id, asset_type, asset_id, asset_data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [
|
||||
modId,
|
||||
assetType,
|
||||
assetId,
|
||||
JSON.stringify(data)
|
||||
]);
|
||||
|
||||
return await this.findById(result.id);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM mod_assets WHERE id = ?';
|
||||
const asset = await LocalDatabase.get(sql, [id]);
|
||||
return this.parseAsset(asset);
|
||||
}
|
||||
|
||||
static async findByModId(modId) {
|
||||
const sql = 'SELECT * FROM mod_assets WHERE mod_id = ?';
|
||||
const assets = await LocalDatabase.all(sql, [modId]);
|
||||
return assets.map(asset => this.parseAsset(asset));
|
||||
}
|
||||
|
||||
static async findByType(assetType) {
|
||||
const sql = `
|
||||
SELECT ma.*, m.name as mod_name, m.enabled as mod_enabled
|
||||
FROM mod_assets ma
|
||||
JOIN mods m ON ma.mod_id = m.id
|
||||
WHERE ma.asset_type = ? AND m.enabled = 1
|
||||
ORDER BY m.name, ma.asset_id
|
||||
`;
|
||||
|
||||
const assets = await LocalDatabase.all(sql, [assetType]);
|
||||
return assets.map(asset => this.parseAsset(asset));
|
||||
}
|
||||
|
||||
static async findByTypeAndId(assetType, assetId) {
|
||||
const sql = `
|
||||
SELECT ma.*, m.name as mod_name, m.enabled as mod_enabled
|
||||
FROM mod_assets ma
|
||||
JOIN mods m ON ma.mod_id = m.id
|
||||
WHERE ma.asset_type = ? AND ma.asset_id = ? AND m.enabled = 1
|
||||
`;
|
||||
|
||||
const asset = await LocalDatabase.get(sql, [assetType, assetId]);
|
||||
return this.parseAsset(asset);
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
const sql = 'DELETE FROM mod_assets WHERE id = ?';
|
||||
const result = await LocalDatabase.run(sql, [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async deleteByModId(modId) {
|
||||
const sql = 'DELETE FROM mod_assets WHERE mod_id = ?';
|
||||
const result = await LocalDatabase.run(sql, [modId]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static parseAsset(asset) {
|
||||
if (!asset) return null;
|
||||
|
||||
return {
|
||||
...asset,
|
||||
assetData: JSON.parse(asset.asset_data),
|
||||
modEnabled: Boolean(asset.mod_enabled)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerModPreferences Model - Handles server mod preferences
|
||||
*/
|
||||
class ServerModPreferencesModel {
|
||||
static async set(key, value) {
|
||||
const sql = `
|
||||
INSERT OR REPLACE INTO server_mod_preferences (key, value, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [key, JSON.stringify(value)]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async get(key) {
|
||||
const sql = 'SELECT * FROM server_mod_preferences WHERE key = ?';
|
||||
const pref = await LocalDatabase.get(sql, [key]);
|
||||
|
||||
if (!pref) return null;
|
||||
return JSON.parse(pref.value);
|
||||
}
|
||||
|
||||
static async getAll() {
|
||||
const sql = 'SELECT * FROM server_mod_preferences';
|
||||
const prefs = await LocalDatabase.all(sql);
|
||||
|
||||
const result = {};
|
||||
prefs.forEach(pref => {
|
||||
result[pref.key] = JSON.parse(pref.value);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async delete(key) {
|
||||
const sql = 'DELETE FROM server_mod_preferences WHERE key = ?';
|
||||
const result = await LocalDatabase.run(sql, [key]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ModModel,
|
||||
ModAssetModel,
|
||||
ServerModPreferencesModel
|
||||
};
|
||||
3293
GameServer/package-lock.json
generated
Normal file
39
GameServer/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "galaxystrikeonline-game-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Galaxy Strike Online - Game Server (Real-time Multiplayer)",
|
||||
"keywords": [
|
||||
"game",
|
||||
"server",
|
||||
"mmorpg",
|
||||
"multiplayer",
|
||||
"websocket"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "Korvarix Studios",
|
||||
"type": "commonjs",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"debug": "node --inspect server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"socket.io": "^4.7.4",
|
||||
"sqlite3": "^5.0.2",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
214
GameServer/routes/mods.js
Normal file
@ -0,0 +1,214 @@
|
||||
const express = require('express');
|
||||
const logger = require('../utils/logger');
|
||||
const ModService = require('../services/ModService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get all mods
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { enabledOnly } = req.query;
|
||||
const mods = await ModService.getMods(enabledOnly === 'true');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mods
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const mod = await ModService.getMod(parseInt(id));
|
||||
|
||||
if (!mod) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mod
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Enable mod
|
||||
router.post('/:id/enable', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.enableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod enabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error enabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Disable mod
|
||||
router.post('/:id/disable', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.disableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod disabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error disabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod assets by type
|
||||
router.get('/assets/:type', async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const assets = await ModService.getModAssets(type);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
assets
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod assets:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod asset
|
||||
router.get('/assets/:type/:assetId', async (req, res) => {
|
||||
try {
|
||||
const { type, assetId } = req.params;
|
||||
const asset = await ModService.getModAsset(type, assetId);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({ error: 'Asset not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
asset
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod asset:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod load order
|
||||
router.get('/load-order', async (req, res) => {
|
||||
try {
|
||||
const loadOrder = await ModService.getLoadOrder();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
loadOrder
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set mod load order
|
||||
router.post('/load-order', async (req, res) => {
|
||||
try {
|
||||
const { modIds } = req.body;
|
||||
|
||||
if (!Array.isArray(modIds)) {
|
||||
return res.status(400).json({ error: 'modIds must be an array' });
|
||||
}
|
||||
|
||||
const success = await ModService.setLoadOrder(modIds);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set load order' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Load order updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error setting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server mod preferences
|
||||
router.get('/preferences/server', async (req, res) => {
|
||||
try {
|
||||
const preferences = await ModService.getServerPreferences();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
preferences
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting server preferences:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set server mod preference
|
||||
router.post('/preferences/server', async (req, res) => {
|
||||
try {
|
||||
const { key, value } = req.body;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: 'key and value are required' });
|
||||
}
|
||||
|
||||
const success = await ModService.setServerPreference(key, value);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set preference' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Preference set successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error setting server preference:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reload mods from filesystem
|
||||
router.post('/reload', async (req, res) => {
|
||||
try {
|
||||
await ModService.reloadMods();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mods reloaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error reloading mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
202
GameServer/server.js
Normal file
@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Game Server - Real-time Multiplayer
|
||||
* Handles actual game instances, player connections, and real-time gameplay
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const dotenv = require('dotenv');
|
||||
require('dotenv').config();
|
||||
|
||||
const logger = require('./utils/logger');
|
||||
const connectDB = require('./config/database');
|
||||
const { initializeGameSystems } = require('./systems/GameSystem');
|
||||
const SocketHandlers = require('./socket/socketHandlers');
|
||||
const ServerRegistrationService = require('./services/ServerRegistrationService');
|
||||
const ModService = require('./services/ModService');
|
||||
const modRoutes = require('./routes/mods');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://localhost:3000"],
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(compression());
|
||||
app.use(cors({
|
||||
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://localhost:3000"],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Game Server Routes (minimal - mostly for health checks and server management)
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'Game Server OK',
|
||||
service: 'galaxystrikeonline-game',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
activeServers: Object.keys(gameServers).length,
|
||||
connectedPlayers: connectedPlayers.size
|
||||
});
|
||||
});
|
||||
|
||||
// Get server status
|
||||
app.get('/api/game/status', (req, res) => {
|
||||
res.json({
|
||||
activeServers: Object.keys(gameServers).length,
|
||||
connectedPlayers: connectedPlayers.size,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Mod management routes
|
||||
app.use('/api/mods', modRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Global game server instances
|
||||
const gameServers = {};
|
||||
let serverRegistration; // Global reference to registration service
|
||||
|
||||
// Player tracking
|
||||
const connectedPlayers = new Set(); // Track actual player connections
|
||||
let socketHandlers;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
logger.info(`Game Server: Player connected - ${socket.id}`);
|
||||
socketHandlers.handleConnection(socket);
|
||||
});
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('Uncaught Exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// Graceful shutdown handlers
|
||||
async function gracefulShutdown(signal) {
|
||||
logger.info(`[GRACEFUL SHUTDOWN] Received ${signal}, shutting down gracefully...`);
|
||||
|
||||
try {
|
||||
// Stop accepting new connections
|
||||
server.close(async () => {
|
||||
logger.info('[GRACEFUL SHUTDOWN] HTTP server closed');
|
||||
|
||||
// Unregister from API
|
||||
if (serverRegistration) {
|
||||
await serverRegistration.stopHeartbeat();
|
||||
const unregistered = await serverRegistration.unregisterWithAPI();
|
||||
if (unregistered) {
|
||||
logger.info('[GRACEFUL SHUTDOWN] Server unregistered from API successfully');
|
||||
} else {
|
||||
logger.warn('[GRACEFUL SHUTDOWN] Failed to unregister from API');
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown mod service
|
||||
await ModService.shutdown();
|
||||
|
||||
// Close database connections
|
||||
const mongoose = require('mongoose');
|
||||
await mongoose.connection.close();
|
||||
logger.info('[GRACEFUL SHUTDOWN] Database connections closed');
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Force shutdown after 30 seconds
|
||||
setTimeout(() => {
|
||||
logger.error('[GRACEFUL SHUTDOWN] Forced shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GRACEFUL SHUTDOWN] Error during shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
// Handle Socket.IO adapter errors
|
||||
io.engine.on('connection_error', (err) => {
|
||||
logger.error('Socket.IO connection error:', err);
|
||||
});
|
||||
|
||||
io.of('/').adapter.on('error', (err) => {
|
||||
logger.error('Socket.IO adapter error:', err);
|
||||
});
|
||||
|
||||
// Initialize database and game systems
|
||||
async function startGameServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB();
|
||||
logger.info('Game Server: Database connected successfully');
|
||||
|
||||
// Initialize mod service
|
||||
await ModService.initialize();
|
||||
logger.info('Game Server: Mod service initialized');
|
||||
|
||||
// Initialize game systems
|
||||
await initializeGameSystems();
|
||||
logger.info('Game Server: Game systems initialized');
|
||||
|
||||
// Initialize server registration service
|
||||
const gameServerUrl = `https://api.korvarix.com:${process.env.GAME_PORT || 3002}`;
|
||||
const apiUrl = process.env.API_SERVER_URL || 'https://api.korvarix.com';
|
||||
const serverName = process.env.SERVER_NAME || 'Game Server';
|
||||
const serverRegion = process.env.SERVER_REGION || 'us-east';
|
||||
const maxPlayers = parseInt(process.env.MAX_PLAYERS) || 10;
|
||||
|
||||
serverRegistration = new ServerRegistrationService(gameServerUrl, apiUrl, serverName, serverRegion, maxPlayers);
|
||||
|
||||
// Set up player count callback
|
||||
serverRegistration.setPlayerCountCallback(() => connectedPlayers.size);
|
||||
|
||||
serverRegistration.startHeartbeat();
|
||||
|
||||
// Initialize socket handlers
|
||||
socketHandlers = new SocketHandlers(io, gameServers, connectedPlayers);
|
||||
|
||||
// Make registration service available to socket handlers
|
||||
socketHandlers.serverRegistration = serverRegistration;
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.GAME_PORT || 3002; // Game Server on port 3002
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`Game Server running on port ${PORT}`);
|
||||
logger.info('Game Server handles: Real-time Multiplayer, Game Instances, Socket.IO');
|
||||
logger.info(`Game Server Name: ${serverName}`);
|
||||
logger.info(`Game Server Region: ${serverRegion}`);
|
||||
logger.info(`Game Server URL: ${gameServerUrl}`);
|
||||
logger.info(`API Server URL: ${apiUrl}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start Game Server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startGameServer();
|
||||
|
||||
module.exports = { app, server, io, gameServers };
|
||||
264
GameServer/services/ModService.js
Normal file
@ -0,0 +1,264 @@
|
||||
const { ModModel, ModAssetModel, ServerModPreferencesModel } = require('../models/ModModels');
|
||||
const LocalDatabase = require('../config/LocalDatabase');
|
||||
const logger = require('../utils/logger');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class ModService {
|
||||
constructor() {
|
||||
this.modsDirectory = path.join(__dirname, '../../mods');
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Initialize local database
|
||||
await LocalDatabase.initialize();
|
||||
|
||||
// Create mods directory if it doesn't exist
|
||||
if (!fs.existsSync(this.modsDirectory)) {
|
||||
fs.mkdirSync(this.modsDirectory, { recursive: true });
|
||||
logger.info(`[MOD SERVICE] Created mods directory: ${this.modsDirectory}`);
|
||||
}
|
||||
|
||||
// Load existing mods from filesystem
|
||||
await this.loadModsFromFilesystem();
|
||||
|
||||
this.initialized = true;
|
||||
logger.info('[MOD SERVICE] Mod service initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('[MOD SERVICE] Failed to initialize mod service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadModsFromFilesystem() {
|
||||
try {
|
||||
if (!fs.existsSync(this.modsDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modFolders = fs.readdirSync(this.modsDirectory, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
for (const modFolder of modFolders) {
|
||||
await this.loadModFromFolder(modFolder);
|
||||
}
|
||||
|
||||
logger.info(`[MOD SERVICE] Loaded ${modFolders.length} mods from filesystem`);
|
||||
} catch (error) {
|
||||
logger.error('[MOD SERVICE] Error loading mods from filesystem:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadModFromFolder(modFolder) {
|
||||
try {
|
||||
const modPath = path.join(this.modsDirectory, modFolder);
|
||||
const manifestPath = path.join(modPath, 'mod.json');
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
logger.warn(`[MOD SERVICE] Mod ${modFolder} missing mod.json manifest`);
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
|
||||
// Validate required fields
|
||||
if (!manifest.name || !manifest.version || !manifest.author) {
|
||||
logger.warn(`[MOD SERVICE] Mod ${modFolder} has invalid manifest`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if mod already exists in database
|
||||
const existingMod = await ModModel.findByName(manifest.name);
|
||||
|
||||
// Calculate checksum of mod files
|
||||
const checksum = await this.calculateModChecksum(modPath);
|
||||
|
||||
const modData = {
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
author: manifest.author,
|
||||
description: manifest.description || '',
|
||||
filePath: modPath,
|
||||
checksum,
|
||||
dependencies: manifest.dependencies || [],
|
||||
config: manifest.config || {}
|
||||
};
|
||||
|
||||
if (existingMod) {
|
||||
// Update existing mod if checksum changed
|
||||
if (existingMod.checksum !== checksum) {
|
||||
await ModModel.update(existingMod.id, modData);
|
||||
logger.info(`[MOD SERVICE] Updated mod: ${manifest.name}`);
|
||||
}
|
||||
} else {
|
||||
// Create new mod
|
||||
await ModModel.create(modData);
|
||||
logger.info(`[MOD SERVICE] Installed new mod: ${manifest.name}`);
|
||||
}
|
||||
|
||||
// Load mod assets
|
||||
await this.loadModAssets(manifest.name, modPath);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading mod ${modFolder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadModAssets(modName, modPath) {
|
||||
try {
|
||||
const mod = await ModModel.findByName(modName);
|
||||
if (!mod) return;
|
||||
|
||||
// Clear existing assets for this mod
|
||||
await ModAssetModel.deleteByModId(mod.id);
|
||||
|
||||
// Load assets from assets folder
|
||||
const assetsPath = path.join(modPath, 'assets');
|
||||
if (fs.existsSync(assetsPath)) {
|
||||
await this.loadAssetsFromDirectory(mod.id, assetsPath);
|
||||
}
|
||||
|
||||
logger.info(`[MOD SERVICE] Loaded assets for mod: ${modName}`);
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading assets for mod ${modName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAssetsFromDirectory(modId, assetsPath) {
|
||||
const assetTypes = ['ships', 'items', 'quests', 'systems'];
|
||||
|
||||
for (const assetType of assetTypes) {
|
||||
const assetTypePath = path.join(assetsPath, assetType);
|
||||
|
||||
if (fs.existsSync(assetTypePath)) {
|
||||
const assetFiles = fs.readdirSync(assetTypePath);
|
||||
|
||||
for (const assetFile of assetFiles) {
|
||||
if (assetFile.endsWith('.json')) {
|
||||
try {
|
||||
const assetData = JSON.parse(
|
||||
fs.readFileSync(path.join(assetTypePath, assetFile), 'utf8')
|
||||
);
|
||||
|
||||
const assetId = path.basename(assetFile, '.json');
|
||||
|
||||
await ModAssetModel.create({
|
||||
modId,
|
||||
assetType: assetType.slice(0, -1), // Remove 's' for singular
|
||||
assetId,
|
||||
assetData
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading asset ${assetFile}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async calculateModChecksum(modPath) {
|
||||
try {
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
// Hash all files in mod directory
|
||||
const hashDirectory = (dir) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
hashDirectory(filePath);
|
||||
} else {
|
||||
const data = fs.readFileSync(filePath);
|
||||
hash.update(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
hashDirectory(modPath);
|
||||
return hash.digest('hex');
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error calculating checksum for ${modPath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
async getMods(enabledOnly = false) {
|
||||
return await ModModel.findAll(enabledOnly);
|
||||
}
|
||||
|
||||
async getMod(id) {
|
||||
return await ModModel.findById(id);
|
||||
}
|
||||
|
||||
async getModByName(name) {
|
||||
return await ModModel.findByName(name);
|
||||
}
|
||||
|
||||
async enableMod(id) {
|
||||
const result = await ModModel.enable(id);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Enabled mod: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async disableMod(id) {
|
||||
const result = await ModModel.disable(id);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Disabled mod: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getModAssets(assetType) {
|
||||
return await ModAssetModel.findByType(assetType);
|
||||
}
|
||||
|
||||
async getModAsset(assetType, assetId) {
|
||||
return await ModAssetModel.findByTypeAndId(assetType, assetId);
|
||||
}
|
||||
|
||||
async getLoadOrder() {
|
||||
return await ModModel.getLoadOrder();
|
||||
}
|
||||
|
||||
async setLoadOrder(modIds) {
|
||||
const result = await ModModel.setLoadOrder(modIds);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Updated mod load order`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getServerPreferences() {
|
||||
return await ServerModPreferencesModel.getAll();
|
||||
}
|
||||
|
||||
async setServerPreference(key, value) {
|
||||
return await ServerModPreferencesModel.set(key, value);
|
||||
}
|
||||
|
||||
async reloadMods() {
|
||||
logger.info('[MOD SERVICE] Reloading mods from filesystem...');
|
||||
await this.loadModsFromFilesystem();
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.initialized) {
|
||||
await LocalDatabase.close();
|
||||
this.initialized = false;
|
||||
logger.info('[MOD SERVICE] Mod service shut down');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ModService();
|
||||
161
GameServer/services/ServerRegistrationService.js
Normal file
@ -0,0 +1,161 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class ServerRegistrationService {
|
||||
constructor(gameServerUrl, apiUrl, serverName, serverRegion, maxPlayers = 10) {
|
||||
this.gameServerUrl = gameServerUrl;
|
||||
this.apiUrl = apiUrl;
|
||||
this.serverName = serverName;
|
||||
this.serverRegion = serverRegion;
|
||||
this.maxPlayers = maxPlayers;
|
||||
this.serverId = `gameserver_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.registrationInterval = null;
|
||||
this.isRegistered = false;
|
||||
this.getCurrentPlayerCount = null; // Callback to get current player count
|
||||
}
|
||||
|
||||
setPlayerCountCallback(callback) {
|
||||
this.getCurrentPlayerCount = callback;
|
||||
}
|
||||
|
||||
async registerWithAPI() {
|
||||
try {
|
||||
logger.info(`[SERVER REGISTRATION] Registering server ${this.serverId} with API at ${this.apiUrl}`);
|
||||
|
||||
const currentPlayers = this.getCurrentPlayerCount ? this.getCurrentPlayerCount() : 0;
|
||||
|
||||
const serverData = {
|
||||
serverId: this.serverId,
|
||||
name: this.serverName,
|
||||
type: 'public',
|
||||
region: this.serverRegion,
|
||||
maxPlayers: this.maxPlayers,
|
||||
currentPlayers: currentPlayers,
|
||||
gameServerUrl: this.gameServerUrl,
|
||||
owner: {
|
||||
userId: 'system',
|
||||
username: 'Game Server'
|
||||
}
|
||||
};
|
||||
|
||||
logger.info(`[SERVER REGISTRATION] Registering with ${currentPlayers} current players`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/api/servers/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
logger.info(`[SERVER REGISTRATION] Server registered successfully:`, result);
|
||||
this.isRegistered = true;
|
||||
return true;
|
||||
} else {
|
||||
const error = await response.text();
|
||||
logger.error(`[SERVER REGISTRATION] Failed to register server: ${error}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[SERVER REGISTRATION] Error registering server:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateServerStatus(currentPlayers, status) {
|
||||
if (!this.isRegistered) {
|
||||
logger.warn(`[SERVER REGISTRATION] Cannot update status - server not registered`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`[SERVER REGISTRATION] Updating server ${this.serverId} status:`, {
|
||||
currentPlayers,
|
||||
status
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/api/servers/update-status/${this.serverId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ currentPlayers, status })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
logger.info(`[SERVER REGISTRATION] Status updated successfully:`, result);
|
||||
return true;
|
||||
} else {
|
||||
const error = await response.text();
|
||||
logger.error(`[SERVER REGISTRATION] Failed to update status: ${error}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[SERVER REGISTRATION] Error updating server status:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async updatePlayerCount(playerCount) {
|
||||
return await this.updateServerStatus(playerCount, 'active');
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
// Register immediately
|
||||
this.registerWithAPI();
|
||||
|
||||
// Set up periodic registration updates (every 30 seconds)
|
||||
this.registrationInterval = setInterval(async () => {
|
||||
await this.registerWithAPI();
|
||||
}, 30000);
|
||||
|
||||
logger.info(`[SERVER REGISTRATION] Heartbeat started for server ${this.serverId}`);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.registrationInterval) {
|
||||
clearInterval(this.registrationInterval);
|
||||
this.registrationInterval = null;
|
||||
logger.info(`[SERVER REGISTRATION] Heartbeat stopped for server ${this.serverId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async unregisterWithAPI() {
|
||||
try {
|
||||
logger.info(`[SERVER REGISTRATION] Unregistering server ${this.serverId} from API at ${this.apiUrl}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/api/servers/unregister/${this.serverId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
logger.info(`[SERVER REGISTRATION] Server unregistered successfully:`, result);
|
||||
this.isRegistered = false;
|
||||
return true;
|
||||
} else {
|
||||
const error = await response.text();
|
||||
logger.error(`[SERVER REGISTRATION] Failed to unregister server: ${error}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[SERVER REGISTRATION] Error unregistering server:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getServerId() {
|
||||
return this.serverId;
|
||||
}
|
||||
|
||||
isServerRegistered() {
|
||||
return this.isRegistered;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerRegistrationService;
|
||||
393
GameServer/socket/socketHandlers.js
Normal file
@ -0,0 +1,393 @@
|
||||
/**
|
||||
* Socket Handlers - Manages real-time socket connections for game server
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const logger = require('../utils/logger');
|
||||
const { getGameSystem } = require('../systems/GameSystem');
|
||||
|
||||
class SocketHandlers {
|
||||
constructor(io, gameServers, connectedPlayers) {
|
||||
this.io = io;
|
||||
this.gameServers = gameServers;
|
||||
this.connectedPlayers = connectedPlayers; // Track actual player connections
|
||||
this.gameSystem = null;
|
||||
|
||||
// Track connected users to prevent duplicate connections
|
||||
this.connectedUsers = new Map(); // userId -> socket.id
|
||||
this.userSockets = new Map(); // socket.id -> userId
|
||||
|
||||
// Add connection cleanup interval
|
||||
this.startConnectionCleanup();
|
||||
}
|
||||
|
||||
startConnectionCleanup() {
|
||||
// Clean up stale connections every 30 seconds
|
||||
setInterval(() => {
|
||||
this.cleanupStaleConnections();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
cleanupStaleConnections() {
|
||||
logger.info(`[GAME SERVER] Checking ${this.connectedUsers.size} active connections...`);
|
||||
logger.info(`[GAME SERVER] Current tracked players: ${Array.from(this.connectedPlayers)}`);
|
||||
|
||||
let playersRemoved = 0;
|
||||
for (const [userId, socketId] of this.connectedUsers.entries()) {
|
||||
const socket = this.io.sockets.sockets.get(socketId);
|
||||
if (!socket || !socket.connected) {
|
||||
logger.warn(`[GAME SERVER] Cleaning up stale connection for user ${userId} (socket: ${socketId})`);
|
||||
this.connectedUsers.delete(userId);
|
||||
this.userSockets.delete(socketId);
|
||||
if (this.connectedPlayers.has(userId)) {
|
||||
this.connectedPlayers.delete(userId);
|
||||
playersRemoved++;
|
||||
logger.info(`[GAME SERVER] Removed stale player ${userId}. Players removed: ${playersRemoved}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[GAME SERVER] Cleanup complete. Players removed: ${playersRemoved}, Total players now: ${this.connectedPlayers.size}`);
|
||||
|
||||
// Update player count on API if players were removed
|
||||
if (playersRemoved > 0 && this.serverRegistration) {
|
||||
logger.info(`[GAME SERVER] Updating API player count after cleanup to: ${this.connectedPlayers.size}`);
|
||||
this.serverRegistration.updatePlayerCount(this.connectedPlayers.size);
|
||||
}
|
||||
}
|
||||
|
||||
async initializeGameSystem() {
|
||||
const { initializeGameSystems } = require('../systems/GameSystem');
|
||||
this.gameSystem = await initializeGameSystems();
|
||||
}
|
||||
|
||||
handleConnection(socket) {
|
||||
logger.info(`Game Server: Socket connected - ${socket.id}`);
|
||||
|
||||
// Authentication middleware
|
||||
socket.use(async (packet, next) => {
|
||||
try {
|
||||
const token = socket.handshake.auth.token;
|
||||
if (!token) {
|
||||
return next(new Error('Authentication required'));
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
socket.userId = decoded.userId;
|
||||
socket.email = decoded.email;
|
||||
|
||||
// Check if user is already connected from another client
|
||||
const existingSocketId = this.connectedUsers.get(decoded.userId);
|
||||
const wasAlreadyConnected = this.connectedPlayers.has(decoded.userId);
|
||||
|
||||
logger.info(`[GAME SERVER] User ${decoded.userId} (${decoded.email}) connecting. Was already connected: ${wasAlreadyConnected}, Existing socket: ${existingSocketId}`);
|
||||
|
||||
if (existingSocketId && existingSocketId !== socket.id) {
|
||||
logger.warn(`[GAME SERVER] User ${decoded.userId} attempting to connect from multiple clients. Disconnecting previous client.`);
|
||||
logger.warn(`[GAME SERVER] Existing socket: ${existingSocketId}, New socket: ${socket.id}`);
|
||||
|
||||
// Disconnect the previous client
|
||||
const previousSocket = this.io.sockets.sockets.get(existingSocketId);
|
||||
if (previousSocket) {
|
||||
logger.info(`[GAME SERVER] Force disconnecting previous socket: ${existingSocketId}`);
|
||||
previousSocket.emit('force_disconnect', {
|
||||
reason: 'Another client connected with your account',
|
||||
newSocketId: socket.id
|
||||
});
|
||||
previousSocket.disconnect(true);
|
||||
} else {
|
||||
logger.warn(`[GAME SERVER] Previous socket ${existingSocketId} not found in active connections`);
|
||||
// Clean up the stale mapping
|
||||
this.connectedUsers.delete(decoded.userId);
|
||||
this.userSockets.delete(existingSocketId);
|
||||
if (!wasAlreadyConnected) {
|
||||
this.connectedPlayers.delete(decoded.userId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info(`[GAME SERVER] New connection for user ${decoded.userId} (socket: ${socket.id})`);
|
||||
}
|
||||
|
||||
// Store user connection
|
||||
this.connectedUsers.set(decoded.userId, socket.id);
|
||||
this.userSockets.set(socket.id, decoded.userId);
|
||||
|
||||
// Add to connected players tracking only if not already there
|
||||
const wasNotAlreadyConnected = !this.connectedPlayers.has(decoded.userId);
|
||||
this.connectedPlayers.add(decoded.userId);
|
||||
|
||||
logger.info(`[GAME SERVER] User ${decoded.userId} added to tracking. Was not already connected: ${wasNotAlreadyConnected}, Total players: ${this.connectedPlayers.size}`);
|
||||
|
||||
// Update player count on API only if this is a new unique user
|
||||
if (this.serverRegistration && wasNotAlreadyConnected) {
|
||||
logger.info(`[GAME SERVER] Updating API player count to: ${this.connectedPlayers.size}`);
|
||||
this.serverRegistration.updatePlayerCount(this.connectedPlayers.size);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error(`Socket authentication failed: ${error.message}`);
|
||||
socket.emit('authError', { error: 'Authentication failed' });
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
socket.on('joinServer', (data) => this.handleJoinServer(socket, data));
|
||||
socket.on('leaveServer', (data) => this.handleLeaveServer(socket, data));
|
||||
socket.on('gameAction', (data) => this.handleGameAction(socket, data));
|
||||
socket.on('chatMessage', (data) => this.handleChatMessage(socket, data));
|
||||
socket.on('getPlayerList', (data) => this.handleGetPlayerList(socket, data));
|
||||
socket.on('disconnect', () => this.handleDisconnect(socket));
|
||||
}
|
||||
|
||||
async handleJoinServer(socket, data) {
|
||||
try {
|
||||
const { serverId, userId, username } = data;
|
||||
|
||||
// Verify user matches socket authentication
|
||||
if (socket.userId !== userId) {
|
||||
socket.emit('error', { message: 'User authentication mismatch' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or get game instance
|
||||
let gameInstance = this.gameSystem.getGameInstance(serverId);
|
||||
if (!gameInstance) {
|
||||
// This should ideally be handled by the API server
|
||||
// But for now, create a basic game instance
|
||||
gameInstance = this.gameSystem.createGameInstance({
|
||||
id: serverId,
|
||||
name: `Game ${serverId}`,
|
||||
type: 'public',
|
||||
region: 'us-east',
|
||||
maxPlayers: 10
|
||||
});
|
||||
}
|
||||
|
||||
// Join player to game instance
|
||||
const playerData = {
|
||||
userId: userId,
|
||||
username: username,
|
||||
currentShip: data.currentShip,
|
||||
stats: data.stats
|
||||
};
|
||||
|
||||
const joinedGame = this.gameSystem.joinGameInstance(socket, serverId, playerData);
|
||||
|
||||
if (joinedGame) {
|
||||
// Notify player of successful join
|
||||
socket.emit('joinedServer', {
|
||||
serverId: serverId,
|
||||
gameInstance: {
|
||||
id: joinedGame.id,
|
||||
name: joinedGame.name
|
||||
}
|
||||
});
|
||||
|
||||
// Notify other players
|
||||
socket.to(`game_${gameId}`).emit('playerJoined', {
|
||||
userId: socket.userId,
|
||||
username: socket.email || 'Player'
|
||||
});
|
||||
|
||||
logger.info(`Player ${socket.userId} joined game ${gameId}`);
|
||||
} else {
|
||||
socket.emit('error', { message: 'Failed to join game' });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Join game error: ${error.message}`);
|
||||
socket.emit('error', { message: 'Failed to join game' });
|
||||
}
|
||||
}
|
||||
|
||||
async handleLeaveGame(socket, data) {
|
||||
try {
|
||||
const { gameId } = data;
|
||||
|
||||
const leftGame = this.gameSystem.leaveGameInstance(socket, gameId);
|
||||
|
||||
if (leftGame) {
|
||||
// Notify player of successful leave
|
||||
socket.emit('leftGame', { gameId: gameId });
|
||||
|
||||
// Notify other players
|
||||
socket.to(`game_${gameId}`).emit('playerLeft', {
|
||||
userId: socket.userId,
|
||||
currentPlayers: leftGame.currentPlayers
|
||||
});
|
||||
|
||||
logger.info(`Player left game ${gameId}`);
|
||||
} else {
|
||||
socket.emit('error', { message: 'Failed to leave game' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Leave game error: ${error.message}`);
|
||||
socket.emit('error', { message: 'Failed to leave game' });
|
||||
}
|
||||
}
|
||||
|
||||
async handleGameAction(socket, data) {
|
||||
try {
|
||||
const { type, actionData } = data;
|
||||
|
||||
// Handle game action through game system
|
||||
const success = this.gameSystem.handlePlayerAction(socket, type, actionData);
|
||||
|
||||
if (!success) {
|
||||
socket.emit('error', { message: 'Failed to process game action' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Game action error: ${error.message}`);
|
||||
socket.emit('error', { message: 'Failed to process game action' });
|
||||
}
|
||||
}
|
||||
|
||||
async handleChatMessage(socket, data) {
|
||||
try {
|
||||
const { message } = data;
|
||||
|
||||
// Handle chat through game system
|
||||
const success = this.gameSystem.handlePlayerChat(socket, { message });
|
||||
|
||||
if (!success) {
|
||||
socket.emit('error', { message: 'Failed to send chat message' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Chat message error: ${error.message}`);
|
||||
socket.emit('error', { message: 'Failed to send chat message' });
|
||||
}
|
||||
}
|
||||
|
||||
async handleGetPlayerList(socket, data) {
|
||||
try {
|
||||
const { serverId } = data;
|
||||
this.sendPlayerList(socket, serverId);
|
||||
} catch (error) {
|
||||
logger.error(`Get player list error: ${error.message}`);
|
||||
socket.emit('error', { message: 'Failed to get player list' });
|
||||
}
|
||||
}
|
||||
|
||||
async sendPlayerList(socket, serverId) {
|
||||
const gameInstance = this.gameSystem.getGameInstance(serverId);
|
||||
|
||||
if (gameInstance) {
|
||||
const players = Array.from(gameInstance.players.values()).map(player => ({
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
joinedAt: player.joinedAt,
|
||||
isReady: player.isReady,
|
||||
stats: player.stats
|
||||
}));
|
||||
|
||||
socket.emit('playerList', {
|
||||
serverId: serverId,
|
||||
players: players,
|
||||
currentPlayers: gameInstance.currentPlayers,
|
||||
maxPlayers: gameInstance.maxPlayers
|
||||
});
|
||||
} else {
|
||||
socket.emit('error', { message: 'Game instance not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async handleDisconnect(socket) {
|
||||
try {
|
||||
logger.info(`Game Server: Socket disconnected - ${socket.id}`);
|
||||
|
||||
// Get user ID from socket
|
||||
const userId = this.userSockets.get(socket.id);
|
||||
if (userId) {
|
||||
logger.info(`[GAME SERVER] User ${userId} disconnecting (socket: ${socket.id})`);
|
||||
|
||||
// Remove from tracking maps
|
||||
this.connectedUsers.delete(userId);
|
||||
this.userSockets.delete(socket.id);
|
||||
|
||||
// Remove from connected players tracking
|
||||
const wasTracked = this.connectedPlayers.has(userId);
|
||||
this.connectedPlayers.delete(userId);
|
||||
|
||||
logger.info(`[GAME SERVER] User ${userId} removed from tracking. Was tracked: ${wasTracked}, Total players: ${this.connectedPlayers.size}`);
|
||||
|
||||
// Update player count on API only if user was being tracked
|
||||
if (this.serverRegistration && wasTracked) {
|
||||
logger.info(`[GAME SERVER] Updating API player count to: ${this.connectedPlayers.size}`);
|
||||
this.serverRegistration.updatePlayerCount(this.connectedPlayers.size);
|
||||
}
|
||||
|
||||
// Get player connection info
|
||||
const connection = this.gameSystem.getPlayerConnection(socket.id);
|
||||
if (connection && connection.gameId) {
|
||||
const gameId = connection.gameId;
|
||||
|
||||
// Remove player from game
|
||||
const success = this.gameSystem.removePlayerFromGame(gameId, userId);
|
||||
|
||||
if (success) {
|
||||
// Notify other players in the game
|
||||
const game = this.gameSystem.getGame(gameId);
|
||||
if (game) {
|
||||
this.io.to(gameId).emit('playerLeft', {
|
||||
userId,
|
||||
username: socket.email || 'Unknown',
|
||||
gameId,
|
||||
currentPlayers: game.currentPlayers
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Player ${userId} disconnected from game ${gameId}`);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Notify other players
|
||||
this.io.to(`game_${gameId}`).emit('playerLeft', {
|
||||
userId: socket.userId,
|
||||
currentPlayers: game.currentPlayers
|
||||
});
|
||||
logger.info(`Player ${socket.userId} disconnected from game ${gameId}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[GAME SERVER] User ${userId} fully disconnected and cleaned up`);
|
||||
} else {
|
||||
logger.warn(`[GAME SERVER] Unknown socket ${socket.id} disconnected without user mapping`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[GAME SERVER] Disconnect error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Server management methods
|
||||
getServerStatus() {
|
||||
const gameInstances = this.gameSystem.getAllGameInstances();
|
||||
|
||||
return {
|
||||
activeServers: gameInstances.length,
|
||||
connectedPlayers: this.connectedPlayers.size,
|
||||
};
|
||||
}
|
||||
|
||||
getConnectedUsers() {
|
||||
return Array.from(this.connectedUsers.keys());
|
||||
}
|
||||
|
||||
getUserCount() {
|
||||
return this.connectedUsers.size;
|
||||
}
|
||||
|
||||
broadcastToAll(event, data) {
|
||||
this.io.emit(event, data);
|
||||
}
|
||||
|
||||
broadcastToServer(serverId, event, data) {
|
||||
this.io.to(`game_${serverId}`).emit(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SocketHandlers;
|
||||
284
GameServer/systems/GameSystem.js
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Game System - Manages game instances and real-time multiplayer logic
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class GameSystem {
|
||||
constructor() {
|
||||
this.gameInstances = new Map(); // serverId -> game instance
|
||||
this.playerConnections = new Map(); // socketId -> player data
|
||||
}
|
||||
|
||||
async initializeGameSystems() {
|
||||
logger.info('Initializing Game Server systems...');
|
||||
|
||||
// Initialize any game systems needed
|
||||
this.initializeEconomySystem();
|
||||
this.initializeDungeonSystem();
|
||||
|
||||
logger.info('Game Server systems initialized successfully');
|
||||
}
|
||||
|
||||
initializeEconomySystem() {
|
||||
// Economy system initialization for game server
|
||||
logger.info('Economy System initialized for Game Server');
|
||||
}
|
||||
|
||||
initializeDungeonSystem() {
|
||||
// Dungeon system initialization for game server
|
||||
logger.info('Dungeon System initialized for Game Server');
|
||||
}
|
||||
|
||||
createGameInstance(serverData) {
|
||||
const gameId = serverData.id;
|
||||
|
||||
const gameInstance = {
|
||||
id: gameId,
|
||||
name: serverData.name,
|
||||
type: serverData.type,
|
||||
region: serverData.region,
|
||||
maxPlayers: serverData.maxPlayers,
|
||||
currentPlayers: 0,
|
||||
players: new Map(), // socketId -> player data
|
||||
gameState: {
|
||||
active: false,
|
||||
startTime: null,
|
||||
currentDungeon: null,
|
||||
enemies: new Map(),
|
||||
items: new Map(),
|
||||
environment: null
|
||||
},
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date()
|
||||
};
|
||||
|
||||
this.gameInstances.set(gameId, gameInstance);
|
||||
logger.info(`Game instance created: ${gameId} - ${serverData.name}`);
|
||||
|
||||
return gameInstance;
|
||||
}
|
||||
|
||||
joinGameInstance(socket, serverId, playerData) {
|
||||
const gameInstance = this.gameInstances.get(serverId);
|
||||
|
||||
if (!gameInstance) {
|
||||
logger.error(`Game instance not found: ${serverId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gameInstance.currentPlayers >= gameInstance.maxPlayers) {
|
||||
logger.warn(`Game instance full: ${serverId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add player to game instance
|
||||
gameInstance.players.set(socket.id, {
|
||||
socketId: socket.id,
|
||||
userId: playerData.userId,
|
||||
username: playerData.username,
|
||||
joinedAt: new Date(),
|
||||
isReady: false,
|
||||
ship: playerData.currentShip || null,
|
||||
stats: playerData.stats || { level: 1, health: 100 }
|
||||
});
|
||||
|
||||
// Track player connection
|
||||
this.playerConnections.set(socket.id, {
|
||||
gameId: serverId,
|
||||
socket: socket,
|
||||
data: playerData
|
||||
});
|
||||
|
||||
gameInstance.currentPlayers++;
|
||||
gameInstance.lastActivity = new Date();
|
||||
|
||||
// Join socket to game room
|
||||
socket.join(`game_${serverId}`);
|
||||
|
||||
logger.info(`Player ${playerData.username} joined game instance ${serverId}`);
|
||||
|
||||
return gameInstance;
|
||||
}
|
||||
|
||||
leaveGameInstance(socket, serverId) {
|
||||
const gameInstance = this.gameInstances.get(serverId);
|
||||
|
||||
if (!gameInstance) {
|
||||
logger.error(`Game instance not found: ${serverId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const player = gameInstance.players.get(socket.id);
|
||||
if (player) {
|
||||
gameInstance.players.delete(socket.id);
|
||||
gameInstance.currentPlayers--;
|
||||
gameInstance.lastActivity = new Date();
|
||||
|
||||
// Leave socket room
|
||||
socket.leave(`game_${serverId}`);
|
||||
|
||||
// Remove player connection tracking
|
||||
this.playerConnections.delete(socket.id);
|
||||
|
||||
logger.info(`Player ${player.username} left game instance ${serverId}`);
|
||||
|
||||
// Clean up empty game instances
|
||||
if (gameInstance.currentPlayers === 0) {
|
||||
this.cleanupGameInstance(serverId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
cleanupGameInstance(serverId) {
|
||||
const gameInstance = this.gameInstances.get(serverId);
|
||||
|
||||
if (gameInstance && gameInstance.currentPlayers === 0) {
|
||||
this.gameInstances.delete(serverId);
|
||||
logger.info(`Cleaned up empty game instance: ${serverId}`);
|
||||
}
|
||||
}
|
||||
|
||||
getGameInstance(serverId) {
|
||||
return this.gameInstances.get(serverId);
|
||||
}
|
||||
|
||||
getAllGameInstances() {
|
||||
return Array.from(this.gameInstances.values()).map(instance => ({
|
||||
id: instance.id,
|
||||
name: instance.name,
|
||||
type: instance.type,
|
||||
region: instance.region,
|
||||
currentPlayers: instance.currentPlayers,
|
||||
maxPlayers: instance.maxPlayers,
|
||||
gameState: instance.gameState.active ? 'active' : 'waiting',
|
||||
createdAt: instance.createdAt
|
||||
}));
|
||||
}
|
||||
|
||||
getPlayerConnection(socketId) {
|
||||
return this.playerConnections.get(socketId);
|
||||
}
|
||||
|
||||
// Game action handlers
|
||||
handlePlayerAction(socket, actionType, actionData) {
|
||||
const connection = this.playerConnections.get(socket.id);
|
||||
if (!connection) {
|
||||
logger.warn(`No connection found for socket: ${socket.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const gameInstance = this.gameInstances.get(connection.gameId);
|
||||
if (!gameInstance) {
|
||||
logger.warn(`No game instance found for player: ${socket.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const player = gameInstance.players.get(socket.id);
|
||||
if (!player) {
|
||||
logger.warn(`Player not found in game instance: ${socket.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle different action types
|
||||
switch (actionType) {
|
||||
case 'move':
|
||||
return this.handlePlayerMove(gameInstance, player, actionData);
|
||||
case 'attack':
|
||||
return this.handlePlayerAttack(gameInstance, player, actionData);
|
||||
case 'interact':
|
||||
return this.handlePlayerInteract(gameInstance, player, actionData);
|
||||
case 'chat':
|
||||
return this.handlePlayerChat(gameInstance, player, actionData);
|
||||
default:
|
||||
logger.warn(`Unknown action type: ${actionType}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayerMove(gameInstance, player, moveData) {
|
||||
// Update player position
|
||||
player.position = moveData.position;
|
||||
gameInstance.lastActivity = new Date();
|
||||
|
||||
// Broadcast movement to other players in the game
|
||||
const socket = this.playerConnections.get(player.socketId)?.socket;
|
||||
if (socket) {
|
||||
socket.to(`game_${gameInstance.id}`).emit('playerMoved', {
|
||||
playerId: player.socketId,
|
||||
username: player.username,
|
||||
position: moveData.position
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handlePlayerAttack(gameInstance, player, attackData) {
|
||||
// Handle combat logic
|
||||
logger.info(`Player ${player.username} attacked in game ${gameInstance.id}`);
|
||||
|
||||
// Broadcast attack to other players
|
||||
const socket = this.playerConnections.get(player.socketId)?.socket;
|
||||
if (socket) {
|
||||
socket.to(`game_${gameInstance.id}`).emit('playerAttacked', {
|
||||
playerId: player.socketId,
|
||||
username: player.username,
|
||||
target: attackData.target,
|
||||
damage: attackData.damage
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handlePlayerInteract(gameInstance, player, interactData) {
|
||||
// Handle interaction logic (picking up items, activating objects, etc.)
|
||||
logger.info(`Player ${player.username} interacted in game ${gameInstance.id}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handlePlayerChat(gameInstance, player, chatData) {
|
||||
// Handle chat messages
|
||||
const message = {
|
||||
playerId: player.socketId,
|
||||
username: player.username,
|
||||
message: chatData.message,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Broadcast chat to all players in the game
|
||||
const socket = this.playerConnections.get(player.socketId)?.socket;
|
||||
if (socket) {
|
||||
socket.to(`game_${gameInstance.id}`).emit('chatMessage', message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let gameSystem = null;
|
||||
|
||||
async function initializeGameSystems() {
|
||||
if (!gameSystem) {
|
||||
gameSystem = new GameSystem();
|
||||
await gameSystem.initializeGameSystems();
|
||||
}
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
function getGameSystem() {
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GameSystem,
|
||||
initializeGameSystems,
|
||||
getGameSystem
|
||||
};
|
||||
27
GameServer/utils/logger.js
Normal file
@ -0,0 +1,27 @@
|
||||
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()
|
||||
),
|
||||
defaultMeta: { service: 'galaxystrikeonline-game-server' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
]
|
||||
});
|
||||
|
||||
// If not in production, also log to console
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||