initial github push

This commit is contained in:
Robert MacRae 2026-01-24 16:47:19 -04:00
parent d943d1753d
commit cdc7e2035f
124 changed files with 60510 additions and 0 deletions

18
API/.env.example Normal file
View 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
View 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
View 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
};

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

41
API/package.json Normal file
View 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
View 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
View 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
View 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
View 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;

View 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
View 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;

View 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
View 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
View 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
View 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();

View 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;

View 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
View 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
View 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
View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

451
Client/electron-main.js Normal file
View 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
View 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>

View 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
View 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;
}

View 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');

View 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');

View 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

File diff suppressed because it is too large Load Diff

1558
Client/js/core/GameEngine.js Normal file

File diff suppressed because it is too large Load Diff

1134
Client/js/core/Inventory.js Normal file

File diff suppressed because it is too large Load Diff

306
Client/js/core/Logger.js Normal file
View 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
View 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;
}
}
}

View 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
View 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
View 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);
}
});

File diff suppressed because it is too large Load Diff

View 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;
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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

File diff suppressed because it is too large Load Diff

1251
Client/js/ui/MainMenu.js Normal file

File diff suppressed because it is too large Load Diff

2321
Client/js/ui/UIManager.js Normal file

File diff suppressed because it is too large Load Diff

4688
Client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

127
Client/package.json Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

2544
Client/styles/main.css Normal file

File diff suppressed because it is too large Load Diff

842
Client/styles/tables.css Normal file
View 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;
}
}

View 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();

View 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

Binary file not shown.

View 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 };

View 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

File diff suppressed because it is too large Load Diff

39
GameServer/package.json Normal file
View 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
View 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
View 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 };

View 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();

View 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;

View 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;

View 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
};

View 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;

Some files were not shown because too many files have changed in this diff Show More