full re-write now ready for proper share
This commit is contained in:
parent
68f854be38
commit
3c11d27916
157
.gitignore
vendored
157
.gitignore
vendored
@ -1,139 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
**/node_modules/
|
||||
**/package-lock.json
|
||||
**/npm-debug.log*
|
||||
**/yarn-error.log*
|
||||
|
||||
game-server/*.sqlite
|
||||
game-server/*.sqlite-shm
|
||||
game-server/*.sqlite-wal
|
||||
|
||||
**/.env
|
||||
**/.env.local
|
||||
**/.env.development.local
|
||||
**/.env.test.local
|
||||
**/.env.production.local
|
||||
|
||||
**/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
client/dist/
|
||||
client/build/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite logs files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
game-server-*
|
||||
@ -1,21 +0,0 @@
|
||||
# Galaxy Strike Online - API Server Environment
|
||||
# Copy to .env and fill in values
|
||||
|
||||
# Server
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
|
||||
# Game Server URL (for server browser)
|
||||
GAME_SERVER_URL=http://localhost:3002
|
||||
|
||||
# Client URL (for CORS)
|
||||
CLIENT_URL=http://localhost:3000
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
@ -1,19 +0,0 @@
|
||||
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;
|
||||
@ -1,131 +0,0 @@
|
||||
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
|
||||
};
|
||||
@ -1,134 +0,0 @@
|
||||
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
|
||||
};
|
||||
@ -1,134 +0,0 @@
|
||||
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: 100
|
||||
},
|
||||
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);
|
||||
@ -1,306 +0,0 @@
|
||||
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);
|
||||
@ -1,155 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
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 (simplified for API server)
|
||||
stats: {
|
||||
level: { type: Number, default: 1 },
|
||||
experience: { type: Number, default: 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 (simplified for API server)
|
||||
playerSchema.methods.addExperience = function(amount) {
|
||||
this.stats.experience += amount;
|
||||
return this.stats.experience;
|
||||
};
|
||||
|
||||
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);
|
||||
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* API/models/PlayerData.js
|
||||
*
|
||||
* Minimal PlayerData model for the API server — only what the payment webhook
|
||||
* needs to read and credit gems. Shares the 'playerdatas' collection with
|
||||
* the GameServer's full PlayerData model (same MongoDB, same schema name).
|
||||
*
|
||||
* Do NOT add complex game logic here — that belongs in GameServer.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Minimal schema — Mixed types let us read/write without strict field validation
|
||||
const playerDataSchema = new mongoose.Schema({
|
||||
userId: { type: String, required: true, unique: true },
|
||||
username: { type: String },
|
||||
stats: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}, {
|
||||
strict: false, // allow all fields to be stored (don't strip unknown fields)
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// Prevent model re-registration error on hot-reload
|
||||
let PlayerData;
|
||||
try {
|
||||
PlayerData = mongoose.model('PlayerData');
|
||||
} catch (e) {
|
||||
PlayerData = mongoose.model('PlayerData', playerDataSchema);
|
||||
}
|
||||
|
||||
module.exports = PlayerData;
|
||||
@ -1,189 +0,0 @@
|
||||
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
6068
API/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,23 @@
|
||||
{
|
||||
"name": "galaxystrikeonline-server",
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"description": "Galaxy Strike Online - Server Backend",
|
||||
"license": "MIT",
|
||||
"author": "Korvarix Studios",
|
||||
"type": "commonjs",
|
||||
"main": "server.js",
|
||||
"description": "",
|
||||
"main": "index.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"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node ./src/index.js"
|
||||
},
|
||||
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"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"
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mongoose": "^9.3.0",
|
||||
"socket.io": "^4.8.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,214 +0,0 @@
|
||||
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;
|
||||
@ -1,173 +0,0 @@
|
||||
/**
|
||||
* routes/payments.js — GSO Premium Gem Store (GDD Phase 3 Monetisation)
|
||||
*
|
||||
* Endpoints:
|
||||
* POST /api/payments/create-checkout — create Stripe Checkout session
|
||||
* POST /api/payments/webhook — Stripe webhook (payment confirmation)
|
||||
* GET /api/payments/products — list purchasable gem packages
|
||||
*
|
||||
* Requires ENV vars:
|
||||
* STRIPE_SECRET_KEY — sk_live_... (or sk_test_... for development)
|
||||
* STRIPE_WEBHOOK_SECRET — whsec_... (from Stripe Dashboard → Webhooks)
|
||||
* CLIENT_URL — https://galaxystrike.online (for redirect URLs)
|
||||
*
|
||||
* Stripe is loaded lazily so the server starts even if the package isn't installed yet.
|
||||
* Run: npm install stripe in the API directory before going live.
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// ── Gem packages available for purchase ────────────────────────────────────────
|
||||
const GEM_PACKAGES = [
|
||||
{ id: 'gems_80', gems: 80, priceUSD: 0.99, label: 'Starter Pack', bonus: 0, popular: false },
|
||||
{ id: 'gems_200', gems: 200, priceUSD: 1.99, label: 'Pilot Pack', bonus: 0, popular: false },
|
||||
{ id: 'gems_500', gems: 500, priceUSD: 4.99, label: 'Commander Pack', bonus: 50, popular: true },
|
||||
{ id: 'gems_1200', gems: 1200, priceUSD: 9.99, label: 'Admiral Pack', bonus: 200,popular: false },
|
||||
{ id: 'gems_2500', gems: 2500, priceUSD: 19.99, label: 'Fleet Admiral', bonus: 500,popular: false },
|
||||
{ id: 'gems_6500', gems: 6500, priceUSD: 49.99, label: 'Grand Admiral', bonus: 1500,popular: false},
|
||||
];
|
||||
|
||||
// ── Helper: load Stripe lazily ────────────────────────────────────────────────
|
||||
function getStripe() {
|
||||
if (!process.env.STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not configured');
|
||||
// eslint-disable-next-line global-require
|
||||
return require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||
}
|
||||
|
||||
// ── GET /api/payments/products ────────────────────────────────────────────────
|
||||
router.get('/products', (req, res) => {
|
||||
res.json({ packages: GEM_PACKAGES });
|
||||
});
|
||||
|
||||
// ── POST /api/payments/create-checkout ────────────────────────────────────────
|
||||
// Body: { packageId, userId, username }
|
||||
router.post('/create-checkout', async (req, res) => {
|
||||
try {
|
||||
const { packageId, userId, username } = req.body;
|
||||
if (!packageId || !userId) return res.status(400).json({ error: 'packageId and userId required' });
|
||||
|
||||
const pkg = GEM_PACKAGES.find(p => p.id === packageId);
|
||||
if (!pkg) return res.status(404).json({ error: 'Unknown package' });
|
||||
|
||||
const stripe = getStripe();
|
||||
const totalGems = pkg.gems + pkg.bonus;
|
||||
const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000';
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: 'payment',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [{
|
||||
price_data: {
|
||||
currency: 'usd',
|
||||
unit_amount: Math.round(pkg.priceUSD * 100), // cents
|
||||
product_data: {
|
||||
name: `${pkg.label} — ${totalGems} 💎 Gems`,
|
||||
description: pkg.bonus > 0
|
||||
? `${pkg.gems} gems + ${pkg.bonus} bonus gems (${totalGems} total)`
|
||||
: `${pkg.gems} Galaxy Strike Online gems`,
|
||||
metadata: { packageId: pkg.id },
|
||||
},
|
||||
},
|
||||
quantity: 1,
|
||||
}],
|
||||
client_reference_id: userId,
|
||||
customer_email: req.body.email || undefined,
|
||||
metadata: {
|
||||
userId,
|
||||
username: username || 'unknown',
|
||||
packageId: pkg.id,
|
||||
gems: String(totalGems),
|
||||
},
|
||||
success_url: `${clientUrl}/payment-success?session={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${clientUrl}/payment-cancel`,
|
||||
});
|
||||
|
||||
res.json({ sessionId: session.id, url: session.url });
|
||||
} catch (err) {
|
||||
console.error('[PAYMENTS] create-checkout error:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /api/payments/webhook ────────────────────────────────────────────────
|
||||
// Stripe sends raw body — must use express.raw() middleware for this route.
|
||||
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
||||
const sig = req.headers['stripe-signature'];
|
||||
const secret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
|
||||
if (!secret) {
|
||||
console.error('[PAYMENTS] STRIPE_WEBHOOK_SECRET not set — webhook rejected');
|
||||
return res.status(400).send('Webhook secret not configured');
|
||||
}
|
||||
|
||||
let event;
|
||||
try {
|
||||
const stripe = getStripe();
|
||||
event = stripe.webhooks.constructEvent(req.body, sig, secret);
|
||||
} catch (err) {
|
||||
console.error('[PAYMENTS] Webhook signature verification failed:', err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object;
|
||||
const { userId, username, packageId, gems } = session.metadata || {};
|
||||
|
||||
if (!userId || !gems) {
|
||||
console.error('[PAYMENTS] Webhook missing metadata', session.metadata);
|
||||
return res.json({ received: true });
|
||||
}
|
||||
|
||||
const gemCount = parseInt(gems, 10);
|
||||
const amountCents = session.amount_total || 0;
|
||||
|
||||
console.log(`[PAYMENTS] ✅ Payment confirmed: ${username} (${userId}) — ${gemCount} gems — $${(amountCents/100).toFixed(2)}`);
|
||||
|
||||
// Credit gems in the database directly
|
||||
try {
|
||||
const PlayerData = require('../models/PlayerData');
|
||||
const player = await PlayerData.findOne({ userId });
|
||||
if (player) {
|
||||
player.stats.gems = (player.stats.gems || 0) + gemCount;
|
||||
player.markModified('stats');
|
||||
await player.save();
|
||||
console.log(`[PAYMENTS] Credited ${gemCount} gems to ${username}. New balance: ${player.stats.gems}`);
|
||||
} else {
|
||||
console.warn(`[PAYMENTS] Player not found for userId: ${userId}`);
|
||||
}
|
||||
} catch (dbErr) {
|
||||
console.error('[PAYMENTS] DB credit error:', dbErr.message);
|
||||
// Return 200 to Stripe regardless — Stripe will retry on 5xx
|
||||
}
|
||||
|
||||
// Attempt to push live notification to game server via internal HTTP
|
||||
// (Game server will push 'gems_credited' to the player's socket if online)
|
||||
const gameServerUrl = process.env.GAME_SERVER_URL || 'http://localhost:3002';
|
||||
try {
|
||||
const http = require('http');
|
||||
const body = JSON.stringify({ userId, gems: gemCount, packageId, amountCents });
|
||||
const options = {
|
||||
hostname: new URL(gameServerUrl).hostname,
|
||||
port: new URL(gameServerUrl).port || 3002,
|
||||
path: '/internal/credit-gems',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'X-Internal-Key': process.env.INTERNAL_API_KEY || 'gso-internal',
|
||||
},
|
||||
};
|
||||
const req2 = http.request(options);
|
||||
req2.write(body);
|
||||
req2.end();
|
||||
} catch (notifyErr) {
|
||||
// Non-fatal — player will see updated gems on next login
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports.GEM_PACKAGES = GEM_PACKAGES;
|
||||
@ -1,419 +0,0 @@
|
||||
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;
|
||||
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -1,50 +0,0 @@
|
||||
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;
|
||||
@ -1,71 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -1,196 +0,0 @@
|
||||
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;
|
||||
253
API/server.js
253
API/server.js
@ -1,253 +0,0 @@
|
||||
const express = require('express');
|
||||
const errorReporter = require('./utils/ErrorReporter');
|
||||
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 paymentRoutes = require('./routes/payments');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
const GameServer = require('./models/GameServer');
|
||||
|
||||
// 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());
|
||||
const allowedOrigins = [
|
||||
"https://galaxystrike.online",
|
||||
"https://api.korvarix.com",
|
||||
"http://api.korvarix.com:3001",
|
||||
"https://dev.gameserver.galaxystrike.online",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
...(process.env.CLIENT_URL ? [process.env.CLIENT_URL] : []),
|
||||
];
|
||||
app.use(cors({
|
||||
origin: (origin, callback) => {
|
||||
// Allow no-origin (Electron, mobile, curl) + whitelisted origins
|
||||
if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
|
||||
return callback(null, false);
|
||||
},
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(errorReporter.requestMiddleware());
|
||||
app.get('/health', (req, res) => res.json({ status: 'ok', service: 'API', ...errorReporter.getMetrics() }));
|
||||
|
||||
// Static file serving
|
||||
app.use(express.static('../Website/dist'));
|
||||
|
||||
// Rate limiting (more lenient for development)
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const limiter = new RateLimiterMemory({
|
||||
keyGenerator: (req) => req.ip,
|
||||
points: 1000, // limit each IP to 1000 requests per windowMs (increased from 100)
|
||||
duration: 60, // 1 minute window (reduced from 15 minutes)
|
||||
blockDuration: 60, // Block for 1 minute (reduced from 15 minutes)
|
||||
});
|
||||
|
||||
app.use('/api/', async (req, res, next) => {
|
||||
try {
|
||||
// Skip rate limiting for localhost in development
|
||||
const isLocalhost = req.ip === '127.0.0.1' || req.ip === '::1' || req.hostname === 'localhost';
|
||||
|
||||
if (!isLocalhost) {
|
||||
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 (rejRes) {
|
||||
// Handle rate limit exceeded
|
||||
const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;
|
||||
res.set('Retry-After', String(secs));
|
||||
res.status(429).json({ error: 'Too many requests, please try again later.' });
|
||||
}
|
||||
});
|
||||
|
||||
// Routes - API Server Only (Auth + Server Browser)
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/servers', serverRoutes);
|
||||
app.use('/api/payments', paymentRoutes);
|
||||
|
||||
// Manual cleanup endpoint (for testing)
|
||||
app.post('/api/admin/cleanup-dead-servers', async (req, res) => {
|
||||
try {
|
||||
await cleanupDeadServers();
|
||||
res.json({ success: true, message: 'Dead server cleanup completed' });
|
||||
} catch (error) {
|
||||
logger.error('Manual cleanup error:', error);
|
||||
res.status(500).json({ success: false, error: 'Cleanup failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// Clean up dead servers
|
||||
async function cleanupDeadServers() {
|
||||
try {
|
||||
logger.info('[API SERVER] Starting dead server cleanup...');
|
||||
|
||||
// Find servers that haven't been updated in the last 5 minutes
|
||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
||||
|
||||
const deadServers = await GameServer.find({
|
||||
lastActivity: { $lt: fiveMinutesAgo }
|
||||
});
|
||||
|
||||
if (deadServers.length > 0) {
|
||||
logger.info(`[API SERVER] Found ${deadServers.length} potentially dead servers, checking...`);
|
||||
|
||||
for (const server of deadServers) {
|
||||
const isAlive = await checkServerHealth(server.gameServerUrl);
|
||||
|
||||
if (!isAlive) {
|
||||
logger.info(`[API SERVER] Removing dead server: ${server.name} (${server.serverId})`);
|
||||
await GameServer.deleteOne({ _id: server._id });
|
||||
} else {
|
||||
logger.info(`[API SERVER] Server ${server.name} is still alive, updating lastActivity`);
|
||||
server.lastActivity = new Date();
|
||||
await server.save();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info('[API SERVER] No dead servers found');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error during dead server cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a server is healthy
|
||||
async function checkServerHealth(serverUrl) {
|
||||
try {
|
||||
if (!serverUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add /health endpoint to the URL
|
||||
const healthUrl = serverUrl.endsWith('/') ? `${serverUrl}health` : `${serverUrl}/health`;
|
||||
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
timeout: 5000 // 5 second timeout
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
logger.warn(`[API SERVER] Health check failed for ${serverUrl}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 || 3000;
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`API Server running on port ${PORT}`);
|
||||
logger.info('API Server handles: Authentication, Server Browser, User Data');
|
||||
|
||||
// Start dead server cleanup (every 2 minutes)
|
||||
setInterval(cleanupDeadServers, 120000);
|
||||
});
|
||||
} 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 };
|
||||
@ -1,220 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,119 +0,0 @@
|
||||
/**
|
||||
* GSO Error Reporter & Analytics (GDD Phase 3 — v3.2 lightweight implementation)
|
||||
* Logs structured errors to file + provides in-memory analytics counters.
|
||||
* Can be wired to external services (Sentry, Datadog) by replacing _sendToExternal().
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class ErrorReporter {
|
||||
constructor(options = {}) {
|
||||
this.logDir = options.logDir || path.join(__dirname, '../logs');
|
||||
this.service = options.service || 'GameServer';
|
||||
this.env = process.env.NODE_ENV || 'development';
|
||||
this.counters = {}; // event_type → count
|
||||
this.errors = []; // last 100 errors in memory
|
||||
this.maxErrors = 100;
|
||||
|
||||
// Ensure logs directory exists
|
||||
if (!fs.existsSync(this.logDir)) {
|
||||
fs.mkdirSync(this.logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error capture ──────────────────────────────────────────────────────────
|
||||
captureError(err, context = {}) {
|
||||
const entry = {
|
||||
ts: new Date().toISOString(),
|
||||
service: this.service,
|
||||
env: this.env,
|
||||
message: err?.message || String(err),
|
||||
stack: err?.stack || null,
|
||||
context,
|
||||
};
|
||||
|
||||
// Store in memory ring buffer
|
||||
this.errors.push(entry);
|
||||
if (this.errors.length > this.maxErrors) this.errors.shift();
|
||||
|
||||
// Write to daily log file (non-blocking)
|
||||
const logFile = path.join(this.logDir, `errors-${new Date().toISOString().slice(0,10)}.log`);
|
||||
const line = JSON.stringify(entry) + '\n';
|
||||
fs.appendFile(logFile, line, () => {}); // fire-and-forget
|
||||
|
||||
// Increment counter
|
||||
this.increment('errors.total');
|
||||
|
||||
// Hook for external service (Sentry, etc.)
|
||||
this._sendToExternal(entry);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
// ── Analytics event tracking ───────────────────────────────────────────────
|
||||
track(event, data = {}) {
|
||||
this.increment(event);
|
||||
if (this.env === 'development') {
|
||||
const logFile = path.join(this.logDir, `analytics-${new Date().toISOString().slice(0,10)}.log`);
|
||||
fs.appendFile(logFile, JSON.stringify({ ts: new Date().toISOString(), event, data }) + '\n', () => {});
|
||||
}
|
||||
}
|
||||
|
||||
increment(key) {
|
||||
this.counters[key] = (this.counters[key] || 0) + 1;
|
||||
}
|
||||
|
||||
// ── Metrics snapshot ───────────────────────────────────────────────────────
|
||||
getMetrics() {
|
||||
return {
|
||||
service: this.service,
|
||||
uptime: Math.round(process.uptime()),
|
||||
memoryMB: Math.round(process.memoryUsage().heapUsed / 1048576),
|
||||
counters: { ...this.counters },
|
||||
recentErrors: this.errors.slice(-10).map(e => ({ ts: e.ts, message: e.message, context: e.context })),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Express middleware ─────────────────────────────────────────────────────
|
||||
requestMiddleware() {
|
||||
return (req, res, next) => {
|
||||
const start = Date.now();
|
||||
this.increment('http.requests');
|
||||
res.on('finish', () => {
|
||||
const ms = Date.now() - start;
|
||||
this.increment(`http.${res.statusCode >= 400 ? 'errors' : 'success'}`);
|
||||
if (ms > 2000) this.track('http.slow_request', { path: req.path, ms });
|
||||
});
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
errorMiddleware() {
|
||||
return (err, req, res, next) => {
|
||||
this.captureError(err, { path: req.path, method: req.method });
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
};
|
||||
}
|
||||
|
||||
// ── Socket.IO event tracking helper ───────────────────────────────────────
|
||||
trackSocketEvent(eventName, userId) {
|
||||
this.increment(`socket.${eventName}`);
|
||||
this.increment('socket.total');
|
||||
}
|
||||
|
||||
// ── External service stub ──────────────────────────────────────────────────
|
||||
_sendToExternal(entry) {
|
||||
// Replace with: Sentry.captureException(new Error(entry.message))
|
||||
// or: axios.post(process.env.ERROR_WEBHOOK_URL, entry)
|
||||
// Currently: no-op in development; log to console in production
|
||||
if (this.env === 'production') {
|
||||
console.error('[ErrorReporter]', entry.message, entry.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const reporter = new ErrorReporter({ service: 'GameServer' });
|
||||
|
||||
module.exports = reporter;
|
||||
@ -1,27 +0,0 @@
|
||||
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;
|
||||
@ -1,120 +0,0 @@
|
||||
{
|
||||
"_readme": [
|
||||
"Galaxy Strike Online — Starbase World Layout",
|
||||
"Edit this file to customize your starbase. Changes take effect next time you visit the Starbases tab.",
|
||||
"",
|
||||
"GRID",
|
||||
" cols / rows : overall size of the world (min 8×8, max ~32×26 before performance drops)",
|
||||
"",
|
||||
"STYLE (global defaults — all optional hex strings)",
|
||||
" wallColor / wallColorLeft / wallColorRight / wallColorTop",
|
||||
" floorColorEven / floorColorOdd",
|
||||
" doorColor / doorFrameColor",
|
||||
"",
|
||||
"WALLS — each entry draws a run of wall tiles",
|
||||
" col, row : start position",
|
||||
" span : number of tiles (default 1)",
|
||||
" dir : 'h' horizontal | 'v' vertical",
|
||||
" color / colorLeft / colorRight / colorTop : per-segment color overrides",
|
||||
"",
|
||||
"DOORS — walkable openings; panel slides up when player is adjacent",
|
||||
" col, row : position",
|
||||
" color : panel color override",
|
||||
" frameColor : pillar/frame color override",
|
||||
"",
|
||||
"ROOMS — named regions; rendered as ghost labels + used for per-room wallpapers",
|
||||
" id : unique string (used by the unlock / wallpaper system)",
|
||||
" label : display text",
|
||||
" bounds : { col, row, cols, rows }",
|
||||
" unlock : item id required to unlock this room (omit = always open)",
|
||||
" Locked rooms are filled with sealed-wall tiles and shown as 'LOCKED'",
|
||||
"",
|
||||
"PLAYER START",
|
||||
" col, row : spawn tile (must be walkable floor)"
|
||||
],
|
||||
|
||||
"name": "Starbase Alpha-7",
|
||||
|
||||
"grid": { "cols": 26, "rows": 20 },
|
||||
|
||||
"style": {
|
||||
"wallColor": "#00d4ff",
|
||||
"wallColorLeft": "#0c1626",
|
||||
"wallColorRight": "#0a1220",
|
||||
"wallColorTop": "#1a2840",
|
||||
"floorColorEven": "#151c2e",
|
||||
"floorColorOdd": "#111827",
|
||||
"doorColor": "#00ffcc",
|
||||
"doorFrameColor": "#00d4ff"
|
||||
},
|
||||
|
||||
"walls": [
|
||||
{ "col": 0, "row": 0, "span": 26, "dir": "h", "_": "top wall" },
|
||||
{ "col": 0, "row": 19, "span": 26, "dir": "h", "_": "bottom wall" },
|
||||
{ "col": 0, "row": 0, "span": 20, "dir": "v", "_": "left wall" },
|
||||
{ "col": 25, "row": 0, "span": 20, "dir": "v", "_": "right wall" },
|
||||
|
||||
{ "col": 1, "row": 6, "span": 24, "dir": "h", "_": "main hall separator",
|
||||
"color": "#00d4ff", "colorLeft": "#0d1a2e", "colorRight": "#0a1525", "colorTop": "#182a42" },
|
||||
|
||||
{ "col": 7, "row": 7, "span": 13, "dir": "v", "_": "left inner wall" },
|
||||
{ "col": 17, "row": 7, "span": 2, "dir": "v", "_": "right stub top" },
|
||||
{ "col": 17, "row": 10, "span": 10, "dir": "v", "_": "right stub bottom",
|
||||
"color": "#4488ff", "colorLeft": "#0a1830", "colorRight": "#080e20", "colorTop": "#102040" },
|
||||
|
||||
{ "col": 7, "row": 13, "span": 10, "dir": "h", "_": "operations divider",
|
||||
"color": "#ff00ff", "colorLeft": "#1a0a20", "colorRight": "#120616", "colorTop": "#200a30" },
|
||||
|
||||
{ "col": 17, "row": 13, "span": 6, "dir": "h", "_": "vault corridor wall",
|
||||
"color": "#ffcc00", "colorLeft": "#1a1200", "colorRight": "#140e00", "colorTop": "#221800" }
|
||||
],
|
||||
|
||||
"doors": [
|
||||
{ "col": 13, "row": 6, "dir": "h", "_": "main hall → command centre" },
|
||||
{ "col": 17, "row": 6, "dir": "h", "color": "#4488ff", "frameColor": "#0066ff", "_": "main hall → right wing" },
|
||||
{ "col": 7, "row": 10, "dir": "v", "_": "left wing ↔ command centre" },
|
||||
{ "col": 17, "row": 9, "dir": "v", "color": "#ff88ff", "frameColor": "#cc44cc", "_": "command centre ↔ right wing" },
|
||||
{ "col": 7, "row": 15, "dir": "v", "_": "left wing → operations" },
|
||||
{ "col": 13, "row": 13, "dir": "h", "color": "#ff00ff", "frameColor": "#cc00cc", "_": "command centre → operations" },
|
||||
{ "col": 20, "row": 13, "dir": "h", "color": "#ffcc00", "frameColor": "#ddaa00", "_": "right wing → vault corridor" }
|
||||
],
|
||||
|
||||
"rooms": [
|
||||
{
|
||||
"id": "main_hall",
|
||||
"label": "Main Hall",
|
||||
"bounds": { "col": 1, "row": 1, "cols": 24, "rows": 5 }
|
||||
},
|
||||
{
|
||||
"id": "left_wing",
|
||||
"label": "Armory Wing",
|
||||
"bounds": { "col": 1, "row": 7, "cols": 6, "rows": 12 },
|
||||
"unlock": "room_armory"
|
||||
},
|
||||
{
|
||||
"id": "command_centre",
|
||||
"label": "Command Centre",
|
||||
"bounds": { "col": 8, "row": 7, "cols": 9, "rows": 6 }
|
||||
},
|
||||
{
|
||||
"id": "right_wing",
|
||||
"label": "Research Lab",
|
||||
"bounds": { "col": 18, "row": 7, "cols": 7, "rows": 6 },
|
||||
"unlock": "room_research_lab"
|
||||
},
|
||||
{
|
||||
"id": "operations",
|
||||
"label": "Operations Centre",
|
||||
"bounds": { "col": 8, "row": 14, "cols": 9, "rows": 5 },
|
||||
"unlock": "room_operations"
|
||||
},
|
||||
{
|
||||
"id": "commanders_vault",
|
||||
"label": "Commander's Vault",
|
||||
"bounds": { "col": 18, "row": 14, "cols": 7, "rows": 5 },
|
||||
"unlock": "room_vault"
|
||||
}
|
||||
],
|
||||
|
||||
"playerStart": { "col": 13, "row": 3 }
|
||||
}
|
||||
@ -1,480 +0,0 @@
|
||||
const { app, BrowserWindow, Menu, shell, ipcMain, Notification } = 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
|
||||
|
||||
// ── Push Notifications (Electron desktop) ────────────────────────────────────
|
||||
ipcMain.on('show-notification', (event, { title, body, icon, tag }) => {
|
||||
try {
|
||||
if (!Notification.isSupported()) return;
|
||||
const n = new Notification({
|
||||
title: title || 'Galaxy Strike Online',
|
||||
body: body || '',
|
||||
icon: icon || path.join(__dirname, 'assets/icon.png'),
|
||||
silent: false,
|
||||
urgency: 'normal', // 'low' | 'normal' | 'critical'
|
||||
});
|
||||
n.on('click', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
n.show();
|
||||
} catch (err) {
|
||||
console.error('[MAIN PROCESS] Notification error:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Allow renderer to check if window is focused (suppress notifications when in focus)
|
||||
ipcMain.handle('is-window-focused', () => {
|
||||
return mainWindow ? mainWindow.isFocused() : true;
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
10810
Client/index.html
10810
Client/index.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,325 +0,0 @@
|
||||
/**
|
||||
* Save System Integration
|
||||
* Integrates SmartSaveManager with existing game systems
|
||||
*/
|
||||
|
||||
console.log('[SAVE INTEGRATION] Save system integration loading');
|
||||
|
||||
// Override the game's save method to use SmartSaveManager
|
||||
function integrateWithGameEngine() {
|
||||
if (window.game && window.game.save) {
|
||||
// Store original save method
|
||||
const originalSave = window.game.save;
|
||||
|
||||
// Override game save method
|
||||
window.game.save = async function() {
|
||||
// console.log('[SAVE INTEGRATION] Game save called');
|
||||
|
||||
if (window.smartSaveManager) {
|
||||
await window.smartSaveManager.save();
|
||||
} else {
|
||||
// Fallback to original save if SmartSaveManager not available
|
||||
return await originalSave.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game save method overridden');
|
||||
}
|
||||
}
|
||||
|
||||
// Override the game's load method to use SmartSaveManager
|
||||
function integrateLoadSystem() {
|
||||
if (window.game && window.game.load) {
|
||||
// Store original load method
|
||||
const originalLoad = window.game.load;
|
||||
|
||||
// Override load method
|
||||
window.game.load = async function(saveData = null) {
|
||||
console.log('[SAVE INTEGRATION] Game load called, using SmartSaveManager');
|
||||
|
||||
try {
|
||||
let dataToLoad = saveData;
|
||||
|
||||
// If no data provided, use SmartSaveManager
|
||||
if (!dataToLoad && window.smartSaveManager) {
|
||||
dataToLoad = await window.smartSaveManager.loadPlayerData();
|
||||
}
|
||||
|
||||
// Load the data
|
||||
if (dataToLoad) {
|
||||
if (this.loadPlayerData) {
|
||||
this.loadPlayerData(dataToLoad);
|
||||
} else {
|
||||
// Fallback to original load
|
||||
return await originalLoad.call(this, dataToLoad);
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game data loaded successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('[SAVE INTEGRATION] No save data found, starting fresh');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SAVE INTEGRATION] Load error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game load method overridden');
|
||||
}
|
||||
}
|
||||
|
||||
// Add server data loading method to game
|
||||
function addServerDataSupport() {
|
||||
if (window.game) {
|
||||
// Store pending server data for later application
|
||||
window.game.pendingServerData = null;
|
||||
|
||||
window.game.loadServerPlayerData = function(serverData) {
|
||||
console.log('[SAVE INTEGRATION] Loading server player data into game');
|
||||
console.log('[SAVE INTEGRATION] Server data received:', serverData);
|
||||
console.log('[SAVE INTEGRATION] Server data type:', typeof serverData);
|
||||
console.log('[SAVE INTEGRATION] Server data keys:', serverData ? Object.keys(serverData) : 'No data');
|
||||
console.log('[SAVE INTEGRATION] Game systems available:', this.systems ? Object.keys(this.systems) : 'No systems');
|
||||
|
||||
// Store server data for later if systems aren't ready
|
||||
if (!this.systems || Object.keys(this.systems).length === 0) {
|
||||
console.log('[SAVE INTEGRATION] Game systems not ready, storing data for later');
|
||||
this.pendingServerData = serverData;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game systems ready, applying server data now');
|
||||
|
||||
try {
|
||||
// Apply player stats
|
||||
if (serverData.stats && this.systems && this.systems.player) {
|
||||
console.log('[SAVE INTEGRATION] Applying player stats:', serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player system methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(this.systems.player)));
|
||||
|
||||
// Check if load method exists
|
||||
if (typeof this.systems.player.load === 'function') {
|
||||
this.systems.player.load(serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player stats applied successfully');
|
||||
console.log('[SAVE INTEGRATION] Updated player stats:', this.systems.player.stats);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Player system has no load method, trying direct assignment');
|
||||
// Direct assignment as fallback
|
||||
if (this.systems.player.stats) {
|
||||
Object.assign(this.systems.player.stats, serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player stats assigned directly:', this.systems.player.stats);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] No player system or stats in server data');
|
||||
console.log('[SAVE INTEGRATION] Has serverData.stats:', !!serverData?.stats);
|
||||
console.log('[SAVE INTEGRATION] Has systems.player:', !!(this.systems?.player));
|
||||
}
|
||||
|
||||
// Apply inventory
|
||||
if (serverData.inventory && this.systems && this.systems.inventory) {
|
||||
console.log('[SAVE INTEGRATION] Applying player inventory:', serverData.inventory);
|
||||
if (typeof this.systems.inventory.load === 'function') {
|
||||
this.systems.inventory.load(serverData.inventory);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Inventory system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ship data
|
||||
if (serverData.ship && this.systems && this.systems.ship) {
|
||||
console.log('[SAVE INTEGRATION] Applying player ship:', serverData.ship);
|
||||
if (typeof this.systems.ship.load === 'function') {
|
||||
this.systems.ship.load(serverData.ship);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Ship system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply base data
|
||||
if (serverData.base && this.systems && this.systems.base) {
|
||||
console.log('[SAVE INTEGRATION] Applying player base:', serverData.base);
|
||||
if (typeof this.systems.base.load === 'function') {
|
||||
this.systems.base.load(serverData.base);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Base system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification
|
||||
if (this.showNotification) {
|
||||
this.showNotification(`Welcome back! Level ${serverData.stats?.level || 1}`, 'success', 3000);
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Server player data application completed');
|
||||
|
||||
// Force UI update
|
||||
if (this.systems && this.systems.ui && this.systems.ui.updateUI) {
|
||||
this.systems.ui.updateUI();
|
||||
console.log('[SAVE INTEGRATION] Server player data application completed');
|
||||
}
|
||||
|
||||
// Apply pending server data if any exists
|
||||
if (this.pendingServerData) {
|
||||
console.log('[SAVE INTEGRATION] Applying pending server data');
|
||||
this.loadServerPlayerData(this.pendingServerData);
|
||||
this.pendingServerData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SAVE INTEGRATION] Error applying server player data:', error);
|
||||
if (this.showNotification) {
|
||||
this.showNotification('Failed to load server data!', 'error', 3000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Method to check and apply pending server data
|
||||
window.game.checkAndApplyPendingServerData = function() {
|
||||
if (this.pendingServerData && this.systems && Object.keys(this.systems).length > 0) {
|
||||
console.log('[SAVE INTEGRATION] Systems ready, applying pending server data');
|
||||
this.loadServerPlayerData(this.pendingServerData);
|
||||
this.pendingServerData = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback loadPlayerData method if GameEngine doesn't have it
|
||||
if (!window.game.loadPlayerData) {
|
||||
window.game.loadPlayerData = window.game.loadServerPlayerData;
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Server data support added to game');
|
||||
}
|
||||
}
|
||||
|
||||
// Add save mode switching to UI
|
||||
function addSaveModeUI() {
|
||||
// Add save mode indicator to UI
|
||||
const createSaveModeIndicator = () => {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.id = 'save-mode-indicator';
|
||||
indicator.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
`;
|
||||
document.body.appendChild(indicator);
|
||||
return indicator;
|
||||
};
|
||||
|
||||
const updateSaveModeIndicator = () => {
|
||||
const indicator = document.getElementById('save-mode-indicator') || createSaveModeIndicator();
|
||||
|
||||
if (window.smartSaveManager) {
|
||||
const info = window.smartSaveManager.getSaveInfo();
|
||||
indicator.textContent = `Save: ${info.saveLocation}`;
|
||||
indicator.style.display = 'block';
|
||||
|
||||
// Color code based on mode
|
||||
indicator.style.background = info.isMultiplayer ? 'rgba(0, 100, 200, 0.8)' : 'rgba(0, 150, 0, 0.8)';
|
||||
}
|
||||
};
|
||||
|
||||
// Update indicator when mode changes
|
||||
if (window.smartSaveManager) {
|
||||
const originalSetMultiplayerMode = window.smartSaveManager.setMultiplayerMode;
|
||||
window.smartSaveManager.setMultiplayerMode = function(...args) {
|
||||
originalSetMultiplayerMode.apply(this, args);
|
||||
updateSaveModeIndicator();
|
||||
};
|
||||
}
|
||||
|
||||
// Initial update
|
||||
setTimeout(updateSaveModeIndicator, 1000);
|
||||
}
|
||||
|
||||
// Debug function to check data flow
|
||||
function debugDataFlow() {
|
||||
console.log('[DEBUG] === DATA FLOW DEBUG ===');
|
||||
|
||||
// Check GameInitializer
|
||||
if (window.gameInitializer) {
|
||||
console.log('[DEBUG] GameInitializer exists:', !!window.gameInitializer);
|
||||
console.log('[DEBUG] GameInitializer serverPlayerData:', window.gameInitializer.serverPlayerData);
|
||||
console.log('[DEBUG] GameInitializer gameMode:', window.gameInitializer.gameMode);
|
||||
} else {
|
||||
console.log('[DEBUG] GameInitializer NOT found');
|
||||
}
|
||||
|
||||
// Check game systems
|
||||
if (window.game) {
|
||||
console.log('[DEBUG] Game exists:', !!window.game);
|
||||
console.log('[DEBUG] Game systems:', window.game.systems ? Object.keys(window.game.systems) : 'No systems');
|
||||
|
||||
if (window.game.systems && window.game.systems.player) {
|
||||
console.log('[DEBUG] Player system exists:', !!window.game.systems.player);
|
||||
console.log('[DEBUG] Player stats:', window.game.systems.player.stats);
|
||||
console.log('[DEBUG] Player credits:', window.game.systems.player.stats?.credits);
|
||||
}
|
||||
} else {
|
||||
console.log('[DEBUG] Game NOT found');
|
||||
}
|
||||
|
||||
// Check SmartSaveManager
|
||||
if (window.smartSaveManager) {
|
||||
console.log('[DEBUG] SmartSaveManager exists:', !!window.smartSaveManager);
|
||||
console.log('[DEBUG] SmartSaveManager mode:', window.smartSaveManager.isMultiplayer ? 'multiplayer' : 'singleplayer');
|
||||
} else {
|
||||
console.log('[DEBUG] SmartSaveManager NOT found');
|
||||
}
|
||||
|
||||
console.log('[DEBUG] === END DEBUG ===');
|
||||
}
|
||||
|
||||
// Debug function available for manual testing
|
||||
window.debugDataFlow = debugDataFlow;
|
||||
|
||||
// Enhanced debug function for connection testing
|
||||
window.debugConnectionState = function() {
|
||||
console.log('=== CONNECTION STATE DEBUG ===');
|
||||
console.log('GameInitializer exists:', !!window.gameInitializer);
|
||||
console.log('GameInitializer socket connected:', !!window.gameInitializer?.socket?.connected);
|
||||
console.log('GameInitializer gameMode:', window.gameInitializer?.gameMode);
|
||||
console.log('GameInitializer serverPlayerData:', !!window.gameInitializer?.serverPlayerData);
|
||||
console.log('SmartSaveManager exists:', !!window.smartSaveManager);
|
||||
console.log('SmartSaveManager mode:', window.smartSaveManager?.isMultiplayer ? 'multiplayer' : 'singleplayer');
|
||||
console.log('Game exists:', !!window.game);
|
||||
console.log('Game isRunning:', window.game?.isRunning);
|
||||
console.log('=== END CONNECTION DEBUG ===');
|
||||
};
|
||||
|
||||
// Initialize integration when DOM is ready
|
||||
function initializeIntegration() {
|
||||
console.log('[SAVE INTEGRATION] Initializing save system integration');
|
||||
|
||||
// Wait for game to be ready
|
||||
const checkGameReady = () => {
|
||||
if (window.game) {
|
||||
integrateWithGameEngine();
|
||||
integrateLoadSystem();
|
||||
addServerDataSupport();
|
||||
addSaveModeUI();
|
||||
console.log('[SAVE INTEGRATION] Integration complete');
|
||||
} else {
|
||||
setTimeout(checkGameReady, 500);
|
||||
}
|
||||
};
|
||||
|
||||
checkGameReady();
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeIntegration);
|
||||
} else {
|
||||
initializeIntegration();
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Save system integration loaded');
|
||||
@ -1,228 +0,0 @@
|
||||
/**
|
||||
* Smart Save Manager
|
||||
* Intelligently handles save data for both singleplayer and multiplayer modes
|
||||
*/
|
||||
|
||||
class SmartSaveManager {
|
||||
constructor() {
|
||||
this.isMultiplayer = false;
|
||||
this.serverPlayerData = null;
|
||||
this.localSaveData = null;
|
||||
this.gameInitializer = null;
|
||||
|
||||
console.log('[SMART SAVE] SmartSaveManager initialized');
|
||||
}
|
||||
|
||||
setMultiplayerMode(isMultiplayer, gameInitializer = null) {
|
||||
const oldMode = this.isMultiplayer;
|
||||
this.isMultiplayer = isMultiplayer;
|
||||
this.gameInitializer = gameInitializer;
|
||||
|
||||
console.log(`[SMART SAVE] Mode change: ${oldMode ? 'multiplayer' : 'singleplayer'} -> ${isMultiplayer ? 'multiplayer' : 'singleplayer'}`);
|
||||
console.log(`[SMART SAVE] Set to ${isMultiplayer ? 'multiplayer' : 'singleplayer'} mode`);
|
||||
|
||||
if (isMultiplayer && gameInitializer) {
|
||||
// Load server data when switching to multiplayer
|
||||
this.loadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
// Load player data (intelligently chooses source)
|
||||
async loadPlayerData() {
|
||||
if (this.isMultiplayer) {
|
||||
return await this.loadServerData();
|
||||
} else {
|
||||
return await this.loadLocalData();
|
||||
}
|
||||
}
|
||||
|
||||
// Save player data (intelligently chooses destination)
|
||||
async savePlayerData(gameData) {
|
||||
if (this.isMultiplayer) {
|
||||
return await this.saveServerData(gameData);
|
||||
} else {
|
||||
return await this.saveLocalData(gameData);
|
||||
}
|
||||
}
|
||||
|
||||
// Load server data
|
||||
async loadServerData() {
|
||||
try {
|
||||
if (!this.gameInitializer || !this.gameInitializer.socket) {
|
||||
// Don't warn during initialization - this is expected before socket is ready
|
||||
// console.warn('[SMART SAVE] No multiplayer connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[SMART SAVE] Loading server player data');
|
||||
|
||||
// Request data from server
|
||||
this.gameInitializer.loadGameDataFromServer();
|
||||
|
||||
// Return cached server data if available
|
||||
return this.serverPlayerData;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error loading server data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save server data (DISABLED - client should not send data to server)
|
||||
async saveServerData(gameData) {
|
||||
console.warn('[SMART SAVE] Client save disabled - server is authoritative');
|
||||
return true; // Pretend it worked to avoid client errors
|
||||
}
|
||||
|
||||
// Load local data
|
||||
async loadLocalData() {
|
||||
try {
|
||||
console.log('[SMART SAVE] Loading local save data');
|
||||
|
||||
// Use existing local save system
|
||||
if (window.mainMenu && window.mainMenu.loadGame) {
|
||||
const saveData = await window.mainMenu.loadGame(1); // Load slot 1
|
||||
this.localSaveData = saveData;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
const saveKey = 'gso_save_slot_1';
|
||||
const saveData = localStorage.getItem(saveKey);
|
||||
|
||||
if (saveData) {
|
||||
const parsed = JSON.parse(saveData);
|
||||
this.localSaveData = parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error loading local data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save local data
|
||||
async saveLocalData(gameData) {
|
||||
try {
|
||||
// Don't save locally when in multiplayer mode
|
||||
if (this.isMultiplayer) {
|
||||
console.log('[SMART SAVE] Skipping local save - in multiplayer mode');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[SMART SAVE] Saving locally');
|
||||
|
||||
// Use existing local save system
|
||||
if (window.mainMenu && window.mainMenu.saveGame) {
|
||||
await window.mainMenu.saveGame(1, gameData); // Save to slot 1
|
||||
this.localSaveData = gameData;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
const saveKey = 'gso_save_slot_1';
|
||||
localStorage.setItem(saveKey, JSON.stringify(gameData));
|
||||
this.localSaveData = gameData;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error saving local data:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply server data to game
|
||||
applyServerDataToGame(serverData) {
|
||||
console.log('[SMART SAVE] Applying server data to game');
|
||||
|
||||
this.serverPlayerData = serverData;
|
||||
|
||||
// Apply to game if game is running (try both methods)
|
||||
if (window.game) {
|
||||
console.log('[SMART SAVE] Game is available, checking for data loading methods');
|
||||
console.log('[SMART SAVE] - loadPlayerData:', !!window.game.loadPlayerData);
|
||||
console.log('[SMART SAVE] - loadServerPlayerData:', !!window.game.loadServerPlayerData);
|
||||
|
||||
if (window.game.loadServerPlayerData) {
|
||||
console.log('[SMART SAVE] Using loadServerPlayerData method');
|
||||
window.game.loadServerPlayerData(serverData);
|
||||
console.log('[SMART SAVE] Server data applied to game, forcing UI refresh');
|
||||
|
||||
// Force UI refresh after applying server data
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Forcing UI refresh after server data application');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] UI refresh not available after server data application - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
}
|
||||
} else if (window.game.loadPlayerData) {
|
||||
console.log('[SMART SAVE] Using loadPlayerData method');
|
||||
window.game.loadPlayerData(serverData);
|
||||
console.log('[SMART SAVE] Server data applied to game, forcing UI refresh');
|
||||
|
||||
// Force UI refresh after applying server data
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Forcing UI refresh after server data application');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] UI refresh not available after server data application - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
|
||||
// Try delayed UI refresh since UIManager might not be ready yet
|
||||
console.log('[SMART SAVE] Attempting delayed UI refresh...');
|
||||
setTimeout(() => {
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Delayed UI refresh successful');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] Delayed UI refresh also failed - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
}
|
||||
}, 2000); // Wait 2 seconds for systems to initialize
|
||||
}
|
||||
} else {
|
||||
console.warn('[SMART SAVE] No data loading method available on game object');
|
||||
}
|
||||
} else {
|
||||
console.warn('[SMART SAVE] Game not available for data application');
|
||||
}
|
||||
|
||||
// Store for game engine
|
||||
if (window.gameInitializer) {
|
||||
window.gameInitializer.serverPlayerData = serverData;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current save source info
|
||||
getSaveInfo() {
|
||||
return {
|
||||
isMultiplayer: this.isMultiplayer,
|
||||
hasServerData: !!this.serverPlayerData,
|
||||
hasLocalData: !!this.localSaveData,
|
||||
saveLocation: this.isMultiplayer ? 'Server Database' : 'Local Storage'
|
||||
};
|
||||
}
|
||||
|
||||
// Sync data between local and server (for migration)
|
||||
async syncData(direction = 'toServer') {
|
||||
if (direction === 'toServer') {
|
||||
// Upload local data to server
|
||||
const localData = await this.loadLocalData();
|
||||
if (localData) {
|
||||
await this.saveServerData(localData);
|
||||
console.log('[SMART SAVE] Synced local data to server');
|
||||
}
|
||||
} else {
|
||||
// Download server data to local
|
||||
const serverData = await this.loadServerData();
|
||||
if (serverData) {
|
||||
await this.saveLocalData(serverData);
|
||||
console.log('[SMART SAVE] Synced server data to local');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.smartSaveManager = new SmartSaveManager();
|
||||
|
||||
console.log('[SMART SAVE] SmartSaveManager loaded and available globally');
|
||||
@ -1,179 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Debug Logger
|
||||
* Enhanced debugging that integrates with existing Logger system
|
||||
*/
|
||||
|
||||
class DebugLogger {
|
||||
constructor() {
|
||||
// Completely disable debug logging to prevent console flooding
|
||||
this.debugEnabled = false;
|
||||
|
||||
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
|
||||
if (this.debugEnabled) {
|
||||
this.log('=== DEBUG SESSION STARTED ===');
|
||||
}
|
||||
}
|
||||
|
||||
async log(message, data = null) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Skip console logging to prevent flooding
|
||||
// console.log(`[DEBUG] ${message}`, data || '');
|
||||
|
||||
// Skip performance logging to prevent flooding
|
||||
// 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) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
this.stepTimers.set(stepName, performance.now());
|
||||
await this.log(`STEP START: ${stepName}`, {
|
||||
type: 'step_start',
|
||||
step: stepName,
|
||||
elapsed: '0ms'
|
||||
});
|
||||
}
|
||||
|
||||
async endStep(stepName, data = null) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
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) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
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();
|
||||
@ -1,905 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Economy System
|
||||
* Manages player currency, transactions, and shop functionality
|
||||
* Now uses server-side ItemSystem for all item data
|
||||
*/
|
||||
|
||||
class Economy {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Currency - don't override in multiplayer mode, will be set by server data
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
this.credits = 0; // Will be updated by server
|
||||
this.gems = 0; // Will be updated by server
|
||||
this.premiumCurrency = 0; // Will be updated by server
|
||||
} else {
|
||||
this.credits = 10000; // Starting credits for singleplayer
|
||||
this.gems = 50; // Starting premium currency
|
||||
this.premiumCurrency = 0; // Additional premium currency
|
||||
}
|
||||
|
||||
// Transaction history
|
||||
this.transactions = [];
|
||||
|
||||
// Shop categories
|
||||
this.shopCategories = {
|
||||
ships: 'Ships',
|
||||
weapons: 'Weapons',
|
||||
armors: 'Armors',
|
||||
cosmetics: 'Cosmetics',
|
||||
consumables: 'Consumables',
|
||||
materials: 'Materials'
|
||||
};
|
||||
|
||||
// Random shop system - now uses server ItemSystem
|
||||
this.randomShopItems = {}; // Current random items per category
|
||||
this.shopRefreshInterval = null; // Timer for 2-hour refresh
|
||||
this.shopHeartbeatInterval = null; // Timer for live countdown updates
|
||||
this.lastShopRefresh = null; // Timestamp of last refresh
|
||||
this.currentShopData = null; // Current shop data from server
|
||||
this.SHOP_REFRESH_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
|
||||
this.MAX_ITEMS_PER_CATEGORY = 8;
|
||||
this.categoryPurchaseLimits = {}; // Track purchases per category per refresh
|
||||
|
||||
// Shop items - now loaded from server ItemSystem
|
||||
this.shopItems = null; // Will be populated by ItemSystem in multiplayer
|
||||
|
||||
// Owned cosmetics
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
// Owned ships
|
||||
this.ownedShips = [];
|
||||
|
||||
console.log('[ECONOMY] Economy system initialized');
|
||||
|
||||
// Initialize global purchase function
|
||||
Economy.initGlobalPurchaseFunction();
|
||||
}
|
||||
|
||||
// Create global purchase function for shop buttons
|
||||
static initGlobalPurchaseFunction() {
|
||||
window.purchaseShopItem = function(itemId) {
|
||||
console.log('[GLOBAL] Purchase shop item called:', itemId);
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.purchaseItem(itemId, 1);
|
||||
} else {
|
||||
console.error('[GLOBAL] Economy system not available for purchase');
|
||||
}
|
||||
};
|
||||
|
||||
// Add test function for idle system
|
||||
window.testIdleRewards = function() {
|
||||
console.log('[GLOBAL] Testing idle rewards...');
|
||||
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('testIdleRewards', {});
|
||||
|
||||
// Listen for response
|
||||
window.game.socket.once('testIdleRewards', (data) => {
|
||||
console.log('[GLOBAL] Test idle rewards response:', data);
|
||||
});
|
||||
} else {
|
||||
console.error('[GLOBAL] No socket available for idle test');
|
||||
}
|
||||
};
|
||||
|
||||
// Add socket event monitor
|
||||
window.monitorSocketEvents = function() {
|
||||
if (window.game && window.game.socket) {
|
||||
console.log('[GLOBAL] Monitoring socket events...');
|
||||
|
||||
// Monitor all incoming events
|
||||
const originalOn = window.game.socket.on;
|
||||
window.game.socket.on = function(event, callback) {
|
||||
const wrappedCallback = function(data) {
|
||||
if (event === 'onlineIdleRewards' || event === 'economy_data') {
|
||||
console.log('[SOCKET MONITOR] Received event:', event, data);
|
||||
}
|
||||
return callback(data);
|
||||
};
|
||||
return originalOn.call(this, event, wrappedCallback);
|
||||
};
|
||||
|
||||
console.log('[GLOBAL] Socket event monitoring enabled');
|
||||
} else {
|
||||
console.error('[GLOBAL] No socket available for monitoring');
|
||||
}
|
||||
};
|
||||
|
||||
// Add function to give player energy for testing dungeons
|
||||
window.addEnergy = function(amount = 50) {
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
const player = window.game.systems.player;
|
||||
const oldEnergy = player.attributes.energy || 0;
|
||||
player.attributes.energy = Math.min(oldEnergy + amount, player.attributes.maxEnergy || 100);
|
||||
console.log('[GLOBAL] Added energy:', oldEnergy, '->', player.attributes.energy);
|
||||
|
||||
// Update UI
|
||||
if (player.updateUI) {
|
||||
player.updateUI();
|
||||
}
|
||||
|
||||
// Update dungeon UI if available
|
||||
if (window.game.systems.dungeonSystem && window.game.systems.dungeonSystem.updateUI) {
|
||||
window.game.systems.dungeonSystem.updateUI();
|
||||
}
|
||||
|
||||
return player.attributes.energy;
|
||||
} else {
|
||||
console.error('[GLOBAL] Player system not available');
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[GLOBAL] Global functions initialized - purchaseShopItem() and testIdleRewards() available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up socket listeners for economy data synchronization
|
||||
*/
|
||||
setupSocketListeners() {
|
||||
if (!this.game.socket) {
|
||||
console.warn('[ECONOMY] No socket available for economy sync');
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for economy data updates from server
|
||||
this.game.socket.on('economy_data', (data) => {
|
||||
console.log('[ECONOMY] Received economy data from server:', data);
|
||||
console.log('[ECONOMY] Current credits before update:', this.credits);
|
||||
console.log('[ECONOMY] Current gems before update:', this.gems);
|
||||
|
||||
this.credits = data.credits || 0;
|
||||
this.gems = data.gems || 0;
|
||||
|
||||
console.log('[ECONOMY] Updated credits:', this.credits);
|
||||
console.log('[ECONOMY] Updated gems:', this.gems);
|
||||
|
||||
// Update UI immediately
|
||||
if (this.game.ui) {
|
||||
this.game.ui.updatePlayerStats();
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Economy synced - Credits:', this.credits, 'Gems:', this.gems);
|
||||
});
|
||||
|
||||
// Note: onlineIdleRewards is handled by GameInitializer to avoid duplicate event handling
|
||||
|
||||
// Listen for play time updates from server
|
||||
this.game.socket.on('playTimeUpdated', (data) => {
|
||||
console.log('[ECONOMY] Received play time update from server:', data);
|
||||
|
||||
// Update player stats if available
|
||||
if (this.game.systems.player && this.game.systems.player.stats) {
|
||||
this.game.systems.player.stats.playTime = data.playTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request economy data from server
|
||||
*/
|
||||
requestEconomyData() {
|
||||
if (this.game.socket) {
|
||||
console.log('[ECONOMY] Requesting economy data from server');
|
||||
this.game.socket.emit('get_economy_data');
|
||||
} else {
|
||||
console.warn('[ECONOMY] Cannot request economy data - no socket available');
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('[ECONOMY] Initializing economy system');
|
||||
|
||||
// In multiplayer mode, wait for ItemSystem to be ready (handled by event listener)
|
||||
this.game.on('itemSystemReady', () => {
|
||||
console.log('[ECONOMY] ItemSystem is ready, updating shop UI');
|
||||
this.updateShopUI();
|
||||
});
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
console.log('[ECONOMY] Multiplayer mode - waiting for ItemSystem to be ready');
|
||||
// ItemSystem initialization removed - wait for event instead
|
||||
} else {
|
||||
console.log('[ECONOMY] Singleplayer mode - using local shop data');
|
||||
// Initialize random shop for singleplayer
|
||||
this.initializeRandomShop();
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Economy system initialized');
|
||||
}
|
||||
|
||||
// Shop functionality - now uses ItemSystem in multiplayer
|
||||
purchaseItem(itemId, quantity = 1) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// In multiplayer mode, send request to server
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
if (debugLogger) debugLogger.logStep('Sending purchase request to server', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
// Send purchase request to server
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('purchaseItem', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
// Show loading message
|
||||
this.game.showNotification('Processing purchase...', 'info', 2000);
|
||||
} else {
|
||||
this.game.showNotification('Not connected to server', 'error', 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Singleplayer mode - use local logic
|
||||
const item = this.findShopItem(itemId);
|
||||
|
||||
if (!item) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - item not found', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
this.game.showNotification('Item not found in shop', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
const totalCost = item.price * quantity;
|
||||
const currency = item.currency;
|
||||
const oldCredits = this.credits;
|
||||
const oldGems = this.gems;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Item purchase attempted', {
|
||||
itemId: itemId,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
quantity: quantity,
|
||||
unitPrice: item.price,
|
||||
totalCost: totalCost,
|
||||
currency: currency,
|
||||
currentCredits: oldCredits,
|
||||
currentGems: oldGems
|
||||
});
|
||||
|
||||
// Check if player can afford
|
||||
if (currency === 'credits' && this.credits < totalCost) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient credits', {
|
||||
totalCost: totalCost,
|
||||
currentCredits: oldCredits,
|
||||
deficit: totalCost - oldCredits
|
||||
});
|
||||
this.game.showNotification('Not enough credits!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currency === 'gems' && this.gems < totalCost) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient gems', {
|
||||
totalCost: totalCost,
|
||||
currentGems: oldGems,
|
||||
deficit: totalCost - oldGems
|
||||
});
|
||||
this.game.showNotification('Not enough gems!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already owns this cosmetic
|
||||
if (item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id)) {
|
||||
this.game.showNotification('You already own this cosmetic!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process payment and give item based on type
|
||||
if (currency === 'credits') {
|
||||
this.credits -= totalCost;
|
||||
} else if (currency === 'gems') {
|
||||
this.gems -= totalCost;
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case 'ship':
|
||||
this.purchaseShip(item, quantity);
|
||||
break;
|
||||
case 'cosmetic':
|
||||
this.purchaseCosmetic(item, quantity);
|
||||
break;
|
||||
case 'consumable':
|
||||
this.purchaseConsumable(item, quantity);
|
||||
break;
|
||||
case 'material':
|
||||
this.purchaseMaterial(item, quantity);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ECONOMY] Unknown item type: ${item.type}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
this.game.showNotification(`Purchased ${item.name}!`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Item purchase completed successfully', {
|
||||
itemId: itemId,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
quantity: quantity,
|
||||
totalCost: totalCost,
|
||||
currency: currency,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: this.credits,
|
||||
oldGems: oldGems,
|
||||
newGems: this.gems
|
||||
});
|
||||
|
||||
// Update UI without calling updateShopUI to avoid circular updates
|
||||
return true;
|
||||
}
|
||||
|
||||
findShopItem(itemId) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[ECONOMY] Looking for shop item:', itemId);
|
||||
console.log('[ECONOMY] Multiplayer mode:', window.smartSaveManager?.isMultiplayer);
|
||||
console.log('[ECONOMY] ItemSystem available:', !!(this.game.systems.itemSystem));
|
||||
|
||||
// In multiplayer mode, use ItemSystem (required)
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
// Check if ItemSystem is ready before using it
|
||||
if (!this.game.systems.itemSystem || !this.game.systems.itemSystem.itemCatalog) {
|
||||
console.log('[ECONOMY] ItemSystem not ready yet, cannot find shop item');
|
||||
if (debugLogger) debugLogger.logStep('Shop item lookup failed - ItemSystem not ready', {
|
||||
itemId: itemId,
|
||||
multiplayer: true
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search in ItemSystem catalog
|
||||
const item = this.game.systems.itemSystem.itemCatalog.get(itemId);
|
||||
if (item) {
|
||||
console.log('[ECONOMY] Found item in ItemSystem:', item.name);
|
||||
return item;
|
||||
} else {
|
||||
console.log('[ECONOMY] Item not found in ItemSystem:', itemId);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Singleplayer mode - search in local random shop
|
||||
for (const categoryItems of Object.values(this.randomShopItems)) {
|
||||
const item = categoryItems.find(item => item.id === itemId);
|
||||
if (item) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Purchase methods
|
||||
purchaseShip(ship) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const player = this.game.systems.player;
|
||||
const oldShipName = player.ship.name;
|
||||
const oldShipClass = player.ship.class;
|
||||
const oldAttributes = { ...player.attributes };
|
||||
|
||||
// Update player ship
|
||||
player.ship = {
|
||||
name: ship.name,
|
||||
class: ship.id,
|
||||
texture: ship.texture,
|
||||
stats: ship.stats || {}
|
||||
};
|
||||
|
||||
// Update player attributes
|
||||
if (ship.stats) {
|
||||
player.attributes = { ...player.attributes, ...ship.stats };
|
||||
}
|
||||
|
||||
// Add to owned ships
|
||||
if (!player.ownedShips) {
|
||||
player.ownedShips = [];
|
||||
}
|
||||
if (!player.ownedShips.includes(ship.id)) {
|
||||
player.ownedShips.push(ship.id);
|
||||
}
|
||||
|
||||
// Add ship to BaseSystem ship gallery (singleplayer)
|
||||
if (this.game.systems.baseSystem) {
|
||||
const shipData = {
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.name.replace(/\s+/g, '_').toLowerCase(), // Generate class from name
|
||||
level: 1,
|
||||
stats: ship.stats || {},
|
||||
texture: ship.texture || `assets/textures/ships/${ship.id}.png`,
|
||||
isCurrent: false,
|
||||
rarity: ship.rarity || 'common'
|
||||
};
|
||||
|
||||
// Initialize ship gallery if needed
|
||||
if (!this.game.systems.baseSystem.purchasedShips) {
|
||||
this.game.systems.baseSystem.initializeShipGallery();
|
||||
}
|
||||
|
||||
// Add ship to gallery
|
||||
this.game.systems.baseSystem.purchasedShips.push(shipData);
|
||||
|
||||
// Update the ship gallery UI
|
||||
this.game.systems.baseSystem.updateShipGallery();
|
||||
|
||||
console.log('[ECONOMY] Ship added to BaseSystem gallery (singleplayer):', shipData.name);
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship purchase completed', {
|
||||
shipId: ship.id,
|
||||
shipName: ship.name,
|
||||
oldShipName: oldShipName,
|
||||
oldShipClass: oldShipClass,
|
||||
newShipName: ship.name,
|
||||
newShipClass: ship.id,
|
||||
oldAttributes: oldAttributes,
|
||||
newAttributes: player.attributes
|
||||
});
|
||||
}
|
||||
|
||||
purchaseCosmetic(cosmetic) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldOwnedCount = this.ownedCosmetics.length;
|
||||
|
||||
// Add to owned cosmetics
|
||||
this.ownedCosmetics.push(cosmetic.id);
|
||||
this.game.showNotification(`Cosmetic unlocked: ${cosmetic.name}`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Cosmetic purchase completed', {
|
||||
cosmeticId: cosmetic.id,
|
||||
cosmeticName: cosmetic.name,
|
||||
oldOwnedCount: oldOwnedCount,
|
||||
newOwnedCount: this.ownedCosmetics.length,
|
||||
totalOwnedCosmetics: this.ownedCosmetics.length
|
||||
});
|
||||
}
|
||||
|
||||
purchaseConsumable(consumable, quantity) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const inventory = this.game.systems.inventory;
|
||||
|
||||
// Create item object for inventory
|
||||
const item = {
|
||||
id: consumable.id,
|
||||
name: consumable.name,
|
||||
type: consumable.type,
|
||||
rarity: consumable.rarity,
|
||||
quantity: quantity,
|
||||
description: consumable.description,
|
||||
texture: consumable.texture,
|
||||
stats: consumable.stats || {},
|
||||
acquired: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
const oldInventorySize = inventory.items.length;
|
||||
inventory.addItem(item);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Consumable purchase completed', {
|
||||
itemId: consumable.id,
|
||||
itemName: consumable.name,
|
||||
quantity: quantity,
|
||||
oldInventorySize: oldInventorySize,
|
||||
newInventorySize: inventory.items.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ECONOMY] Error adding consumable to inventory:', error);
|
||||
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
purchaseMaterial(material, quantity) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const inventory = this.game.systems.inventory;
|
||||
|
||||
// Create item object for inventory
|
||||
const item = {
|
||||
id: material.id,
|
||||
name: material.name,
|
||||
type: material.type,
|
||||
rarity: material.rarity,
|
||||
quantity: quantity,
|
||||
description: material.description,
|
||||
texture: material.texture,
|
||||
stackable: material.stackable || true,
|
||||
acquired: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
const oldInventorySize = inventory.items.length;
|
||||
inventory.addItem(item);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Material purchase completed', {
|
||||
itemId: material.id,
|
||||
itemName: material.name,
|
||||
quantity: quantity,
|
||||
oldInventorySize: oldInventorySize,
|
||||
newInventorySize: inventory.items.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ECONOMY] Error adding material to inventory:', error);
|
||||
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Currency management
|
||||
addCredits(amount, source = 'unknown') {
|
||||
const oldCredits = this.credits;
|
||||
this.credits += amount;
|
||||
|
||||
// Add transaction
|
||||
this.addTransaction({
|
||||
type: 'credit',
|
||||
amount: amount,
|
||||
source: source,
|
||||
balance: this.credits,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`[ECONOMY] Added ${amount} credits from ${source}. New balance: ${this.credits}`);
|
||||
this.updateUI();
|
||||
|
||||
return this.credits - oldCredits;
|
||||
}
|
||||
|
||||
addGems(amount, source = 'unknown') {
|
||||
const oldGems = this.gems;
|
||||
this.gems += amount;
|
||||
|
||||
// Add transaction
|
||||
this.addTransaction({
|
||||
type: 'gem',
|
||||
amount: amount,
|
||||
source: source,
|
||||
balance: this.gems,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`[ECONOMY] Added ${amount} gems from ${source}. New balance: ${this.gems}`);
|
||||
this.updateUI();
|
||||
|
||||
return this.gems - oldGems;
|
||||
}
|
||||
|
||||
canAfford(cost, currency = 'credits') {
|
||||
if (currency === 'credits') {
|
||||
return this.credits >= cost;
|
||||
} else if (currency === 'gems') {
|
||||
return this.gems >= cost;
|
||||
} else if (currency === 'premium') {
|
||||
return this.premiumCurrency >= cost;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transaction management
|
||||
addTransaction(transaction) {
|
||||
this.transactions.push(transaction);
|
||||
this.transactionHistory.push(transaction);
|
||||
|
||||
// Keep only last 100 transactions in memory
|
||||
if (this.transactions.length > 100) {
|
||||
this.transactions = this.transactions.slice(-100);
|
||||
}
|
||||
}
|
||||
|
||||
// Manual sync with server data - call this to force update
|
||||
syncWithServerData(serverPlayerData) {
|
||||
console.log('[ECONOMY] Manual sync with server data:', {
|
||||
serverCredits: serverPlayerData?.stats?.credits,
|
||||
serverGems: serverPlayerData?.stats?.gems,
|
||||
currentCredits: this.credits,
|
||||
currentGems: this.gems
|
||||
});
|
||||
|
||||
if (serverPlayerData?.stats?.credits !== undefined) {
|
||||
this.credits = serverPlayerData.stats.credits;
|
||||
console.log('[ECONOMY] Updated credits from server:', this.credits);
|
||||
}
|
||||
|
||||
if (serverPlayerData?.stats?.gems !== undefined) {
|
||||
this.gems = serverPlayerData.stats.gems;
|
||||
console.log('[ECONOMY] Updated gems from server:', this.gems);
|
||||
}
|
||||
|
||||
// Update UI after sync
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
// Debug logging to track current values
|
||||
console.log('[ECONOMY] updateUI called - Current values:', {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
gameSystemsAvailable: !!(this.game && this.game.systems),
|
||||
uiSystemAvailable: !!(this.game && this.game.systems && this.game.systems.ui)
|
||||
});
|
||||
|
||||
// Update resource display
|
||||
if (this.game.systems.ui) {
|
||||
this.game.systems.ui.updateResourceDisplay();
|
||||
}
|
||||
|
||||
// Update shop UI if open
|
||||
this.updateShopUI();
|
||||
}
|
||||
|
||||
updateShopUI() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[ECONOMY] updateShopUI called');
|
||||
|
||||
if (this.game.multiplayerMode && this.game.itemSystem) {
|
||||
// Support both .catalog getter (new) and .shopItemsByCategory (legacy)
|
||||
const shopItems = this.game.itemSystem.catalog || this.game.itemSystem.shopItemsByCategory || {};
|
||||
const activeCategory = this.game.itemSystem.activeCategory || 'ships';
|
||||
const categoryItems = shopItems[activeCategory] || [];
|
||||
this.renderShopItems(categoryItems);
|
||||
} else {
|
||||
// Singleplayer mode - use local shop data
|
||||
const items = Object.values(this.randomShopItems).flat();
|
||||
|
||||
// Convert to categorized structure for consistency
|
||||
const categorizedItems = this.randomShopItems || {};
|
||||
this.renderShopItems(categorizedItems);
|
||||
}
|
||||
}
|
||||
|
||||
renderShopItems(items) {
|
||||
const shopItemsElement = document.getElementById('shopItems');
|
||||
if (!shopItemsElement) return;
|
||||
|
||||
const activeCategory = document.querySelector('.shop-cat-btn.active')?.dataset.category || 'ships';
|
||||
console.log('[ECONOMY] Active shop category:', activeCategory);
|
||||
|
||||
// Handle new shop data structure (items by category) or old structure (flat array)
|
||||
let categoryItems = [];
|
||||
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
// New structure: { ships: [...], weapons: [...], ... }
|
||||
categoryItems = items[activeCategory] || [];
|
||||
console.log('[ECONOMY] Using new shop structure - found', Object.keys(items).length, 'categories');
|
||||
} else if (Array.isArray(items)) {
|
||||
// Old structure: flat array of items
|
||||
const targetItemType = activeCategory.slice(0, -1); // Remove 's' from 'ships', 'weapons', etc.
|
||||
categoryItems = items.filter(item => item.type === targetItemType);
|
||||
console.log('[ECONOMY] Using old shop structure - filtered', items.length, 'total items');
|
||||
} else {
|
||||
console.warn('[ECONOMY] Invalid shop items structure:', typeof items);
|
||||
shopItemsElement.innerHTML = '<p>No items available</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Filtered items for category', activeCategory, ':', categoryItems.length, 'items');
|
||||
console.log('[ECONOMY] Item types in category:', categoryItems.map(item => item.type));
|
||||
|
||||
if (categoryItems.length === 0) {
|
||||
shopItemsElement.innerHTML = '<p>No items available in this category</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
shopItemsElement.innerHTML = categoryItems.map(item => {
|
||||
const canAfford = this.canAfford(item.price, item.currency);
|
||||
const isOwned = item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id);
|
||||
|
||||
// Generate image URL - server will serve images
|
||||
const imageUrl = this.getItemImageUrl(item);
|
||||
const placeholderUrl = 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
return `
|
||||
<div class="shop-item ${canAfford ? '' : 'cant-afford'} ${isOwned ? 'owned' : ''}" data-item-id="${item.id}">
|
||||
<div class="shop-item-content">
|
||||
<div class="shop-item-image">
|
||||
<img src="${imageUrl}" alt="${item.name}"
|
||||
onerror="this.src='${placeholderUrl}'"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<div class="shop-item-info">
|
||||
<div class="shop-item-header">
|
||||
<h3 class="shop-item-name">${item.name}</h3>
|
||||
<span class="shop-item-rarity ${item.rarity}">${item.rarity}</span>
|
||||
</div>
|
||||
<div class="shop-item-body">
|
||||
<p class="shop-item-description">${item.description}</p>
|
||||
<div class="shop-item-price">
|
||||
${this.formatPrice(item)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-item-footer">
|
||||
<button class="shop-item-purchase-btn"
|
||||
data-item-id="${item.id}"
|
||||
onclick="purchaseShopItem('${item.id}')"
|
||||
${!canAfford || isOwned ? 'disabled' : ''}>
|
||||
${isOwned ? 'Owned' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add event listeners to purchase buttons
|
||||
shopItemsElement.querySelectorAll('.shop-item-purchase-btn').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const itemId = button.getAttribute('data-item-id');
|
||||
if (itemId && !button.disabled) {
|
||||
console.log('[ECONOMY] Purchase button clicked for item:', itemId);
|
||||
this.purchaseItem(itemId, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
formatPrice(item) {
|
||||
if (!item.price) return 'Free';
|
||||
|
||||
const currency = item.currency || 'credits';
|
||||
const price = this.game.formatNumber(item.price);
|
||||
|
||||
return `${price} ${currency}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image URL for an item from server
|
||||
*/
|
||||
getItemImageUrl(item) {
|
||||
if (!item) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
// For multiplayer, ALWAYS get from server
|
||||
if (window.smartSaveManager?.isMultiplayer && this.game.socket) {
|
||||
const serverUrl = this.getServerUrl();
|
||||
|
||||
// Map item types to proper server paths
|
||||
switch (item.type) {
|
||||
case 'ship':
|
||||
return `${serverUrl}/images/ships/${item.id}.png`;
|
||||
case 'weapon':
|
||||
return `${serverUrl}/images/weapons/${item.id}.png`;
|
||||
case 'armor':
|
||||
return `${serverUrl}/images/armors/${item.id}.png`;
|
||||
case 'material':
|
||||
return `${serverUrl}/images/items/materials/${item.id}.png`;
|
||||
case 'consumable':
|
||||
return `${serverUrl}/images/items/consumables/${item.id}.png`;
|
||||
case 'cosmetic':
|
||||
return `${serverUrl}/images/items/cosmetics/${item.id}.png`;
|
||||
default:
|
||||
return `${serverUrl}/images/ui/placeholder.png`;
|
||||
}
|
||||
}
|
||||
|
||||
// For singleplayer, use local texture path (if available)
|
||||
if (item.texture) {
|
||||
return item.texture;
|
||||
}
|
||||
|
||||
// Fallback to server placeholder
|
||||
return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server URL for image requests
|
||||
*/
|
||||
getServerUrl() {
|
||||
// Get server URL from socket connection
|
||||
if (this.game.socket && this.game.socket.io && this.game.socket.io.uri) {
|
||||
return this.game.socket.io.uri.replace('/socket.io', '');
|
||||
}
|
||||
// Fallback to environment variable or production server
|
||||
return process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||
}
|
||||
|
||||
// Save/Load functionality
|
||||
save() {
|
||||
return {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactions: this.transactions,
|
||||
ownedCosmetics: this.ownedCosmetics,
|
||||
shopData: {
|
||||
randomShopItems: this.randomShopItems,
|
||||
categoryPurchaseLimits: this.categoryPurchaseLimits,
|
||||
lastShopRefresh: this.lastShopRefresh
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.credits !== undefined) this.credits = data.credits;
|
||||
if (data.gems !== undefined) this.gems = data.gems;
|
||||
if (data.premiumCurrency !== undefined) this.premiumCurrency = data.premiumCurrency;
|
||||
if (data.transactions) this.transactions = data.transactions;
|
||||
if (data.ownedCosmetics) this.ownedCosmetics = data.ownedCosmetics;
|
||||
|
||||
// Load shop data
|
||||
if (data.shopData) {
|
||||
this.randomShopItems = data.shopData.randomShopItems || {};
|
||||
this.categoryPurchaseLimits = data.shopData.categoryPurchaseLimits || {};
|
||||
this.lastShopRefresh = data.shopData.lastShopRefresh || null;
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
// Reset functionality
|
||||
reset() {
|
||||
const oldState = {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length
|
||||
};
|
||||
|
||||
this.credits = 1000;
|
||||
this.gems = 10;
|
||||
this.premiumCurrency = 0;
|
||||
this.transactions = [];
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
// Reset daily rewards
|
||||
localStorage.removeItem('lastDailyReward');
|
||||
|
||||
this.updateUI();
|
||||
|
||||
return oldState;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const oldState = {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length
|
||||
};
|
||||
|
||||
this.credits = 0;
|
||||
this.gems = 0;
|
||||
this.premiumCurrency = 0;
|
||||
this.transactions = [];
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
this.updateUI();
|
||||
|
||||
return oldState;
|
||||
}
|
||||
|
||||
// Initialize random shop for singleplayer (minimal implementation)
|
||||
initializeRandomShop() {
|
||||
console.log('[ECONOMY] Random shop not available in singleplayer mode');
|
||||
this.randomShopItems = {};
|
||||
}
|
||||
|
||||
// Get system statistics
|
||||
getStats() {
|
||||
return {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length,
|
||||
shopItemsCount: this.game.systems.itemSystem && this.game.systems.itemSystem.itemCatalog ?
|
||||
this.game.systems.itemSystem.getStats().totalItems : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Economy;
|
||||
} else {
|
||||
window.Economy = Economy;
|
||||
}
|
||||
@ -1,753 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Game Engine
|
||||
* Core game loop and state management
|
||||
*/
|
||||
|
||||
class GameEngine extends EventTarget {
|
||||
constructor() {
|
||||
// Must call super() first since we extend EventTarget
|
||||
super();
|
||||
|
||||
// Basic game state
|
||||
this.isRunning = false;
|
||||
this.isPaused = false;
|
||||
this.gameTime = 0;
|
||||
this.lastSaveTime = 0;
|
||||
this.autoSaveInterval = 5000; // 5 seconds
|
||||
this.gameLogicInterval = 1000; // 1 second for game updates
|
||||
|
||||
// Game systems
|
||||
this.systems = {};
|
||||
|
||||
// Save slot configuration
|
||||
this.saveSlotInfo = {
|
||||
slot: 1,
|
||||
useFileSystem: true
|
||||
};
|
||||
|
||||
// Game state
|
||||
this.state = {
|
||||
paused: false,
|
||||
currentTab: 'dashboard',
|
||||
notifications: []
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
this.eventListeners = new Map();
|
||||
|
||||
// Initialize immediately
|
||||
this.init();
|
||||
}
|
||||
|
||||
setMultiplayerMode(isMultiplayer, socket = null, serverData = null, currentUser = null) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[GAME ENGINE] Setting multiplayer mode:', isMultiplayer);
|
||||
console.log('[GAME ENGINE] Previous mode was:', this.isMultiplayer);
|
||||
if (debugLogger) debugLogger.logStep('setMultiplayerMode', { isMultiplayer, previousMode: this.isMultiplayer });
|
||||
|
||||
// CRITICAL: Once set to multiplayer, never allow fallback to singleplayer
|
||||
if (this.isMultiplayer && !isMultiplayer) {
|
||||
console.warn('[GAME ENGINE] ATTEMPTED FALLBACK TO SINGLEPLAYER - BLOCKING!');
|
||||
console.log('[GAME ENGINE] Preserving multiplayer mode');
|
||||
return; // Don't allow fallback to singleplayer
|
||||
}
|
||||
|
||||
this.isMultiplayer = isMultiplayer;
|
||||
this.socket = socket;
|
||||
this.serverData = serverData;
|
||||
this.currentUser = currentUser;
|
||||
|
||||
// Store multiplayer settings for systems that need them
|
||||
this.multiplayerConfig = {
|
||||
isMultiplayer,
|
||||
socket,
|
||||
serverData,
|
||||
currentUser
|
||||
};
|
||||
|
||||
console.log('[GAME ENGINE] Multiplayer mode configured:', {
|
||||
isMultiplayer,
|
||||
hasSocket: !!socket,
|
||||
hasServerData: !!serverData,
|
||||
hasCurrentUser: !!currentUser
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('[GAME ENGINE] Initializing game engine');
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (logger) await logger.info('Initializing game engine');
|
||||
if (debugLogger) await debugLogger.startStep('gameEngineInit');
|
||||
|
||||
try {
|
||||
// In multiplayer mode, use simplified initialization to avoid hanging
|
||||
if (this.isMultiplayer) {
|
||||
console.log('[GAME ENGINE] Using simplified multiplayer initialization');
|
||||
try {
|
||||
await this.initializeMultiplayerSystems();
|
||||
console.log('[GAME ENGINE] Multiplayer initialization complete - skipping event listeners');
|
||||
} catch (multiplayerError) {
|
||||
console.error('[GAME ENGINE] Multiplayer systems initialization failed:', multiplayerError);
|
||||
// Don't fall back to singleplayer - keep multiplayer mode but with minimal systems
|
||||
console.log('[GAME ENGINE] Continuing with minimal multiplayer systems');
|
||||
// Create essential systems only
|
||||
this.systems.player = new Player(this);
|
||||
this.systems.inventory = new Inventory(this);
|
||||
this.systems.economy = new Economy(this);
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
}
|
||||
} else {
|
||||
// Full initialization for singleplayer
|
||||
await this.initializeSystemsForLoad();
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Systems initialized, setting up event listeners');
|
||||
|
||||
// Set up event listeners (only in singleplayer to avoid conflicts)
|
||||
await this.setupEventListeners();
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.endStep('gameEngineInit', {
|
||||
systemsInitialized: Object.keys(this.systems).length,
|
||||
eventListeners: this.eventListeners.size
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game:', error);
|
||||
if (logger) await logger.errorEvent(error, 'Game Engine Initialization');
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Game Engine Initialization');
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified multiplayer-only system initialization to avoid hanging
|
||||
async initializeMultiplayerSystems() {
|
||||
console.log('[GAME ENGINE] Initializing multiplayer systems');
|
||||
|
||||
try {
|
||||
// Initialize texture manager first
|
||||
this.systems.textureManager = new TextureManager(this);
|
||||
|
||||
// Create essential systems immediately
|
||||
console.log('[GAME ENGINE] Creating essential systems');
|
||||
this.systems.player = new Player(this);
|
||||
this.systems.inventory = new Inventory(this);
|
||||
this.systems.economy = new Economy(this);
|
||||
this.systems.ui = new UIManager(this);
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
|
||||
console.log('[GAME ENGINE] Essential systems created successfully');
|
||||
|
||||
// Initialize ItemSystem ONCE and emit event when ready
|
||||
console.log('[GAME ENGINE] Initializing ItemSystem (single initialization)');
|
||||
this.systems.itemSystem.initialize().then(() => {
|
||||
console.log('[GAME ENGINE] ItemSystem fully initialized, emitting ready event');
|
||||
this.emit('itemSystemReady');
|
||||
}).catch(error => {
|
||||
console.error('[GAME ENGINE] ItemSystem initialization failed:', error);
|
||||
// Still emit event so Economy can fallback gracefully
|
||||
this.emit('itemSystemReady');
|
||||
});
|
||||
|
||||
// Initialize Economy (without ItemSystem - it will wait for the event)
|
||||
console.log('[GAME ENGINE] Initializing Economy system');
|
||||
this.systems.economy.initialize().catch(error => {
|
||||
console.error('[GAME ENGINE] Economy initialization failed:', error);
|
||||
});
|
||||
|
||||
// Create additional systems asynchronously to avoid blocking
|
||||
setTimeout(() => {
|
||||
console.log('[GAME ENGINE] Creating additional multiplayer systems asynchronously');
|
||||
|
||||
if (typeof SkillSystem !== 'undefined') {
|
||||
this.systems.skillSystem = new SkillSystem(this);
|
||||
}
|
||||
if (typeof DungeonSystem !== 'undefined') {
|
||||
this.systems.dungeonSystem = new DungeonSystem(this);
|
||||
// Initialize server-driven dungeon system
|
||||
this.systems.dungeonSystem.initialize().then(() => {
|
||||
console.log('[GAME ENGINE] DungeonSystem initialized with server data');
|
||||
}).catch(error => {
|
||||
console.error('[GAME ENGINE] Failed to initialize DungeonSystem:', error);
|
||||
});
|
||||
}
|
||||
if (typeof QuestSystem !== 'undefined') {
|
||||
this.systems.questSystem = new QuestSystem(this);
|
||||
console.log('[GAME ENGINE] QuestSystem created');
|
||||
}
|
||||
if (typeof CraftingSystem !== 'undefined') {
|
||||
this.systems.crafting = new CraftingSystem(this);
|
||||
}
|
||||
if (typeof BaseSystem !== 'undefined') {
|
||||
this.systems.base = new BaseSystem(this);
|
||||
}
|
||||
console.log('[GAME ENGINE] All multiplayer systems created asynchronously');
|
||||
}, 100); // Create after 100ms delay
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error in multiplayer systems initialization:', error);
|
||||
// Don't re-throw - allow game to continue with basic systems
|
||||
console.log('[GAME ENGINE] Continuing with available systems');
|
||||
}
|
||||
}
|
||||
|
||||
async initializeSystemsForLoad() {
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) await debugLogger.startStep('initializeSystemsForLoad');
|
||||
|
||||
if (logger) {
|
||||
await logger.timeAsync('Game Systems Initialization for Load', async () => {
|
||||
await logger.info('Initializing game systems for loading');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Initializing TextureManager');
|
||||
// Initialize texture manager first
|
||||
this.systems.textureManager = new TextureManager(this);
|
||||
if (logger) await logger.systemEvent('TextureManager', 'Initialized');
|
||||
if (debugLogger) await debugLogger.logStep('TextureManager initialized');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Player system (without initialization)');
|
||||
// Create systems but don't initialize with default data
|
||||
this.systems.player = new Player(this);
|
||||
if (logger) await logger.systemEvent('Player', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Player system created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Inventory system (without initialization)');
|
||||
this.systems.inventory = new Inventory(this);
|
||||
if (logger) await logger.systemEvent('Inventory', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Inventory system created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Economy system (without initialization)');
|
||||
this.systems.economy = new Economy(this);
|
||||
if (logger) await logger.systemEvent('Economy', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Economy system created');
|
||||
|
||||
// In multiplayer mode, skip singleplayer systems
|
||||
if (!this.isMultiplayer) {
|
||||
if (debugLogger) await debugLogger.logStep('Creating IdleSystem');
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
if (logger) await logger.systemEvent('IdleSystem', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('IdleSystem created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating ItemSystem');
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
if (logger) await logger.systemEvent('ItemSystem', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('ItemSystem created');
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Multiplayer mode - skipping singleplayer systems (IdleSystem, ItemSystem)');
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating UIManager');
|
||||
if (typeof UIManager !== 'undefined') {
|
||||
console.log('[GAME ENGINE] UIManager class found, creating real UIManager');
|
||||
this.systems.ui = new UIManager(this);
|
||||
// Expose UIManager globally for button onclick handlers
|
||||
window.uiManager = this.systems.ui;
|
||||
window.game.systems.ui = this.systems.ui;
|
||||
if (logger) await logger.systemEvent('UIManager', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('UIManager created and exposed');
|
||||
} else {
|
||||
console.error('[GAME ENGINE] UIManager class not found - this should not happen!');
|
||||
if (debugLogger) await debugLogger.error('UIManager class not found - this should not happen!');
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.endStep('initializeSystemsForLoad', {
|
||||
systemsCreated: Object.keys(this.systems).length
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async startGame(continueGame = false) {
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[GAME ENGINE] startGame called with continueGame =', continueGame);
|
||||
if (logger) await logger.info('Starting game', { continueGame });
|
||||
if (debugLogger) await debugLogger.startStep('startGame', { continueGame });
|
||||
|
||||
try {
|
||||
if (continueGame) {
|
||||
console.log('[GAME ENGINE] Loading existing save data...');
|
||||
if (debugLogger) await debugLogger.logStep('Loading existing save data');
|
||||
await this.loadGame();
|
||||
console.log('[GAME ENGINE] Save data loaded');
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Creating new game...');
|
||||
if (debugLogger) await debugLogger.logStep('Creating new game');
|
||||
await this.newGame();
|
||||
console.log('[GAME ENGINE] New game created');
|
||||
}
|
||||
|
||||
// Start game loop
|
||||
this.start();
|
||||
console.log('[GAME ENGINE] Game loop started');
|
||||
if (debugLogger) await debugLogger.logStep('Game loop started');
|
||||
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
if (loadingStatus) {
|
||||
console.log('[GAME ENGINE] Hiding loading status text');
|
||||
if (debugLogger) await debugLogger.logStep('Hiding loading status text');
|
||||
loadingStatus.classList.add('hidden');
|
||||
}
|
||||
|
||||
const gameInterface = document.getElementById('gameInterface');
|
||||
if (gameInterface) {
|
||||
console.log('[GAME ENGINE] Showing game interface');
|
||||
if (debugLogger) await debugLogger.logStep('Showing game interface');
|
||||
gameInterface.classList.remove('hidden');
|
||||
} else {
|
||||
console.warn('[GAME ENGINE] gameInterface element not found');
|
||||
if (debugLogger) await debugLogger.warn('gameInterface element not found');
|
||||
}
|
||||
|
||||
if (logger) await logger.info('Game started successfully');
|
||||
if (debugLogger) await debugLogger.endStep('startGame', {
|
||||
continueGame,
|
||||
isRunning: this.isRunning,
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Failed to start game:', error);
|
||||
if (logger) await logger.errorEvent(error, 'Game Start');
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Game Start');
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (this.isRunning) {
|
||||
if (debugLogger) debugLogger.log('GameEngine.start() called but game is already running', {
|
||||
isRunning: this.isRunning,
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Starting game engine', {
|
||||
gameLogicInterval: this.gameLogicInterval
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
// Start game logic timer (completely independent of frame rate)
|
||||
console.log('[GAME ENGINE] Starting game logic timer with interval:', this.gameLogicInterval);
|
||||
this.gameLogicTimer = setInterval(() => {
|
||||
this.updateGameLogic();
|
||||
}, this.gameLogicInterval);
|
||||
|
||||
// Start auto-save
|
||||
this.startAutoSave();
|
||||
|
||||
console.log('[GAME ENGINE] Game engine started');
|
||||
if (debugLogger) debugLogger.logStep('Game engine started successfully', {
|
||||
gameLogicInterval: this.gameLogicInterval,
|
||||
autoSaveInterval: this.autoSaveInterval
|
||||
});
|
||||
}
|
||||
|
||||
updateGameLogic() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// Use fixed 1-second interval for all updates
|
||||
const fixedDelta = 1000; // 1 second in milliseconds
|
||||
|
||||
if (this.state.paused) {
|
||||
if (debugLogger) debugLogger.logStep('Game logic update called but game is paused', {
|
||||
gameTime: this.gameTime,
|
||||
fixedDelta: fixedDelta
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.gameTime += fixedDelta;
|
||||
|
||||
// Update player play time with fixed delta
|
||||
if (this.systems.player && this.systems.player.updatePlayTime) {
|
||||
this.systems.player.updatePlayTime(fixedDelta);
|
||||
}
|
||||
|
||||
// Update all systems with fixed delta
|
||||
for (const [name, system] of Object.entries(this.systems)) {
|
||||
if (system && typeof system.update === 'function') {
|
||||
try {
|
||||
system.update(fixedDelta);
|
||||
} catch (error) {
|
||||
console.error(`[GAME ENGINE] Error updating ${name} system:`, error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, `Update ${name} system`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI displays (money, gems, energy) after system updates
|
||||
if (this.systems && this.systems.ui) {
|
||||
try {
|
||||
this.systems.ui.updateUI();
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error updating UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit game updated event
|
||||
this.emit('gameUpdated', { gameTime: this.gameTime });
|
||||
}
|
||||
|
||||
startAutoSave() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// Load saved interval or use default
|
||||
const savedInterval = localStorage.getItem('autoSaveInterval');
|
||||
this.autoSaveInterval = savedInterval ? parseInt(savedInterval) : 5;
|
||||
|
||||
console.log(`[GAME ENGINE] Starting auto-save with ${this.autoSaveInterval} minute interval`);
|
||||
|
||||
// Clear any existing timer
|
||||
this.stopAutoSave();
|
||||
|
||||
// Set up new timer
|
||||
this.autoSaveTimer = setInterval(async () => {
|
||||
console.log('[GAME ENGINE] Auto-save timer triggered - isRunning:', this.isRunning, 'paused:', this.state.paused);
|
||||
|
||||
if (this.isRunning && !this.state.paused) {
|
||||
console.log('[GAME ENGINE] Auto-saving game...');
|
||||
|
||||
try {
|
||||
// In multiplayer mode, save to server
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
console.log('[GAME ENGINE] Auto-saving to server...');
|
||||
if (this.socket) {
|
||||
this.socket.emit('saveGameData', {
|
||||
timestamp: Date.now(),
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
} else {
|
||||
console.warn('[GAME ENGINE] No socket available for server save');
|
||||
}
|
||||
} else {
|
||||
// Singleplayer mode - local save (not implemented yet)
|
||||
console.log('[GAME ENGINE] Local auto-save not implemented');
|
||||
}
|
||||
|
||||
this.showNotification('Game auto-saved', 'info', 2000);
|
||||
console.log('[GAME ENGINE] Auto-save completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Auto-save failed:', error);
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Auto-save');
|
||||
}
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Auto-save skipped - game not running or paused');
|
||||
}
|
||||
}, this.autoSaveInterval * 60 * 1000); // Convert minutes to milliseconds
|
||||
}
|
||||
|
||||
stopAutoSave() {
|
||||
if (this.autoSaveTimer) {
|
||||
console.log('[GAME ENGINE] Stopping auto-save timer');
|
||||
clearInterval(this.autoSaveTimer);
|
||||
this.autoSaveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification system
|
||||
async showNotification(message, type = 'info', duration = 3000) {
|
||||
const logger = window.logger;
|
||||
if (logger) await logger.playerAction('Notification', { message, type, duration });
|
||||
|
||||
const notification = {
|
||||
id: Date.now(),
|
||||
message,
|
||||
type,
|
||||
duration,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.state.notifications.push(notification);
|
||||
|
||||
// Auto-remove notification after duration
|
||||
setTimeout(() => {
|
||||
this.removeNotification(notification.id);
|
||||
}, duration);
|
||||
|
||||
// Update UI
|
||||
if (this.systems.ui) {
|
||||
// UI updates handled by individual systems
|
||||
}
|
||||
}
|
||||
|
||||
removeNotification(id) {
|
||||
this.state.notifications = this.state.notifications.filter(notification => notification.id !== id);
|
||||
}
|
||||
|
||||
// Event system
|
||||
on(event, callback) {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
this.eventListeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
this.eventListeners.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`[GAME ENGINE] Error in event listener for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also dispatch as DOM event for UIManager
|
||||
this.dispatchEvent(new CustomEvent(event, { detail: data }));
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
formatNumber(num) {
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceStats() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
const stats = {
|
||||
gameTime: this.gameTime,
|
||||
isRunning: this.isRunning,
|
||||
lastUpdate: this.lastUpdate,
|
||||
memory: null
|
||||
};
|
||||
|
||||
// Add memory info if available
|
||||
if (window.performance && window.performance.memory) {
|
||||
stats.memory = {
|
||||
used: window.performance.memory.usedJSHeapSize,
|
||||
total: window.performance.memory.totalJSHeapSize,
|
||||
limit: window.performance.memory.jsHeapSizeLimit
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Load server player data (transforms server format to client format)
|
||||
async loadServerPlayerData(playerData) {
|
||||
console.log('[GAME ENGINE] Loading server player data with format transformation');
|
||||
console.log('[GAME ENGINE] Original server data structure:', playerData);
|
||||
|
||||
// Transform server data format to client format
|
||||
const transformedData = {
|
||||
...playerData,
|
||||
|
||||
// Transform quests from server format to client format
|
||||
quests: playerData.quests ? {
|
||||
mainQuests: playerData.quests.main || [],
|
||||
dailyQuests: playerData.quests.daily || [],
|
||||
weeklyQuests: playerData.quests.weekly || [],
|
||||
tutorialQuests: playerData.quests.tutorial || [],
|
||||
activeQuests: playerData.quests.active || [],
|
||||
completedQuests: playerData.quests.completed || []
|
||||
} : {
|
||||
mainQuests: [],
|
||||
dailyQuests: [],
|
||||
weeklyQuests: [],
|
||||
tutorialQuests: [],
|
||||
activeQuests: [],
|
||||
completedQuests: []
|
||||
}
|
||||
};
|
||||
|
||||
// DEBUG: Log quest data transformation
|
||||
console.log('[GAME ENGINE] Quest data transformation:', {
|
||||
serverQuests: playerData.quests,
|
||||
transformedQuests: transformedData.quests,
|
||||
mainQuestsCount: transformedData.quests.mainQuests.length,
|
||||
dailyQuestsCount: transformedData.quests.dailyQuests.length,
|
||||
weeklyQuestsCount: transformedData.quests.weeklyQuests.length,
|
||||
tutorialQuestsCount: transformedData.quests.tutorialQuests.length
|
||||
});
|
||||
|
||||
// Use crafting data from server or initialize empty
|
||||
transformedData.crafting = playerData.crafting || {
|
||||
skill: 1,
|
||||
experience: 0,
|
||||
knownRecipes: [],
|
||||
completedDungeons: [],
|
||||
currentInstance: null,
|
||||
dungeonProgress: {}
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
async loadPlayerData(playerData) {
|
||||
console.log('[GAME ENGINE] Loading server player data');
|
||||
console.log('[GAME ENGINE] Full playerData structure:', playerData);
|
||||
console.log('[GAME ENGINE] PlayerData keys:', Object.keys(playerData));
|
||||
|
||||
try {
|
||||
// Apply basic player stats
|
||||
if (playerData.stats && this.systems && this.systems.player) {
|
||||
console.log('[GAME ENGINE] Found player stats and player system, applying...');
|
||||
console.log('[GAME ENGINE] Server playerData.stats:', playerData.stats);
|
||||
console.log('[GAME ENGINE] Server playerData keys:', Object.keys(playerData));
|
||||
|
||||
// Check for playTime in different possible locations
|
||||
const possiblePlayTimeFields = [
|
||||
playerData.stats?.playTime,
|
||||
playerData.playTime,
|
||||
playerData.totalPlayTime,
|
||||
playerData.stats?.totalPlayTime
|
||||
];
|
||||
|
||||
console.log('[GAME ENGINE] Possible playTime fields found:', possiblePlayTimeFields);
|
||||
|
||||
// Preserve existing playTime if server doesn't provide it
|
||||
const existingPlayTime = this.systems.player.stats.playTime || 0;
|
||||
console.log('[GAME ENGINE] Preserving existing playTime:', existingPlayTime);
|
||||
|
||||
this.systems.player.load(playerData.stats);
|
||||
console.log('[GAME ENGINE] Applied player stats:', playerData.stats);
|
||||
|
||||
// Restore playTime if it was lost
|
||||
if (!this.systems.player.stats.playTime || this.systems.player.stats.playTime === 0) {
|
||||
this.systems.player.stats.playTime = existingPlayTime;
|
||||
console.log('[GAME ENGINE] Restored playTime to:', existingPlayTime);
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Final playTime after load:', this.systems.player.stats.playTime);
|
||||
|
||||
// Apply credits from server data to economy system
|
||||
if (playerData.stats.credits !== undefined && this.systems.economy) {
|
||||
this.systems.economy.credits = playerData.stats.credits;
|
||||
console.log('[GAME ENGINE] Applied credits from server:', playerData.stats.credits);
|
||||
}
|
||||
|
||||
// Apply gems from server data to economy system
|
||||
if (playerData.stats.gems !== undefined && this.systems.economy) {
|
||||
this.systems.economy.gems = playerData.stats.gems;
|
||||
console.log('[GAME ENGINE] Applied gems from server:', playerData.stats.gems);
|
||||
}
|
||||
|
||||
// Force manual sync to ensure economy is updated
|
||||
if (this.systems.economy && this.systems.economy.syncWithServerData) {
|
||||
console.log('[GAME ENGINE] Forcing manual economy sync');
|
||||
this.systems.economy.syncWithServerData(playerData);
|
||||
}
|
||||
|
||||
// Request fresh economy data from server to ensure sync
|
||||
if (this.systems.economy && this.systems.economy.requestEconomyData) {
|
||||
setTimeout(() => {
|
||||
this.systems.economy.requestEconomyData();
|
||||
}, 1000); // Delay to ensure socket is ready
|
||||
}
|
||||
|
||||
// Apply energy from server data to player attributes
|
||||
if (playerData.stats.currentEnergy !== undefined && this.systems.player.attributes) {
|
||||
this.systems.player.attributes.currentEnergy = playerData.stats.currentEnergy;
|
||||
console.log('[GAME ENGINE] Applied current energy from server:', playerData.stats.currentEnergy);
|
||||
}
|
||||
|
||||
if (playerData.stats.maxEnergy !== undefined && this.systems.player.attributes) {
|
||||
this.systems.player.attributes.maxEnergy = playerData.stats.maxEnergy;
|
||||
console.log('[GAME ENGINE] Applied max energy from server:', playerData.stats.maxEnergy);
|
||||
}
|
||||
|
||||
// Ensure player has minimum energy for dungeon access
|
||||
if (this.systems.player.attributes) {
|
||||
// Check if energy is missing or too low
|
||||
if (!this.systems.player.attributes.energy || this.systems.player.attributes.energy < 10) {
|
||||
const oldEnergy = this.systems.player.attributes.energy;
|
||||
this.systems.player.attributes.energy = 100;
|
||||
this.systems.player.attributes.maxEnergy = Math.max(this.systems.player.attributes.maxEnergy || 0, 100);
|
||||
console.log('[GAME ENGINE] Set minimum energy for dungeon access:', oldEnergy, '->', this.systems.player.attributes.energy);
|
||||
}
|
||||
|
||||
// Also ensure currentEnergy is set if it exists
|
||||
if (this.systems.player.attributes.currentEnergy !== undefined) {
|
||||
if (this.systems.player.attributes.currentEnergy < 10) {
|
||||
this.systems.player.attributes.currentEnergy = 100;
|
||||
console.log('[GAME ENGINE] Set minimum currentEnergy for dungeon access');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Final player stats after application:', this.systems.player.stats);
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Missing player stats or player system');
|
||||
console.log('[GAME ENGINE] - playerData.stats:', !!playerData.stats);
|
||||
console.log('[GAME ENGINE] - this.systems:', !!this.systems);
|
||||
console.log('[GAME ENGINE] - this.systems.player:', !!this.systems?.player);
|
||||
}
|
||||
|
||||
// Apply inventory
|
||||
if (playerData.inventory && this.systems && this.systems.inventory) {
|
||||
this.systems.inventory.load(playerData.inventory);
|
||||
console.log('[GAME ENGINE] Applied inventory');
|
||||
}
|
||||
|
||||
// REMOVED: QuestSystem should be server-driven only
|
||||
// Quest data will be handled by server-side systems only
|
||||
|
||||
// Show notification
|
||||
if (this.showNotification) {
|
||||
this.showNotification(`Welcome back! Level ${playerData.stats?.level || 1}`, 'success', 3000);
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Server player data loaded successfully');
|
||||
|
||||
// Trigger UI update to refresh all tabs with new data
|
||||
if (this.systems && this.systems.ui) {
|
||||
this.systems.ui.updateUI();
|
||||
console.log('[GAME ENGINE] Triggered UI update after server data load');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error loading server player data:', error);
|
||||
if (this.showNotification) {
|
||||
this.showNotification('Failed to load server data!', 'error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global game instance
|
||||
let game = null;
|
||||
|
||||
// Export GameEngine to global scope
|
||||
if (typeof window !== 'undefined') {
|
||||
window.GameEngine = GameEngine;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,304 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
async debug(message, data = null) {
|
||||
await this.log('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;
|
||||
}
|
||||
@ -1,965 +0,0 @@
|
||||
/**
|
||||
* 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) {
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
console.log('[PLAYER] updatePlayTime called with deltaTime:', deltaTime, 'ms');
|
||||
console.log('[PLAYER] Game state check:', {
|
||||
hasGame: !!this.game,
|
||||
isRunning: this.game?.isRunning,
|
||||
isPaused: this.game?.state?.paused,
|
||||
isHidden: document.hidden
|
||||
});
|
||||
*/
|
||||
|
||||
// Only update playtime when game is actively running and not paused
|
||||
if (!this.game || !this.game.isRunning || this.game.state.paused) {
|
||||
// console.log('[PLAYER] Skipping playtime update - game not running or paused');
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check if tab is visible (don't count time when tab is in background)
|
||||
if (document.hidden) {
|
||||
// console.log('[PLAYER] Skipping playtime update - tab hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
console.log('[PLAYER] Before update - playTime:', this.stats.playTime, 'ms');
|
||||
*/
|
||||
|
||||
// Use real computer time delta
|
||||
this.stats.playTime += deltaTime;
|
||||
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
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');
|
||||
*/
|
||||
}
|
||||
|
||||
// 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 playerTitleElement = document.getElementById('playerTitle');
|
||||
const playerLevelElement = document.getElementById('playerLevel');
|
||||
|
||||
if (playerNameElement) {
|
||||
playerNameElement.textContent = this.info.name;
|
||||
}
|
||||
|
||||
if (playerTitleElement) {
|
||||
playerTitleElement.textContent = ` - ${this.info.title}`;
|
||||
}
|
||||
|
||||
if (playerLevelElement) {
|
||||
playerLevelElement.textContent = `Lv. ${this.stats.level}`;
|
||||
}
|
||||
|
||||
// Update health and energy only if in multiplayer mode or game is actively running
|
||||
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
|
||||
|
||||
if (shouldUpdateUI && 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,
|
||||
playerTitle: !!playerTitleElement,
|
||||
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);
|
||||
console.log('[PLAYER] Current playTime before load:', this.stats.playTime);
|
||||
console.log('[PLAYER] Server playTime:', data.stats.playTime);
|
||||
|
||||
const oldStats = { ...this.stats };
|
||||
|
||||
// Preserve playTime if server doesn't provide it or provides 0
|
||||
const existingPlayTime = this.stats.playTime || 0;
|
||||
const serverPlayTime = data.stats.playTime || 0;
|
||||
|
||||
// Use server playTime if it's greater than existing, otherwise preserve existing
|
||||
const preservedPlayTime = serverPlayTime > existingPlayTime ? serverPlayTime : existingPlayTime;
|
||||
|
||||
console.log('[PLAYER] Preserving playTime:', preservedPlayTime, '(existing:', existingPlayTime, ', server:', serverPlayTime, ')');
|
||||
|
||||
// Merge stats but preserve playTime
|
||||
this.stats = {
|
||||
...this.stats,
|
||||
...data.stats,
|
||||
playTime: preservedPlayTime // Force preserve playTime
|
||||
};
|
||||
|
||||
console.log('[PLAYER] Level after stats load:', this.stats.level);
|
||||
console.log('[PLAYER] PlayTime after stats load:', this.stats.playTime);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Game Data
|
||||
* UI constants and configuration only.
|
||||
* All game content (items, skills, recipes, dungeons, enemies) is loaded from the server.
|
||||
*/
|
||||
|
||||
// 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 (used only for initial UI state before server data arrives)
|
||||
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 (client-side display only; server is authoritative)
|
||||
const EXPERIENCE_TABLE = [];
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
|
||||
}
|
||||
|
||||
// Item rarity display properties (colours/labels only - drop rates are server-side)
|
||||
const ITEM_RARITIES = {
|
||||
common: { name: 'Common', color: '#888888', multiplier: 1.0 },
|
||||
uncommon: { name: 'Uncommon', color: '#00ff00', multiplier: 1.2 },
|
||||
rare: { name: 'Rare', color: '#0088ff', multiplier: 1.5 },
|
||||
epic: { name: 'Epic', color: '#8833ff', multiplier: 2.0 },
|
||||
legendary: { name: 'Legendary', color: '#ff8800', multiplier: 3.0 }
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
getRandomItem(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
},
|
||||
|
||||
getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
|
||||
getRandomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
},
|
||||
|
||||
checkChance(chance) {
|
||||
return Math.random() < chance;
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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`;
|
||||
},
|
||||
|
||||
getExperienceForLevel(level) {
|
||||
return EXPERIENCE_TABLE[level] || 0;
|
||||
},
|
||||
|
||||
deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
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,
|
||||
GAME_MESSAGES,
|
||||
GameUtils
|
||||
};
|
||||
}
|
||||
@ -1,718 +0,0 @@
|
||||
/**
|
||||
* 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 (DISABLED to prevent auto-start after multiplayer disconnect)
|
||||
console.log('[MAIN] Skipping local server auto-start to prevent conflicts with multiplayer mode');
|
||||
/*
|
||||
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.limit > 0.8) {
|
||||
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
@ -1,403 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Crafting System
|
||||
* Recipe definitions are loaded from the server; this file handles
|
||||
* local crafting logic, requirement checking, and UI rendering.
|
||||
*/
|
||||
|
||||
class CraftingSystem extends BaseSystem {
|
||||
constructor(gameEngine) {
|
||||
super(gameEngine);
|
||||
|
||||
this.recipes = new Map(); // recipeId -> recipe object
|
||||
this.currentCategory = 'weapons';
|
||||
this.selectedRecipe = null;
|
||||
|
||||
this._loaded = false;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Initialisation — request recipes from the server
|
||||
// ------------------------------------------------------------------ //
|
||||
async initialize() {
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
console.log('[CRAFTING SYSTEM] Requesting recipes from server');
|
||||
|
||||
if (!window.game?.socket) {
|
||||
console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected');
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recipes = await this._fetchRecipesFromServer();
|
||||
this._applyServerRecipes(recipes);
|
||||
this._loaded = true;
|
||||
console.log(`[CRAFTING SYSTEM] Loaded ${this.recipes.size} recipes from server`);
|
||||
} catch (err) {
|
||||
console.error('[CRAFTING SYSTEM] Failed to load recipes from server:', err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
_fetchRecipesFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = window.game.socket;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
socket.off('recipes_data', handler);
|
||||
reject(new Error('Recipe data request timed out'));
|
||||
}, 10000);
|
||||
|
||||
const handler = (data) => {
|
||||
clearTimeout(timeout);
|
||||
socket.off('recipes_data', handler);
|
||||
if (data && (Array.isArray(data) || typeof data === 'object')) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error('Invalid recipe data from server'));
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('recipes_data', handler);
|
||||
socket.emit('get_recipes');
|
||||
});
|
||||
}
|
||||
|
||||
_applyServerRecipes(serverRecipes) {
|
||||
this.recipes.clear();
|
||||
|
||||
// Server may return array or object keyed by id
|
||||
const asList = Array.isArray(serverRecipes)
|
||||
? serverRecipes
|
||||
: Object.values(serverRecipes);
|
||||
|
||||
for (const recipe of asList) {
|
||||
if (!recipe.id) continue;
|
||||
|
||||
// Normalise materials: server uses { itemId: qty } objects, client expects array
|
||||
let materials = recipe.materials;
|
||||
if (materials && !Array.isArray(materials)) {
|
||||
materials = Object.entries(materials).map(([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
// Normalise results similarly
|
||||
let results = recipe.results;
|
||||
if (results && !Array.isArray(results)) {
|
||||
results = Object.entries(results)
|
||||
.filter(([k]) => k !== 'experience')
|
||||
.map(([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
this.recipes.set(recipe.id, {
|
||||
...recipe,
|
||||
materials: materials || [],
|
||||
results: results || [],
|
||||
category: recipe.type || recipe.category || 'items',
|
||||
unlocked: false // will be resolved by checkRecipeUnlocks()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Runtime
|
||||
// ------------------------------------------------------------------ //
|
||||
addRecipe(id, recipe) {
|
||||
recipe.id = id;
|
||||
recipe.unlocked = false;
|
||||
this.recipes.set(id, recipe);
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.checkRecipeUnlocks();
|
||||
}
|
||||
|
||||
checkRecipeUnlocks() {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return;
|
||||
|
||||
for (const [id, recipe] of this.recipes) {
|
||||
if (recipe.unlocked) continue;
|
||||
|
||||
let canUnlock = true;
|
||||
if (recipe.requirements) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skillName) < requiredLevel) {
|
||||
canUnlock = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canUnlock) {
|
||||
recipe.unlocked = true;
|
||||
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRecipesByCategory(category) {
|
||||
return Array.from(this.recipes.values())
|
||||
.filter(r => r.category === category || r.type === category);
|
||||
}
|
||||
|
||||
canCraftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!recipe) return false;
|
||||
|
||||
if (recipe.requirements && skillSystem) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.materials && inventory) {
|
||||
for (const mat of recipe.materials) {
|
||||
if (!inventory.hasItem(mat.id, mat.quantity)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMissingMaterials(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!recipe?.materials) return [];
|
||||
|
||||
const missing = [];
|
||||
for (const mat of recipe.materials) {
|
||||
let current = 0;
|
||||
if (inventory?.getItemCount) {
|
||||
try { current = inventory.getItemCount(mat.id) || 0; } catch (_) {}
|
||||
}
|
||||
const required = mat.quantity || 0;
|
||||
if (current < required) {
|
||||
missing.push({ id: mat.id, required, current, missing: required - current });
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
async craftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe || !this.canCraftRecipe(recipeId)) return false;
|
||||
|
||||
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
|
||||
|
||||
if (recipe.materials) {
|
||||
for (const mat of recipe.materials) {
|
||||
this.game.systems.inventory.removeItem(mat.id, mat.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.experience && this.game.systems.skillSystem) {
|
||||
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000));
|
||||
|
||||
if (recipe.results) {
|
||||
for (const result of recipe.results) {
|
||||
this.game.systems.inventory.addItem(result.id, result.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.onItemCrafted?.();
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING] Done: ${recipe.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
selectRecipe(recipeId) {
|
||||
this.selectedRecipe = this.recipes.get(recipeId);
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
|
||||
getSelectedRecipe() { return this.selectedRecipe; }
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// UI
|
||||
// ------------------------------------------------------------------ //
|
||||
updateUI() {
|
||||
this.updateRecipeList();
|
||||
this.updateCraftingDetails();
|
||||
this.updateCraftingInfo();
|
||||
}
|
||||
|
||||
updateRecipeList() {
|
||||
const listEl = document.getElementById('recipeList');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!this._loaded) {
|
||||
listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading recipes from server...</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const recipes = this.getRecipesByCategory(this.currentCategory);
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (recipes.length === 0) {
|
||||
listEl.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'recipe-item';
|
||||
el.dataset.recipeId = recipe.id;
|
||||
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
const missingMats = this.getMissingMaterials(recipe.id);
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
let skillsMet = true;
|
||||
|
||||
if (recipe.requirements && skillSystem) {
|
||||
for (const [skill, level] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillsMet) el.classList.add('locked');
|
||||
else if (!canCraft) el.classList.add('missing-materials');
|
||||
else el.classList.add('can-craft');
|
||||
|
||||
const reqText = recipe.requirements
|
||||
? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).join(', ')
|
||||
: 'None';
|
||||
|
||||
const matsHtml = recipe.materials.map(mat => {
|
||||
const mis = missingMats.find(m => m.id === mat.id);
|
||||
const cur = mis ? mis.current : (this.game.systems.inventory?.getItemCount(mat.id) || 0);
|
||||
const cls = mis ? 'material-item missing' : 'material-item';
|
||||
return `<div class="${cls}">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-quantity">${cur}/${mat.quantity}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="recipe-header">
|
||||
<h4>${recipe.name}</h4>
|
||||
<span class="recipe-level">Level ${reqText}</span>
|
||||
</div>
|
||||
<div class="recipe-description">${recipe.description || ''}</div>
|
||||
<div class="recipe-materials">${matsHtml}</div>
|
||||
${missingMats.length > 0 ? `
|
||||
<div class="missing-materials-text">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
|
||||
</div>` : ''}
|
||||
<div class="recipe-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${(recipe.craftingTime || 0) / 1000}s</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
this.selectRecipe(recipe.id);
|
||||
this.updateCraftingDetails();
|
||||
});
|
||||
|
||||
listEl.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
updateCraftingDetails() {
|
||||
const detailsEl = document.getElementById('craftingDetails');
|
||||
if (!detailsEl) return;
|
||||
|
||||
if (!this.selectedRecipe) {
|
||||
detailsEl.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);
|
||||
|
||||
detailsEl.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(([s, l]) =>
|
||||
`<div class="requirement-item">
|
||||
<span class="skill-name">${s}</span>
|
||||
<span class="skill-level">Level ${l}</span>
|
||||
</div>`).join('')
|
||||
: '<p>No special requirements</p>'}
|
||||
</div>
|
||||
<div class="recipe-materials-needed">
|
||||
<h4>Materials Needed:</h4>
|
||||
${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('')}
|
||||
</div>
|
||||
<div class="recipe-results">
|
||||
<h4>Results:</h4>
|
||||
${recipe.results.map(r =>
|
||||
`<div class="result-item">
|
||||
<span class="result-name">${r.id}</span>
|
||||
<span class="result-quantity">x${r.quantity}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="recipe-info">
|
||||
<div class="experience-reward">
|
||||
<i class="fas fa-star"></i>
|
||||
<span>${recipe.experience || 0} XP</span>
|
||||
</div>
|
||||
<div class="crafting-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${(recipe.craftingTime || 0) / 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 levelEl = document.getElementById('craftingLevel');
|
||||
const expEl = document.getElementById('craftingExp');
|
||||
if (levelEl) levelEl.textContent = craftingLevel;
|
||||
if (expEl) expEl.textContent = `${craftingExp}/${expNeeded}`;
|
||||
}
|
||||
|
||||
switchCategory(category) {
|
||||
this.currentCategory = category;
|
||||
this.selectedRecipe = null;
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in GameEngine
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CraftingSystem;
|
||||
}
|
||||
@ -1,832 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Dungeon System
|
||||
* Server-driven dungeon management client
|
||||
*/
|
||||
|
||||
// Create global function for dungeon start that's more reliable
|
||||
window.startDungeon = function(dungeonId) {
|
||||
console.log('[DUNGEON SYSTEM] startDungeon called with:', dungeonId);
|
||||
console.log('[DUNGEON SYSTEM] Game available:', !!window.game);
|
||||
console.log('[DUNGEON SYSTEM] Game systems available:', !!(window.game && window.game.systems));
|
||||
console.log('[DUNGEON SYSTEM] Dungeon system available:', !!(window.game && window.game.systems && window.game.systems.dungeonSystem));
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.startDungeon(dungeonId);
|
||||
}
|
||||
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for dungeon start');
|
||||
};
|
||||
|
||||
// Create global function for process encounter that's more reliable
|
||||
window.processEncounter = function() {
|
||||
console.log('[DUNGEON SYSTEM] processEncounter called');
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.processEncounter();
|
||||
}
|
||||
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for process encounter');
|
||||
};
|
||||
|
||||
// Create global function for dungeon toggle that's more reliable
|
||||
window.toggleDungeonSection = function(sectionId) {
|
||||
// Try to use the dungeon system if available
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.toggleDungeonSection(sectionId);
|
||||
}
|
||||
|
||||
// Fallback: Direct DOM manipulation
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (!section || !indicator) {
|
||||
console.warn('[DUNGEON SYSTEM] Section or indicator not found:', sectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCollapsed = section.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Expand
|
||||
section.classList.remove('collapsed');
|
||||
indicator.classList.remove('fa-chevron-right');
|
||||
indicator.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
// Collapse
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
|
||||
// Save the state in the dungeon system if available
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
window.game.systems.dungeonSystem.collapseStates.set(sectionId, !isCollapsed);
|
||||
}
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
|
||||
};
|
||||
|
||||
class DungeonSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Current dungeon state (runtime only)
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.dungeonProgress = 0;
|
||||
this.isExploring = false;
|
||||
|
||||
// Debouncing to prevent multiple rapid clicks
|
||||
this.lastProcessTime = 0;
|
||||
this.processCooldown = 1000; // 1 second cooldown
|
||||
|
||||
// Prevent duplicate event processing
|
||||
this.lastEncounterData = null;
|
||||
this.lastNextRoomData = null;
|
||||
|
||||
// Store collapse states to preserve them during regeneration
|
||||
this.collapseStates = new Map();
|
||||
|
||||
// Track last generation to prevent unnecessary regenerations
|
||||
this.lastGenerationTime = 0;
|
||||
this.generationThrottle = 500; // 500ms throttle
|
||||
|
||||
// Server dungeons data
|
||||
this.serverDungeons = null;
|
||||
this.roomTypes = {};
|
||||
this.enemyTemplates = {};
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Client DungeonSystem initialized - server-driven mode');
|
||||
|
||||
// Set up socket event listeners
|
||||
this.setupSocketListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up Socket.IO event listeners for dungeon data
|
||||
*/
|
||||
setupSocketListeners() {
|
||||
if (!this.game.socket) {
|
||||
console.warn('[DUNGEON SYSTEM] No socket available for event listeners');
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for dungeon data response
|
||||
this.game.socket.on('dungeons_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received dungeons data:', data);
|
||||
this.serverDungeons = data.dungeons || data;
|
||||
console.log('[DUNGEON SYSTEM] Loaded grouped dungeons from server:', Object.keys(this.serverDungeons));
|
||||
// Update UI when data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for room types response
|
||||
this.game.socket.on('room_types_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received room types data:', data);
|
||||
this.roomTypes = data;
|
||||
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.roomTypes).length} room types from server`);
|
||||
// Update UI when room data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for enemy templates response
|
||||
this.game.socket.on('enemy_templates_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received enemy templates data:', data);
|
||||
this.enemyTemplates = data;
|
||||
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.enemyTemplates).length} enemy templates from server`);
|
||||
// Update UI when enemy data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for dungeon start response
|
||||
this.game.socket.on('dungeon_started', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon started:', data);
|
||||
|
||||
// Handle error responses
|
||||
if (data.success === false) {
|
||||
console.error('[DUNGEON SYSTEM] Failed to start dungeon:', data.error);
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(data.error, 'error', 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing dungeon state first
|
||||
if (this.currentDungeon) {
|
||||
console.warn('[DUNGEON SYSTEM] Clearing existing dungeon state before starting new one');
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
}
|
||||
|
||||
this.currentDungeon = data.instance;
|
||||
this.isExploring = true;
|
||||
this.dungeonProgress = 0;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] About to update UI - State:', {
|
||||
currentDungeon: !!this.currentDungeon,
|
||||
isExploring: this.isExploring,
|
||||
dungeonProgress: this.dungeonProgress,
|
||||
gameUIManager: !!this.game.systems.ui,
|
||||
instanceId: this.currentDungeon?.id
|
||||
});
|
||||
|
||||
// Update UI to show dungeon exploration
|
||||
this.updateUI();
|
||||
|
||||
// Show notification to player
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(`Entered ${data.instance.dungeonId} dungeon!`, 'success', 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for encounter response
|
||||
this.game.socket.on('encounter_data', (data) => {
|
||||
// Skip duplicate events
|
||||
if (this.lastEncounterData &&
|
||||
this.lastEncounterData.encounterIndex === data.encounterIndex &&
|
||||
this.lastEncounterData.encounter?.name === data.encounter?.name) {
|
||||
console.log('[DUNGEON SYSTEM] Skipping duplicate encounter data');
|
||||
return;
|
||||
}
|
||||
this.lastEncounterData = data;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Encounter received:', data);
|
||||
console.log('[DUNGEON SYSTEM] Current state before update:', {
|
||||
currentDungeonId: this.currentDungeon?.id,
|
||||
currentProgress: this.dungeonProgress,
|
||||
newEncounterIndex: data.encounterIndex,
|
||||
encounterType: data.encounter?.type,
|
||||
encounterName: data.encounter?.name
|
||||
});
|
||||
|
||||
this.currentRoom = data.encounter;
|
||||
this.dungeonProgress = data.encounterIndex; // Use server data, not local increment
|
||||
|
||||
// Update UI to show the new encounter
|
||||
this.updateUI();
|
||||
});
|
||||
|
||||
// Listen for encounter completion (auto-combat)
|
||||
this.game.socket.on('encounter_completed', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Encounter completed:', data);
|
||||
if (data.success) {
|
||||
// Check if dungeon is complete
|
||||
if (data.isComplete) {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completed!');
|
||||
|
||||
// Clear all dungeon state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.dungeonProgress = 0;
|
||||
this.isExploring = false;
|
||||
this.lastEncounterData = null;
|
||||
this.lastNextRoomData = null;
|
||||
|
||||
// Show completion notification
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification('Dungeon completed! 🎉', 'success', 5000);
|
||||
}
|
||||
|
||||
// Force UI to show dungeon list
|
||||
setTimeout(() => {
|
||||
this.updateUI();
|
||||
}, 1000);
|
||||
} else {
|
||||
this.currentRoom = data.nextEncounter;
|
||||
this.dungeonProgress = data.encounterIndex;
|
||||
|
||||
// Show rewards notification
|
||||
if (data.rewards && (data.rewards.credits > 0 || data.rewards.experience > 0)) {
|
||||
const rewardText = [];
|
||||
if (data.rewards.credits > 0) rewardText.push(`${data.rewards.credits} credits`);
|
||||
if (data.rewards.experience > 0) rewardText.push(`${data.rewards.experience} exp`);
|
||||
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(`Combat complete! Gained: ${rewardText.join(', ')}`, 'success', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI to show the new state
|
||||
this.updateUI();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Error completing encounter:', data.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for next room response
|
||||
this.game.socket.on('next_room_data', (data) => {
|
||||
// Skip duplicate events
|
||||
if (this.lastNextRoomData &&
|
||||
this.lastNextRoomData.encounterIndex === data.encounterIndex &&
|
||||
this.lastNextRoomData.encounter?.name === data.encounter?.name) {
|
||||
console.log('[DUNGEON SYSTEM] Skipping duplicate next room data');
|
||||
return;
|
||||
}
|
||||
this.lastNextRoomData = data;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Next room received:', data);
|
||||
console.log('[DUNGEON SYSTEM] Current state before update:', {
|
||||
currentDungeonId: this.currentDungeon?.id,
|
||||
currentProgress: this.dungeonProgress,
|
||||
newEncounterIndex: data.encounterIndex,
|
||||
encounterType: data.encounter?.type,
|
||||
encounterName: data.encounter?.name,
|
||||
isComplete: data.isComplete
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
this.currentRoom = data.encounter;
|
||||
this.dungeonProgress = data.encounterIndex;
|
||||
|
||||
// Update UI to show the new room
|
||||
this.updateUI();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Error moving to next room:', data.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for dungeon completion response
|
||||
this.game.socket.on('dungeon_completed', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completed:', data);
|
||||
// Reset dungeon state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
});
|
||||
|
||||
// Listen for dungeon status response
|
||||
this.game.socket.on('dungeon_status', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon status received:', data);
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Socket event listeners set up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dungeon data from server using Socket.IO packets
|
||||
*/
|
||||
async loadServerData() {
|
||||
try {
|
||||
console.log('[DUNGEON SYSTEM] Loading dungeon data from server via packets...');
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request dungeons from server
|
||||
this.game.socket.emit('get_dungeons');
|
||||
|
||||
// Request room types from server
|
||||
this.game.socket.emit('get_room_types');
|
||||
|
||||
// Request enemy templates from server
|
||||
this.game.socket.emit('get_enemy_templates');
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Server data requests sent via packets');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error loading server data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available dungeons
|
||||
*/
|
||||
getAllDungeons() {
|
||||
return this.serverDungeons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dungeons by difficulty
|
||||
*/
|
||||
getDungeonsByDifficulty(difficulty) {
|
||||
return this.serverDungeons.filter(dungeon => dungeon.difficulty === difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific dungeon by ID
|
||||
*/
|
||||
getDungeon(dungeonId) {
|
||||
return this.serverDungeons.find(dungeon => dungeon.id === dungeonId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get room type by ID
|
||||
*/
|
||||
getRoomType(roomTypeId) {
|
||||
return this.roomTypes[roomTypeId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enemy template by ID
|
||||
*/
|
||||
getEnemyTemplate(enemyId) {
|
||||
return this.enemyTemplates[enemyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start exploring a dungeon using Socket.IO packets
|
||||
*/
|
||||
async startDungeon(dungeonId) {
|
||||
try {
|
||||
console.log(`[DUNGEON SYSTEM] Starting dungeon: ${dungeonId}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to start dungeon
|
||||
this.game.socket.emit('start_dungeon', {
|
||||
dungeonId: dungeonId,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon start packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error starting dungeon:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process encounter in current dungeon room
|
||||
*/
|
||||
async processEncounter() {
|
||||
// Debounce to prevent multiple rapid clicks
|
||||
const now = Date.now();
|
||||
if (now - this.lastProcessTime < this.processCooldown) {
|
||||
console.log('[DUNGEON SYSTEM] Process throttled, please wait...');
|
||||
return null;
|
||||
}
|
||||
this.lastProcessTime = now;
|
||||
|
||||
try {
|
||||
// Safety check - make sure we have an active dungeon
|
||||
if (!this.currentDungeon) {
|
||||
console.error('[DUNGEON SYSTEM] No active dungeon to process encounter for');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Processing encounter for dungeon: ${this.currentDungeon.id}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to process encounter
|
||||
this.game.socket.emit('process_encounter', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Encounter process packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error processing encounter:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete current dungeon using Socket.IO packets
|
||||
*/
|
||||
async completeDungeon() {
|
||||
if (!this.currentDungeon || !this.isExploring) {
|
||||
console.warn('[DUNGEON SYSTEM] No active dungeon to complete');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[DUNGEON SYSTEM] Completing dungeon: ${this.currentDungeon.id}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to complete dungeon
|
||||
this.game.socket.emit('complete_dungeon', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completion packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error completing dungeon:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player's current dungeon status using Socket.IO packets
|
||||
*/
|
||||
async getDungeonStatus() {
|
||||
try {
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to get dungeon status
|
||||
this.game.socket.emit('get_dungeon_status', {
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon status request packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error getting dungeon status:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force generate dungeon list (bypasses throttle)
|
||||
*/
|
||||
forceGenerateDungeonList() {
|
||||
this.lastGenerationTime = 0; // Reset throttle
|
||||
this.generateDungeonList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dungeon list UI using server data
|
||||
*/
|
||||
generateDungeonList() {
|
||||
const now = Date.now();
|
||||
|
||||
// Throttle generation to prevent excessive calls
|
||||
if (now - this.lastGenerationTime < this.generationThrottle) {
|
||||
return; // Silently skip instead of logging
|
||||
}
|
||||
|
||||
this.lastGenerationTime = now;
|
||||
// console.log('[DUNGEON SYSTEM] Generating dungeon list UI');
|
||||
|
||||
const dungeonListElement = document.getElementById('dungeonList');
|
||||
if (!dungeonListElement) {
|
||||
console.error('[DUNGEON SYSTEM] Dungeon list element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
dungeonListElement.innerHTML = '';
|
||||
|
||||
if (!this.serverDungeons || Object.keys(this.serverDungeons).length === 0) {
|
||||
dungeonListElement.innerHTML = '<p>Loading dungeons from server...</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate HTML for each difficulty category
|
||||
let html = '';
|
||||
|
||||
Object.entries(this.serverDungeons).forEach(([difficulty, dungeons]) => {
|
||||
if (!dungeons || dungeons.length === 0) return;
|
||||
|
||||
const difficultyClass = difficulty === 'tutorial' ? 'tutorial' : difficulty;
|
||||
const difficultyTitle = difficulty === 'tutorial' ? 'Tutorial Dungeons' :
|
||||
difficulty.charAt(0).toUpperCase() + difficulty.slice(1) + ' Dungeons';
|
||||
const difficultyIcon = this.getDifficultyIcon(difficulty);
|
||||
const sectionId = `dungeon-section-${difficulty}`;
|
||||
|
||||
// Add collapsible difficulty header
|
||||
html += `
|
||||
<div class="dungeon-section">
|
||||
<div class="difficulty-header ${difficultyClass} collapsible" onclick="toggleDungeonSection('${sectionId}')">
|
||||
<div class="header-content">
|
||||
<i class="${difficultyIcon}"></i>
|
||||
<span>${difficultyTitle}</span>
|
||||
<span class="dungeon-count">(${dungeons.length})</span>
|
||||
</div>
|
||||
<div class="collapse-indicator">
|
||||
<i class="fas fa-chevron-down" id="${sectionId}-indicator"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dungeon-content" id="${sectionId}">
|
||||
`;
|
||||
|
||||
dungeons.forEach(dungeon => {
|
||||
const canEnter = this.canEnterDungeon(dungeon);
|
||||
const statusClass = canEnter ? 'available' : 'locked';
|
||||
const energyCost = dungeon.energyCost || 0;
|
||||
const healthType = dungeon.healthType || 'player';
|
||||
const healthIcon = healthType === 'ship' ? '🚀' : '👤';
|
||||
|
||||
// Each dungeon in its own individual container using proper CSS classes
|
||||
html += `
|
||||
<div class="dungeon-item ${statusClass}" data-dungeon-id="${dungeon.id}">
|
||||
<div class="dungeon-name">${dungeon.name}</div>
|
||||
<div class="dungeon-difficulty ${difficulty}">
|
||||
<i class="${difficultyIcon}"></i> ${difficulty} - ${energyCost} Energy
|
||||
</div>
|
||||
<div class="dungeon-description">${dungeon.description}</div>
|
||||
<div class="health-type">${healthIcon}</div>
|
||||
<div class="dungeon-enemies">
|
||||
<strong>Enemies:</strong>
|
||||
<div class="enemy-list">
|
||||
${this.generateEnemyList(dungeon.enemyTypes || [])}
|
||||
</div>
|
||||
</div>
|
||||
<button class="dungeon-btn" ${!canEnter ? 'disabled' : ''}
|
||||
onclick="startDungeon('${dungeon.id}')">
|
||||
${canEnter ? 'Enter Dungeon' : 'Locked'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Close the section
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
dungeonListElement.innerHTML = html;
|
||||
|
||||
// Initialize default collapse states
|
||||
this.initializeDungeonSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dungeon sections with saved collapse states
|
||||
*/
|
||||
initializeDungeonSections() {
|
||||
// Default states: tutorial and easy expanded, others collapsed
|
||||
const defaultStates = {
|
||||
'dungeon-section-tutorial': false, // expanded
|
||||
'dungeon-section-easy': false, // expanded
|
||||
'dungeon-section-medium': true, // collapsed
|
||||
'dungeon-section-hard': true, // collapsed
|
||||
'dungeon-section-extreme': true // collapsed
|
||||
};
|
||||
|
||||
Object.entries(defaultStates).forEach(([sectionId, defaultCollapsed]) => {
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (section && indicator) {
|
||||
// Use saved state if available, otherwise use default
|
||||
const shouldCollapse = this.collapseStates.has(sectionId) ?
|
||||
this.collapseStates.get(sectionId) : defaultCollapsed;
|
||||
|
||||
if (shouldCollapse) {
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle dungeon section collapse/expand
|
||||
*/
|
||||
toggleDungeonSection(sectionId) {
|
||||
// Check if game and systems are available
|
||||
if (!window.game || !window.game.systems || !window.game.systems.dungeonSystem) {
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for toggle');
|
||||
return;
|
||||
}
|
||||
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (!section || !indicator) return;
|
||||
|
||||
const isCollapsed = section.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Expand
|
||||
section.classList.remove('collapsed');
|
||||
indicator.classList.remove('fa-chevron-right');
|
||||
indicator.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
// Collapse
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
|
||||
// Save the state
|
||||
this.collapseStates.set(sectionId, !isCollapsed);
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get difficulty icon for dungeon
|
||||
*/
|
||||
getDifficultyIcon(difficulty) {
|
||||
const icons = {
|
||||
tutorial: 'fas fa-graduation-cap',
|
||||
easy: 'fas fa-smile',
|
||||
medium: 'fas fa-meh',
|
||||
hard: 'fas fa-frown',
|
||||
extreme: 'fas fa-skull'
|
||||
};
|
||||
return icons[difficulty] || 'fas fa-question';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate enemy list HTML for dungeon
|
||||
*/
|
||||
generateEnemyList(enemyTypes) {
|
||||
if (!enemyTypes || enemyTypes.length === 0) {
|
||||
return '<span class="no-enemies">No enemies</span>';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
enemyTypes.forEach(enemyType => {
|
||||
const enemy = this.getEnemyTemplate(enemyType);
|
||||
if (enemy) {
|
||||
html += `
|
||||
<div class="enemy-item">
|
||||
<span class="enemy-name">${enemy.name}</span>
|
||||
<div class="enemy-stats">
|
||||
<span class="health">❤️ ${enemy.health}</span>
|
||||
<span class="attack">⚔️ ${enemy.attack}</span>
|
||||
<span class="defense">🛡️ ${enemy.defense}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can enter dungeon
|
||||
*/
|
||||
canEnterDungeon(dungeon) {
|
||||
if (!this.game.systems.player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const playerLevel = this.game.systems.player.stats?.level || 1;
|
||||
const minLevel = dungeon.minLevel || 1;
|
||||
const maxLevel = dungeon.maxLevel || 999;
|
||||
const energyCost = dungeon.energyCost || 0;
|
||||
const playerEnergy = this.game.systems.player.attributes?.energy || 0;
|
||||
|
||||
return playerLevel >= minLevel &&
|
||||
playerLevel <= maxLevel &&
|
||||
playerEnergy >= energyCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit current dungeon
|
||||
*/
|
||||
exitDungeon() {
|
||||
console.log('[DUNGEON SYSTEM] Exiting dungeon');
|
||||
|
||||
if (this.currentDungeon) {
|
||||
// Send exit packet to server
|
||||
if (this.game.socket) {
|
||||
this.game.socket.emit('exit_dungeon', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset local state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
|
||||
// Update UI to show dungeon list
|
||||
this.updateUI();
|
||||
|
||||
// Show notification
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification('Exited dungeon', 'info', 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next room (for rooms without enemies)
|
||||
*/
|
||||
moveToNextRoom() {
|
||||
console.log('[DUNGEON SYSTEM] Moving to next room');
|
||||
|
||||
if (!this.currentDungeon) {
|
||||
console.warn('[DUNGEON SYSTEM] No active dungeon to continue');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request next room from server
|
||||
if (this.game.socket) {
|
||||
this.game.socket.emit('next_room', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI with current dungeon information
|
||||
*/
|
||||
updateUI() {
|
||||
if (this.game.systems.ui) {
|
||||
this.game.systems.ui.updateDungeonUI({
|
||||
currentDungeon: this.currentDungeon,
|
||||
currentRoom: this.currentRoom,
|
||||
progress: this.dungeonProgress,
|
||||
isExploring: this.isExploring
|
||||
});
|
||||
} else {
|
||||
console.warn('[DUNGEON SYSTEM] UI manager not available in game.systems.ui');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize system and load server data
|
||||
*/
|
||||
async initialize() {
|
||||
console.log('[DUNGEON SYSTEM] Initializing client dungeon system...');
|
||||
|
||||
// Set up socket listeners if not already done
|
||||
if (!this.game.socket) {
|
||||
console.warn('[DUNGEON SYSTEM] Socket not available during initialization, will retry...');
|
||||
// Retry after a short delay
|
||||
setTimeout(() => {
|
||||
if (this.game.socket) {
|
||||
this.setupSocketListeners();
|
||||
this.loadServerData();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Socket still not available after retry');
|
||||
}
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupSocketListeners();
|
||||
await this.loadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
// Export DungeonSystem to global scope
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DungeonSystem = DungeonSystem;
|
||||
}
|
||||
|
||||
// Export for use in GameEngine
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DungeonSystem;
|
||||
}
|
||||
@ -1,378 +0,0 @@
|
||||
/**
|
||||
* 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 (online rates)
|
||||
this.productionRates = {
|
||||
credits: 0.1, // 1 credit every 10 seconds (0.1 per second)
|
||||
experience: 0, // no auto experience - only from dungeons
|
||||
energy: 1/300 // 1 energy every 5 minutes (1/300 per second)
|
||||
};
|
||||
|
||||
// Offline rates (different from online rates)
|
||||
this.offlineProductionRates = {
|
||||
credits: 1/60, // 1 credit every 1 minute (1/60 per second)
|
||||
experience: 0, // no experience offline - only from dungeons
|
||||
energy: 1/300 // 1 energy every 5 minutes (same as online)
|
||||
};
|
||||
|
||||
// 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() {
|
||||
// In multiplayer mode, use server communication
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
this.game.showNotification('Claiming offline rewards from server...', 'info', 2000);
|
||||
|
||||
// Send request to server
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('claimOfflineRewards', {});
|
||||
} else {
|
||||
this.game.showNotification('Not connected to server', 'error', 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Singleplayer mode - use local logic
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,468 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Item System
|
||||
* Dynamically loads and manages items from the GameServer
|
||||
*/
|
||||
|
||||
class ItemSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Item storage
|
||||
this.itemCatalog = new Map(); // itemId -> item data
|
||||
this.shopItems = []; // Array of shop items (legacy)
|
||||
this.shopItemsByCategory = {}; // Categorized shop items (new structure)
|
||||
this.lastUpdated = null;
|
||||
|
||||
// Loading state
|
||||
this.isLoading = false;
|
||||
this.loadPromise = null;
|
||||
|
||||
// Event listeners
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the item system and load data from server
|
||||
*/
|
||||
async initialize() {
|
||||
console.log('[ITEM SYSTEM] Initializing client item system');
|
||||
|
||||
if (this.loadPromise) {
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
this.loadPromise = this.loadFromServer();
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all items from the GameServer
|
||||
*/
|
||||
async loadFromServer() {
|
||||
if (this.isLoading) {
|
||||
console.log('[ITEM SYSTEM] Already loading items from server');
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
console.log('[ITEM SYSTEM] Loading items from GameServer - Multiplayer Mode');
|
||||
console.log('[ITEM SYSTEM] Socket connection status:', !!window.game?.socket);
|
||||
|
||||
if (!window.game || !window.game.socket) {
|
||||
throw new Error('Not connected to server - multiplayer mode requires server connection');
|
||||
}
|
||||
|
||||
// Load shop items from server
|
||||
const shopItems = await this.fetchShopItems();
|
||||
|
||||
// Handle new shop structure (categorized) vs old structure (flat array)
|
||||
let totalItems = 0;
|
||||
if (Array.isArray(shopItems)) {
|
||||
// Old structure: flat array
|
||||
totalItems = shopItems.length;
|
||||
console.log('[ITEM SYSTEM] Received', totalItems, 'items from server (old structure)');
|
||||
this.processServerItems(shopItems);
|
||||
this.shopItemsByCategory = {}; // Clear categorized data
|
||||
} else if (shopItems && typeof shopItems === 'object') {
|
||||
// New structure: categorized object
|
||||
totalItems = Object.values(shopItems).reduce((sum, categoryItems) => sum + categoryItems.length, 0);
|
||||
console.log('[ITEM SYSTEM] Received', totalItems, 'items from server (new structure)');
|
||||
console.log('[ITEM SYSTEM] Categories:', Object.keys(shopItems));
|
||||
|
||||
// Store categorized data
|
||||
this.shopItemsByCategory = shopItems;
|
||||
|
||||
// Flatten all items for processing
|
||||
const allItems = Object.values(shopItems).flat();
|
||||
this.processServerItems(allItems);
|
||||
} else {
|
||||
console.warn('[ITEM SYSTEM] Invalid shop items structure received:', typeof shopItems);
|
||||
totalItems = 0;
|
||||
this.shopItemsByCategory = {};
|
||||
}
|
||||
|
||||
this.lastUpdated = Date.now();
|
||||
console.log(`[ITEM SYSTEM] Successfully loaded ${this.itemCatalog.size} items from server`);
|
||||
console.log('[ITEM SYSTEM] Item categories loaded:', Object.keys(this.itemCatalog).length);
|
||||
|
||||
// Emit loaded event
|
||||
this.emit('itemsLoaded', {
|
||||
itemCount: this.itemCatalog.size,
|
||||
shopItemCount: this.shopItems.length,
|
||||
timestamp: this.lastUpdated
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ITEM SYSTEM] Failed to load items from server:', error);
|
||||
console.error('[ITEM SYSTEM] Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
socketConnected: !!window.game?.socket,
|
||||
socketId: window.game?.socket?.id
|
||||
});
|
||||
|
||||
// No fallback - emit error event
|
||||
this.emit('itemsLoadError', error);
|
||||
return false;
|
||||
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch shop items from the GameServer
|
||||
*/
|
||||
async fetchShopItems() {
|
||||
console.log('[ITEM SYSTEM] Starting fetchShopItems');
|
||||
|
||||
if (!window.game || !window.game.socket) {
|
||||
console.error('[ITEM SYSTEM] No socket connection available');
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Socket ID:', window.game.socket.id);
|
||||
console.log('[ITEM SYSTEM] Socket connected:', window.game.socket.connected);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('[ITEM SYSTEM] Server request timeout after 10 seconds');
|
||||
window.game.socket.off('shopItemsReceived', handleResponse);
|
||||
reject(new Error('Server request timeout'));
|
||||
}, 10000);
|
||||
|
||||
// Test server connection first
|
||||
console.log('[ITEM SYSTEM] Testing server connection...');
|
||||
window.game.socket.emit('ping', { timestamp: Date.now() });
|
||||
|
||||
// Listen for ping response
|
||||
const pingHandler = (data) => {
|
||||
console.log('[ITEM SYSTEM] Ping response received:', data);
|
||||
console.log('[ITEM SYSTEM] Server is responding! Ping roundtrip:', Date.now() - data.received, 'ms');
|
||||
window.game.socket.off('ping', pingHandler);
|
||||
window.game.socket.off('pong', pingHandler);
|
||||
};
|
||||
window.game.socket.on('ping', pingHandler);
|
||||
|
||||
// Listen for pong response (backup)
|
||||
const pongHandler = (data) => {
|
||||
console.log('[ITEM SYSTEM] Pong response received:', data);
|
||||
console.log('[ITEM SYSTEM] Server is responding! Pong roundtrip:', Date.now() - data.timestamp, 'ms');
|
||||
window.game.socket.off('pong', pongHandler);
|
||||
};
|
||||
window.game.socket.on('pong', pongHandler);
|
||||
|
||||
// Request shop items from server
|
||||
console.log('[ITEM SYSTEM] Emitting getShopItems request');
|
||||
console.log('[ITEM SYSTEM] Socket state:', {
|
||||
connected: window.game.socket.connected,
|
||||
id: window.game.socket.id
|
||||
});
|
||||
|
||||
window.game.socket.emit('getShopItems', {});
|
||||
console.log('[ITEM SYSTEM] Request sent, waiting for response...');
|
||||
|
||||
// Listen for response
|
||||
const handleResponse = (data) => {
|
||||
console.log('[ITEM SYSTEM] Received shopItemsReceived response:', data);
|
||||
clearTimeout(timeout);
|
||||
window.game.socket.off('shopItemsReceived', handleResponse);
|
||||
|
||||
console.log('[ITEM SYSTEM] Response success:', data.success);
|
||||
console.log('[ITEM SYSTEM] Response shopItems keys:', data.shopItems ? Object.keys(data.shopItems) : 'none');
|
||||
|
||||
if (data.success) {
|
||||
console.log('[ITEM SYSTEM] Successfully received shop data');
|
||||
console.log('[ITEM SYSTEM] Response timestamp:', data.timestamp);
|
||||
|
||||
// Log item counts per category
|
||||
if (data.shopItems) {
|
||||
Object.entries(data.shopItems).forEach(([category, items]) => {
|
||||
console.log(`[ITEM SYSTEM] ${category}: ${items.length} items`);
|
||||
});
|
||||
}
|
||||
|
||||
resolve(data.shopItems || {});
|
||||
} else {
|
||||
console.error('[ITEM SYSTEM] Server returned error:', data.error);
|
||||
reject(new Error(data.error || 'Failed to load shop items'));
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ITEM SYSTEM] Setting up shopItemsReceived listener');
|
||||
window.game.socket.on('shopItemsReceived', handleResponse);
|
||||
|
||||
// Verify the listener was set up
|
||||
const listeners = window.game.socket.listeners('shopItemsReceived');
|
||||
console.log('[ITEM SYSTEM] shopItemsReceived listeners count:', listeners.length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch specific item details from server
|
||||
*/
|
||||
async fetchItemDetails(itemId) {
|
||||
if (!window.game || !window.game.socket) {
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Server request timeout'));
|
||||
}, 5000);
|
||||
|
||||
// Request item details from server
|
||||
window.game.socket.emit('getItemDetails', { itemId });
|
||||
|
||||
// Listen for response
|
||||
const handleResponse = (data) => {
|
||||
clearTimeout(timeout);
|
||||
window.game.socket.off('itemDetailsReceived', handleResponse);
|
||||
|
||||
if (data.success) {
|
||||
// Cache the item
|
||||
this.itemCatalog.set(itemId, data.item);
|
||||
resolve(data.item);
|
||||
} else {
|
||||
reject(new Error(data.error || 'Item not found'));
|
||||
}
|
||||
};
|
||||
|
||||
window.game.socket.on('itemDetailsReceived', handleResponse);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process items received from server
|
||||
*/
|
||||
processServerItems(items) {
|
||||
// Safety check for items parameter
|
||||
if (!items || !Array.isArray(items)) {
|
||||
console.error('[ITEM SYSTEM] Invalid items parameter:', items);
|
||||
console.error('[ITEM SYSTEM] Expected array, got:', typeof items);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Processing', items.length, 'items from server');
|
||||
console.log('[ITEM SYSTEM] Sample items:', items.slice(0, 3));
|
||||
|
||||
this.itemCatalog.clear();
|
||||
this.shopItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
// Store in catalog
|
||||
this.itemCatalog.set(item.id, item);
|
||||
|
||||
// Add to shop items if available for shop
|
||||
if (item.categories && item.categories.includes('shop')) {
|
||||
this.shopItems.push(item);
|
||||
}
|
||||
|
||||
// console.log('[ITEM SYSTEM] Added item:', {
|
||||
// id: item.id,
|
||||
// name: item.name,
|
||||
// type: item.type,
|
||||
// rarity: item.rarity,
|
||||
// price: item.price,
|
||||
// categories: item.categories
|
||||
// });
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Processing complete - Catalog:', this.itemCatalog.size, 'Shop items:', this.shopItems.length);
|
||||
console.log('[ITEM SYSTEM] Shop items by type:', this.shopItems.reduce((acc, item) => {
|
||||
acc[item.type] = (acc[item.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item by ID
|
||||
*/
|
||||
getItem(itemId) {
|
||||
// Return from cache if available
|
||||
if (this.itemCatalog.has(itemId)) {
|
||||
return this.itemCatalog.get(itemId);
|
||||
}
|
||||
|
||||
// Try to fetch from server if not cached
|
||||
if (window.game && window.game.socket) {
|
||||
this.fetchItemDetails(itemId).catch(error => {
|
||||
console.warn(`[ITEM SYSTEM] Failed to fetch item ${itemId}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all shop items
|
||||
*/
|
||||
getShopItems() {
|
||||
return [...this.shopItems];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shop items by category (new structure)
|
||||
*/
|
||||
getShopItemsByCategory() {
|
||||
return this.shopItemsByCategory || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by category
|
||||
*/
|
||||
getItemsByCategory(category) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item =>
|
||||
item.type === category || (item.categories && item.categories.includes(category))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by type
|
||||
*/
|
||||
getItemsByType(type) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item => item.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by rarity
|
||||
*/
|
||||
getItemsByRarity(rarity) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item => item.rarity === rarity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can use item based on requirements
|
||||
*/
|
||||
canPlayerUseItem(item, playerLevel = null) {
|
||||
if (!item.requirements) return true;
|
||||
|
||||
// Get player level if not provided
|
||||
if (playerLevel === null && window.game && window.game.systems && window.game.systems.player) {
|
||||
playerLevel = window.game.systems.player.level;
|
||||
}
|
||||
|
||||
// Check level requirement
|
||||
if (item.requirements.level && playerLevel < item.requirements.level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add other requirement checks here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered shop items for current player
|
||||
*/
|
||||
getAvailableShopItems() {
|
||||
return this.shopItems.filter(item => this.canPlayerUseItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format item price for display
|
||||
*/
|
||||
formatPrice(item) {
|
||||
if (!item.price) return 'Free';
|
||||
|
||||
const currency = item.currency || 'credits';
|
||||
const price = this.game.formatNumber(item.price);
|
||||
|
||||
return `${price} ${currency}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item rarity color
|
||||
*/
|
||||
getRarityColor(rarity) {
|
||||
const colors = {
|
||||
common: '#888888',
|
||||
uncommon: '#00ff00',
|
||||
rare: '#0088ff',
|
||||
legendary: '#ff8800',
|
||||
epic: '#ff00ff'
|
||||
};
|
||||
|
||||
return colors[rarity] || '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh items from server
|
||||
*/
|
||||
async refresh() {
|
||||
console.log('[ITEM SYSTEM] Refreshing items from server');
|
||||
return this.loadFromServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event system
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
this.eventListeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
for (const callback of this.eventListeners.get(event)) {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`[ITEM SYSTEM] Error in event listener for ${event}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* catalog getter — alias for shopItemsByCategory, used by Economy.updateShopUI
|
||||
*/
|
||||
get catalog() {
|
||||
return this.shopItemsByCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
totalItems: this.itemCatalog.size,
|
||||
shopItems: this.shopItems.length,
|
||||
lastUpdated: this.lastUpdated,
|
||||
isLoading: this.isLoading,
|
||||
socketConnected: !!(window.game?.socket),
|
||||
socketId: window.game?.socket?.id
|
||||
};
|
||||
|
||||
// Add category breakdown
|
||||
stats.categories = {};
|
||||
for (const item of this.itemCatalog.values()) {
|
||||
stats.categories[item.type] = (stats.categories[item.type] || 0) + 1;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ItemSystem;
|
||||
} else {
|
||||
window.ItemSystem = ItemSystem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,247 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ship image URL from server or local
|
||||
*/
|
||||
getShipImageUrl(ship) {
|
||||
if (!ship) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
// For multiplayer, get from server
|
||||
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||
return `${serverUrl}/images/ships/${ship.id}.png`;
|
||||
}
|
||||
|
||||
// For singleplayer, use local path
|
||||
return ship.image || ship.texture || 'assets/textures/ships/starter_cruiser.png';
|
||||
}
|
||||
|
||||
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="${this.getShipImageUrl(ship)}" alt="${ship.name}"
|
||||
onerror="this.src='${window.smartSaveManager?.isMultiplayer ? 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png' : 'assets/textures/missing-texture.png'}'"
|
||||
class="ship-card-image">
|
||||
<div class="ship-card-info">
|
||||
<div class="ship-card-rarity ${ship.rarity.toLowerCase()}">${ship.rarity}</div>
|
||||
</div>
|
||||
</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 server image for multiplayer, local for singleplayer
|
||||
let imagePath;
|
||||
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || 'http://localhost:3002';
|
||||
imagePath = `${serverUrl}/images/ships/${ship.class || 'starter_cruiser'}.png`;
|
||||
} else {
|
||||
imagePath = ship.texture || `assets/textures/ships/starter_cruiser.png`;
|
||||
}
|
||||
|
||||
elements.currentShipImage.src = imagePath;
|
||||
elements.currentShipImage.alt = ship.name;
|
||||
elements.currentShipImage.onerror = function() {
|
||||
this.src = window.smartSaveManager?.isMultiplayer ?
|
||||
'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png' :
|
||||
'assets/textures/missing-texture.png';
|
||||
};
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,407 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Skill System
|
||||
* Skill definitions are loaded from the server; this file handles
|
||||
* local progression tracking, UI rendering, and skill-point spending.
|
||||
*/
|
||||
|
||||
class SkillSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Populated after server responds to 'get_skills'
|
||||
this.skills = { combat: {}, science: {}, crafting: {} };
|
||||
|
||||
this.categories = {
|
||||
combat: 'Combat',
|
||||
science: 'Science',
|
||||
crafting: 'Crafting'
|
||||
};
|
||||
|
||||
this.experienceRates = { combat: 1.0, science: 0.8, crafting: 0.6 };
|
||||
this.activeBuffs = {};
|
||||
|
||||
// Loading state
|
||||
this._loaded = false;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Initialisation — request skill definitions from the server
|
||||
// ------------------------------------------------------------------ //
|
||||
async initialize() {
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
console.log('[SKILL SYSTEM] Requesting skill definitions from server');
|
||||
|
||||
if (!window.game?.socket) {
|
||||
console.warn('[SKILL SYSTEM] No socket connection — skills will load when connected');
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const serverSkills = await this._fetchSkillsFromServer();
|
||||
this._applyServerDefinitions(serverSkills);
|
||||
this._loaded = true;
|
||||
console.log('[SKILL SYSTEM] Skill definitions loaded from server');
|
||||
} catch (err) {
|
||||
console.error('[SKILL SYSTEM] Failed to load skills from server:', err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
_fetchSkillsFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = window.game.socket;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
socket.off('skills_data', handler);
|
||||
reject(new Error('Skill data request timed out'));
|
||||
}, 10000);
|
||||
|
||||
const handler = (data) => {
|
||||
clearTimeout(timeout);
|
||||
socket.off('skills_data', handler);
|
||||
if (data && (Array.isArray(data) || typeof data === 'object')) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error('Invalid skill data from server'));
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('skills_data', handler);
|
||||
socket.emit('get_skills');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge server skill definitions into the local skill map.
|
||||
* Preserves any progress already loaded from playerData.
|
||||
*/
|
||||
_applyServerDefinitions(serverSkills) {
|
||||
// Server may return an array or a categorised object
|
||||
const asList = Array.isArray(serverSkills)
|
||||
? serverSkills
|
||||
: Object.values(serverSkills).flat();
|
||||
|
||||
// Reset to empty categories first
|
||||
this.skills = { combat: {}, science: {}, crafting: {} };
|
||||
|
||||
for (const skill of asList) {
|
||||
const cat = skill.category || 'combat';
|
||||
if (!this.skills[cat]) this.skills[cat] = {};
|
||||
|
||||
// Keep any existing progress if already loaded from save data
|
||||
const existing = this.skills[cat][skill.id] || {};
|
||||
this.skills[cat][skill.id] = {
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
maxLevel: skill.maxLevel || 100,
|
||||
effects: skill.bonuses || skill.effects || {},
|
||||
icon: skill.icon || 'fa-star',
|
||||
// Progress fields — kept from existing save data if present
|
||||
currentLevel: existing.currentLevel ?? 0,
|
||||
experience: existing.experience ?? 0,
|
||||
experienceToNext: existing.experienceToNext ?? (skill.experiencePerLevel || 1000),
|
||||
unlocked: existing.unlocked ?? (skill.defaultUnlocked !== false),
|
||||
requiredLevel: skill.requiredLevel ?? null
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[SKILL SYSTEM] Applied server definitions. Categories:', Object.keys(this.skills));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Skill progression
|
||||
// ------------------------------------------------------------------ //
|
||||
addSkillExperience(category, skillId, amount) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
|
||||
|
||||
skill.experience += amount;
|
||||
|
||||
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];
|
||||
const excess = skill.experience - skill.experienceToNext;
|
||||
|
||||
skill.currentLevel++;
|
||||
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
|
||||
skill.experience = Math.max(0, excess);
|
||||
|
||||
this.applySkillEffects();
|
||||
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
player.stats.skillPoints--;
|
||||
this.levelUpSkill(category, skillId);
|
||||
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
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;
|
||||
}
|
||||
|
||||
player.stats.skillPoints -= 2;
|
||||
skill.unlocked = true;
|
||||
skill.currentLevel = 1;
|
||||
this.applySkillEffects();
|
||||
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
|
||||
return true;
|
||||
}
|
||||
|
||||
applySkillEffects() {
|
||||
const player = this.game.systems.player;
|
||||
this.resetToBaseStats();
|
||||
|
||||
for (const category of Object.values(this.skills)) {
|
||||
for (const skill of Object.values(category)) {
|
||||
if (!skill.unlocked || skill.currentLevel <= 0) continue;
|
||||
|
||||
for (const [effect, value] of Object.entries(skill.effects || {})) {
|
||||
const total = value * skill.currentLevel;
|
||||
switch (effect) {
|
||||
case 'attack': player.attributes.attack += total; break;
|
||||
case 'defense': player.attributes.defense += total; break;
|
||||
case 'speed': player.attributes.speed += total; break;
|
||||
case 'health':
|
||||
case 'maxHealth': player.attributes.maxHealth += total; break;
|
||||
case 'maxEnergy': player.attributes.maxEnergy += total; break;
|
||||
case 'criticalChance': player.attributes.criticalChance += total; break;
|
||||
case 'criticalDamage': player.attributes.criticalDamage += total; break;
|
||||
default:
|
||||
if (!this.activeBuffs[effect]) this.activeBuffs[effect] = 0;
|
||||
this.activeBuffs[effect] += total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.updateUI();
|
||||
}
|
||||
|
||||
resetToBaseStats() {
|
||||
const player = this.game.systems.player;
|
||||
const lvl = player.stats.level || 1;
|
||||
Object.assign(player.attributes, {
|
||||
attack: 10 + (lvl - 1) * 2,
|
||||
defense: 5 + (lvl - 1) * 1,
|
||||
speed: 10,
|
||||
maxHealth: 100 + (lvl - 1) * 10,
|
||||
maxEnergy: 100 + (lvl - 1) * 5,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
});
|
||||
this.activeBuffs = {};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Combat / science / crafting XP helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
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', 'weapons_crafting', amount);
|
||||
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Queries
|
||||
// ------------------------------------------------------------------ //
|
||||
getSkillLevel(category, skillId) {
|
||||
// Support single-arg form used by CraftingSystem: getSkillLevel('crafting')
|
||||
if (skillId === undefined) {
|
||||
let max = 0;
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[category]) max = Math.max(max, cat[category].currentLevel || 0);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return this.skills[category]?.[skillId]?.currentLevel || 0;
|
||||
}
|
||||
|
||||
getSkillExperience(skillId) {
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[skillId]) return cat[skillId].experience || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getExperienceNeeded(skillId) {
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[skillId]) return cat[skillId].experienceToNext || 0;
|
||||
}
|
||||
return 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
|
||||
// ------------------------------------------------------------------ //
|
||||
updateUI() {
|
||||
this.updateSkillsGrid();
|
||||
this.updateSkillPointsDisplay();
|
||||
}
|
||||
|
||||
updateSkillsGrid() {
|
||||
const grid = document.getElementById('skillsGrid');
|
||||
if (!grid) return;
|
||||
|
||||
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
|
||||
const skills = this.skills[activeCategory] || {};
|
||||
|
||||
if (!this._loaded) {
|
||||
grid.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading skills from server...</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
Object.entries(skills).forEach(([skillId, skill]) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
||||
|
||||
const progressPercent = skill.currentLevel > 0
|
||||
? (skill.experience / skill.experienceToNext) * 100
|
||||
: 0;
|
||||
|
||||
const iconClass = this.game.systems.textureManager
|
||||
? this.game.systems.textureManager.getIcon(skill.icon)
|
||||
: (skill.icon || 'fa-question');
|
||||
|
||||
el.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>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
grid.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
updateSkillPointsDisplay() {
|
||||
const player = this.game.systems.player;
|
||||
document.querySelectorAll('.skill-points').forEach(el => {
|
||||
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Save / Load
|
||||
// ------------------------------------------------------------------ //
|
||||
save() {
|
||||
return { skills: this.skills, activeBuffs: this.activeBuffs };
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.skills) {
|
||||
for (const [category, skills] of Object.entries(data.skills)) {
|
||||
if (!this.skills[category]) this.skills[category] = {};
|
||||
for (const [skillId, skillData] of Object.entries(skills)) {
|
||||
if (this.skills[category][skillId]) {
|
||||
Object.assign(this.skills[category][skillId], skillData);
|
||||
} else {
|
||||
// Store progress even before server definitions arrive
|
||||
this.skills[category][skillId] = { ...skillData };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.activeBuffs) this.activeBuffs = data.activeBuffs;
|
||||
this.applySkillEffects();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.activeBuffs = {};
|
||||
for (const category of Object.values(this.skills)) {
|
||||
for (const skill of Object.values(category)) {
|
||||
skill.currentLevel = 0;
|
||||
skill.experience = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() { this.reset(); }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "de",
|
||||
"name": "Deutsch",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"dungeons": "Dungeons",
|
||||
"skills": "Fähigkeiten",
|
||||
"base": "Basis",
|
||||
"quests": "Aufgaben",
|
||||
"inventory": "Inventar",
|
||||
"crafting": "Herstellung",
|
||||
"shop": "Laden",
|
||||
"fleet": "Flotte",
|
||||
"galaxy": "Galaxie",
|
||||
"research": "Forschung",
|
||||
"leaderboard": "Rangliste",
|
||||
"missions": "Missionen",
|
||||
"alliance": "Allianz",
|
||||
"market": "Markt",
|
||||
"social": "Sozial",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 Gebäude",
|
||||
"shipyard": "🚀 Werft",
|
||||
"starbase": "🌌 Sternenbasis",
|
||||
"overview": "📊 Übersicht"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "Metall",
|
||||
"gas": "Gas",
|
||||
"crystal": "Kristall",
|
||||
"energyCells": "Energiezellen",
|
||||
"darkMatter": "Dunkle Materie",
|
||||
"credits": "Credits",
|
||||
"gems": "Edelsteine"
|
||||
},
|
||||
"actions": {
|
||||
"build": "Bauen",
|
||||
"upgrade": "Aufwerten",
|
||||
"cancel": "Abbrechen",
|
||||
"collect": "Einsammeln",
|
||||
"launch": "Starten",
|
||||
"search": "Suchen",
|
||||
"join": "Beitreten",
|
||||
"leave": "Verlassen",
|
||||
"deposit": "Einzahlen",
|
||||
"withdraw": "Abheben",
|
||||
"buy": "Kaufen",
|
||||
"sell": "Verkaufen",
|
||||
"craft": "Herstellen",
|
||||
"research": "Forschen",
|
||||
"repair": "Reparieren",
|
||||
"equip": "Ausrüsten",
|
||||
"save": "Speichern",
|
||||
"send": "Senden",
|
||||
"speedUp": "Beschleunigen",
|
||||
"collectAll": "Alle einsammeln",
|
||||
"unequip": "Ablegen"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"inProgress": "Läuft",
|
||||
"completed": "Abgeschlossen",
|
||||
"locked": "Gesperrt",
|
||||
"maxLevel": "Max. Stufe",
|
||||
"ready": "Fertig!",
|
||||
"queued": "Eingereiht",
|
||||
"failed": "Fehlgeschlagen"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "Dungeons",
|
||||
"selectPrompt": "Wähle einen Dungeon",
|
||||
"difficulty": "Schwierigkeit",
|
||||
"minLevel": "Min. Stufe",
|
||||
"energyCost": "Energiekosten",
|
||||
"enter": "Dungeon betreten",
|
||||
"easy": "Einfach",
|
||||
"medium": "Mittel",
|
||||
"hard": "Schwer",
|
||||
"extreme": "Extrem",
|
||||
"legendary": "Legendär",
|
||||
"boss": "Boss",
|
||||
"room": "Raum",
|
||||
"rooms": "Räume",
|
||||
"loot": "Beute",
|
||||
"rewardMultiplier": "Belohnungsmultiplikator",
|
||||
"maxPlayers": "Max. Spieler",
|
||||
"clearBonus": "Abschlussbonus"
|
||||
},
|
||||
"quests": {
|
||||
"title": "Aufgaben",
|
||||
"main": "Hauptgeschichte",
|
||||
"daily": "Täglich",
|
||||
"weekly": "Wöchentlich",
|
||||
"monthly": "Monatlich",
|
||||
"completed": "Abgeschlossen",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"resetsIn": "Zurückgesetzt in",
|
||||
"claim": "Belohnung einfordern",
|
||||
"objective": "Ziel",
|
||||
"reward": "Belohnung"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "Herstellung",
|
||||
"queue": "Warteschlange",
|
||||
"queueEmpty": "Keine Gegenstände in der Warteschlange",
|
||||
"speedUpCost": "Beschleunigen ({n} Edelsteine)",
|
||||
"collectAll": "Alle fertigen einsammeln",
|
||||
"completes": "Fertig",
|
||||
"progress": "Fortschritt"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "Allianz",
|
||||
"create": "Allianz gründen",
|
||||
"find": "Allianz suchen",
|
||||
"chat": "Allianz-Chat",
|
||||
"research": "Allianzforschung",
|
||||
"warehouse": "Allianzlager",
|
||||
"members": "Mitglieder",
|
||||
"deposit": "Einzahlen",
|
||||
"withdraw": "Abheben"
|
||||
},
|
||||
"market": {
|
||||
"title": "Spielermarkt",
|
||||
"browse": "Durchsuchen",
|
||||
"sell": "Verkaufen",
|
||||
"myListings": "Meine Angebote",
|
||||
"priceHistory": "Preisverlauf",
|
||||
"fee": "2% Listungsgebühr"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventar",
|
||||
"empty": "Inventar ist leer",
|
||||
"equip": "Ausrüsten",
|
||||
"use": "Verwenden",
|
||||
"drop": "Ablegen",
|
||||
"slot": {
|
||||
"helmet": "Helm",
|
||||
"body": "Rüstung",
|
||||
"hands": "Handschuhe",
|
||||
"pants": "Beine",
|
||||
"boots": "Stiefel",
|
||||
"backpack": "Rucksack"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "Laden",
|
||||
"gems": "Edelstein-Laden",
|
||||
"ships": "Schiffe",
|
||||
"weapons": "Waffen",
|
||||
"armour": "Rüstung",
|
||||
"consumables": "Verbrauchsgüter"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"language": "Sprache",
|
||||
"fontSize": "Schriftgröße",
|
||||
"colorBlind": "Farbenblindmodus",
|
||||
"reducedMotion": "Reduzierte Bewegung",
|
||||
"volume": "Lautstärke",
|
||||
"reset": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "Zum Hauptinhalt springen",
|
||||
"closeModal": "Modal schließen",
|
||||
"openMenu": "Navigationsmenü öffnen",
|
||||
"loading": "Lädt...",
|
||||
"toastRegion": "Benachrichtigungen"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "en",
|
||||
"name": "English",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"dungeons": "Dungeons",
|
||||
"skills": "Skills",
|
||||
"base": "Base",
|
||||
"quests": "Quests",
|
||||
"inventory": "Inventory",
|
||||
"crafting": "Crafting",
|
||||
"shop": "Shop",
|
||||
"fleet": "Fleet",
|
||||
"galaxy": "Galaxy",
|
||||
"research": "Research",
|
||||
"leaderboard": "Ranks",
|
||||
"missions": "Missions",
|
||||
"alliance": "Alliance",
|
||||
"market": "Market",
|
||||
"social": "Social",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 Buildings",
|
||||
"shipyard": "🚀 Shipyard",
|
||||
"starbase": "🌌 Starbase",
|
||||
"overview": "📊 Overview"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "Metal",
|
||||
"gas": "Gas",
|
||||
"crystal": "Crystal",
|
||||
"energyCells": "Energy Cells",
|
||||
"darkMatter": "Dark Matter",
|
||||
"credits": "Credits",
|
||||
"gems": "Gems"
|
||||
},
|
||||
"actions": {
|
||||
"build": "Build",
|
||||
"upgrade": "Upgrade",
|
||||
"cancel": "Cancel",
|
||||
"collect": "Collect",
|
||||
"launch": "Launch",
|
||||
"search": "Search",
|
||||
"join": "Join",
|
||||
"leave": "Leave",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw",
|
||||
"buy": "Buy",
|
||||
"sell": "Sell",
|
||||
"craft": "Craft",
|
||||
"research": "Research",
|
||||
"repair": "Repair",
|
||||
"equip": "Equip",
|
||||
"save": "Save",
|
||||
"send": "Send",
|
||||
"speedUp": "Speed Up",
|
||||
"collectAll": "Collect All",
|
||||
"unequip": "Unequip"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"inProgress": "In Progress",
|
||||
"completed": "Completed",
|
||||
"locked": "Locked",
|
||||
"maxLevel": "Max Level",
|
||||
"ready": "Ready!",
|
||||
"queued": "Queued",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "Dungeons",
|
||||
"selectPrompt": "Select a dungeon to begin",
|
||||
"difficulty": "Difficulty",
|
||||
"minLevel": "Min. Level",
|
||||
"energyCost": "Energy Cost",
|
||||
"enter": "Enter Dungeon",
|
||||
"easy": "Easy",
|
||||
"medium": "Medium",
|
||||
"hard": "Hard",
|
||||
"extreme": "Extreme",
|
||||
"legendary": "Legendary",
|
||||
"boss": "Boss",
|
||||
"room": "Room",
|
||||
"rooms": "Rooms",
|
||||
"loot": "Loot",
|
||||
"rewardMultiplier": "Reward Multiplier",
|
||||
"maxPlayers": "Max Players",
|
||||
"clearBonus": "Clear Bonus"
|
||||
},
|
||||
"quests": {
|
||||
"title": "Quests",
|
||||
"main": "Main Story",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed",
|
||||
"resetsIn": "Resets in",
|
||||
"claim": "Claim Reward",
|
||||
"objective": "Objective",
|
||||
"reward": "Reward"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "Crafting",
|
||||
"queue": "Queue",
|
||||
"queueEmpty": "No items in queue",
|
||||
"speedUpCost": "Speed Up ({n} Gems)",
|
||||
"collectAll": "Collect All Ready",
|
||||
"completes": "Completes",
|
||||
"progress": "Progress"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "Alliance",
|
||||
"create": "Create Alliance",
|
||||
"find": "Find Alliance",
|
||||
"chat": "Alliance Chat",
|
||||
"research": "Alliance Research",
|
||||
"warehouse": "Alliance Warehouse",
|
||||
"members": "Members",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw"
|
||||
},
|
||||
"market": {
|
||||
"title": "Player Market",
|
||||
"browse": "Browse",
|
||||
"sell": "Sell",
|
||||
"myListings": "My Listings",
|
||||
"priceHistory": "Price History",
|
||||
"fee": "2% listing fee"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
"empty": "Inventory is empty",
|
||||
"equip": "Equip",
|
||||
"use": "Use",
|
||||
"drop": "Drop",
|
||||
"slot": {
|
||||
"helmet": "Helmet",
|
||||
"body": "Body Armour",
|
||||
"hands": "Gloves",
|
||||
"pants": "Legs",
|
||||
"boots": "Boots",
|
||||
"backpack": "Backpack"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "Shop",
|
||||
"gems": "Gem Store",
|
||||
"ships": "Ships",
|
||||
"weapons": "Weapons",
|
||||
"armour": "Armour",
|
||||
"consumables": "Consumables"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"language": "Language",
|
||||
"fontSize": "Font Size",
|
||||
"colorBlind": "Colour Blind Mode",
|
||||
"reducedMotion": "Reduced Motion",
|
||||
"volume": "Sound Volume",
|
||||
"reset": "Reset to Defaults"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "Skip to main content",
|
||||
"closeModal": "Close modal",
|
||||
"openMenu": "Open navigation menu",
|
||||
"loading": "Loading...",
|
||||
"toastRegion": "Notifications"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "es",
|
||||
"name": "Español",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Panel",
|
||||
"dungeons": "Mazmorras",
|
||||
"skills": "Habilidades",
|
||||
"base": "Base",
|
||||
"quests": "Misiones",
|
||||
"inventory": "Inventario",
|
||||
"crafting": "Fabricación",
|
||||
"shop": "Tienda",
|
||||
"fleet": "Flota",
|
||||
"galaxy": "Galaxia",
|
||||
"research": "Investigación",
|
||||
"leaderboard": "Clasificación",
|
||||
"missions": "Misiones",
|
||||
"alliance": "Alianza",
|
||||
"market": "Mercado",
|
||||
"social": "Social",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 Edificios",
|
||||
"shipyard": "🚀 Astillero",
|
||||
"starbase": "🌌 Base estelar",
|
||||
"overview": "📊 Resumen"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "Metal",
|
||||
"gas": "Gas",
|
||||
"crystal": "Cristal",
|
||||
"energyCells": "Celdas de energía",
|
||||
"darkMatter": "Materia oscura",
|
||||
"credits": "Créditos",
|
||||
"gems": "Gemas"
|
||||
},
|
||||
"actions": {
|
||||
"build": "Construir",
|
||||
"upgrade": "Mejorar",
|
||||
"cancel": "Cancelar",
|
||||
"collect": "Recolectar",
|
||||
"launch": "Lanzar",
|
||||
"search": "Buscar",
|
||||
"join": "Unirse",
|
||||
"leave": "Salir",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Retirar",
|
||||
"buy": "Comprar",
|
||||
"sell": "Vender",
|
||||
"craft": "Fabricar",
|
||||
"research": "Investigar",
|
||||
"repair": "Reparar",
|
||||
"equip": "Equipar",
|
||||
"save": "Guardar",
|
||||
"send": "Enviar",
|
||||
"speedUp": "Acelerar",
|
||||
"collectAll": "Recolectar todo",
|
||||
"unequip": "Desequipar"
|
||||
},
|
||||
"status": {
|
||||
"online": "En línea",
|
||||
"offline": "Sin conexión",
|
||||
"inProgress": "En progreso",
|
||||
"completed": "Completado",
|
||||
"locked": "Bloqueado",
|
||||
"maxLevel": "Nivel máx.",
|
||||
"ready": "¡Listo!",
|
||||
"queued": "En cola",
|
||||
"failed": "Fallido"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "Mazmorras",
|
||||
"selectPrompt": "Selecciona una mazmorra",
|
||||
"difficulty": "Dificultad",
|
||||
"minLevel": "Nivel mín.",
|
||||
"energyCost": "Costo de energía",
|
||||
"enter": "Entrar a la mazmorra",
|
||||
"easy": "Fácil",
|
||||
"medium": "Medio",
|
||||
"hard": "Difícil",
|
||||
"extreme": "Extremo",
|
||||
"legendary": "Legendario",
|
||||
"boss": "Jefe",
|
||||
"room": "Sala",
|
||||
"rooms": "Salas",
|
||||
"loot": "Botín",
|
||||
"rewardMultiplier": "Multiplicador de recompensa",
|
||||
"maxPlayers": "Jugadores máx.",
|
||||
"clearBonus": "Bonificación de limpieza"
|
||||
},
|
||||
"quests": {
|
||||
"title": "Misiones",
|
||||
"main": "Historia principal",
|
||||
"daily": "Diario",
|
||||
"weekly": "Semanal",
|
||||
"monthly": "Mensual",
|
||||
"completed": "Completado",
|
||||
"failed": "Fallido",
|
||||
"resetsIn": "Se reinicia en",
|
||||
"claim": "Reclamar recompensa",
|
||||
"objective": "Objetivo",
|
||||
"reward": "Recompensa"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "Fabricación",
|
||||
"queue": "Cola",
|
||||
"queueEmpty": "Sin objetos en cola",
|
||||
"speedUpCost": "Acelerar ({n} gemas)",
|
||||
"collectAll": "Recolectar todos",
|
||||
"completes": "Completa",
|
||||
"progress": "Progreso"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "Alianza",
|
||||
"create": "Crear alianza",
|
||||
"find": "Buscar alianza",
|
||||
"chat": "Chat de alianza",
|
||||
"research": "Investigación de alianza",
|
||||
"warehouse": "Almacén de alianza",
|
||||
"members": "Miembros",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Retirar"
|
||||
},
|
||||
"market": {
|
||||
"title": "Mercado de jugadores",
|
||||
"browse": "Explorar",
|
||||
"sell": "Vender",
|
||||
"myListings": "Mis listados",
|
||||
"priceHistory": "Historial de precios",
|
||||
"fee": "2% de tarifa de listado"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventario",
|
||||
"empty": "El inventario está vacío",
|
||||
"equip": "Equipar",
|
||||
"use": "Usar",
|
||||
"drop": "Soltar",
|
||||
"slot": {
|
||||
"helmet": "Casco",
|
||||
"body": "Armadura",
|
||||
"hands": "Guantes",
|
||||
"pants": "Piernas",
|
||||
"boots": "Botas",
|
||||
"backpack": "Mochila"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "Tienda",
|
||||
"gems": "Tienda de gemas",
|
||||
"ships": "Naves",
|
||||
"weapons": "Armas",
|
||||
"armour": "Armadura",
|
||||
"consumables": "Consumibles"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
"language": "Idioma",
|
||||
"fontSize": "Tamaño de fuente",
|
||||
"colorBlind": "Modo daltónico",
|
||||
"reducedMotion": "Movimiento reducido",
|
||||
"volume": "Volumen",
|
||||
"reset": "Restablecer ajustes"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "Ir al contenido principal",
|
||||
"closeModal": "Cerrar ventana",
|
||||
"openMenu": "Abrir menú de navegación",
|
||||
"loading": "Cargando...",
|
||||
"toastRegion": "Notificaciones"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "fr",
|
||||
"name": "Français",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Tableau de bord",
|
||||
"dungeons": "Donjons",
|
||||
"skills": "Compétences",
|
||||
"base": "Base",
|
||||
"quests": "Quêtes",
|
||||
"inventory": "Inventaire",
|
||||
"crafting": "Fabrication",
|
||||
"shop": "Boutique",
|
||||
"fleet": "Flotte",
|
||||
"galaxy": "Galaxie",
|
||||
"research": "Recherche",
|
||||
"leaderboard": "Classement",
|
||||
"missions": "Missions",
|
||||
"alliance": "Alliance",
|
||||
"market": "Marché",
|
||||
"social": "Social",
|
||||
"settings": "Paramètres"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 Bâtiments",
|
||||
"shipyard": "🚀 Chantier naval",
|
||||
"starbase": "🌌 Étoile de base",
|
||||
"overview": "📊 Vue d'ensemble"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "Métal",
|
||||
"gas": "Gaz",
|
||||
"crystal": "Cristal",
|
||||
"energyCells": "Cellules d'énergie",
|
||||
"darkMatter": "Matière noire",
|
||||
"credits": "Crédits",
|
||||
"gems": "Gemmes"
|
||||
},
|
||||
"actions": {
|
||||
"build": "Construire",
|
||||
"upgrade": "Améliorer",
|
||||
"cancel": "Annuler",
|
||||
"collect": "Collecter",
|
||||
"launch": "Lancer",
|
||||
"search": "Chercher",
|
||||
"join": "Rejoindre",
|
||||
"leave": "Quitter",
|
||||
"deposit": "Déposer",
|
||||
"withdraw": "Retirer",
|
||||
"buy": "Acheter",
|
||||
"sell": "Vendre",
|
||||
"craft": "Fabriquer",
|
||||
"research": "Rechercher",
|
||||
"repair": "Réparer",
|
||||
"equip": "Équiper",
|
||||
"save": "Sauvegarder",
|
||||
"send": "Envoyer",
|
||||
"speedUp": "Accélérer",
|
||||
"collectAll": "Tout collecter",
|
||||
"unequip": "Déséquiper"
|
||||
},
|
||||
"status": {
|
||||
"online": "En ligne",
|
||||
"offline": "Hors ligne",
|
||||
"inProgress": "En cours",
|
||||
"completed": "Terminé",
|
||||
"locked": "Verrouillé",
|
||||
"maxLevel": "Niveau max",
|
||||
"ready": "Prêt !",
|
||||
"queued": "En file",
|
||||
"failed": "Échoué"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "Donjons",
|
||||
"selectPrompt": "Sélectionner un donjon",
|
||||
"difficulty": "Difficulté",
|
||||
"minLevel": "Niveau min.",
|
||||
"energyCost": "Coût en énergie",
|
||||
"enter": "Entrer dans le donjon",
|
||||
"easy": "Facile",
|
||||
"medium": "Moyen",
|
||||
"hard": "Difficile",
|
||||
"extreme": "Extrême",
|
||||
"legendary": "Légendaire",
|
||||
"boss": "Boss",
|
||||
"room": "Salle",
|
||||
"rooms": "Salles",
|
||||
"loot": "Butin",
|
||||
"rewardMultiplier": "Multiplicateur de récompense",
|
||||
"maxPlayers": "Joueurs max",
|
||||
"clearBonus": "Bonus de victoire"
|
||||
},
|
||||
"quests": {
|
||||
"title": "Quêtes",
|
||||
"main": "Histoire principale",
|
||||
"daily": "Quotidien",
|
||||
"weekly": "Hebdomadaire",
|
||||
"monthly": "Mensuel",
|
||||
"completed": "Terminé",
|
||||
"failed": "Échoué",
|
||||
"resetsIn": "Réinitialisé dans",
|
||||
"claim": "Réclamer la récompense",
|
||||
"objective": "Objectif",
|
||||
"reward": "Récompense"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "Fabrication",
|
||||
"queue": "File d'attente",
|
||||
"queueEmpty": "Aucun objet en attente",
|
||||
"speedUpCost": "Accélérer ({n} gemmes)",
|
||||
"collectAll": "Tout collecter",
|
||||
"completes": "Terminé",
|
||||
"progress": "Progression"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "Alliance",
|
||||
"create": "Créer une alliance",
|
||||
"find": "Trouver une alliance",
|
||||
"chat": "Chat d'alliance",
|
||||
"research": "Recherche d'alliance",
|
||||
"warehouse": "Entrepôt d'alliance",
|
||||
"members": "Membres",
|
||||
"deposit": "Déposer",
|
||||
"withdraw": "Retirer"
|
||||
},
|
||||
"market": {
|
||||
"title": "Marché des joueurs",
|
||||
"browse": "Parcourir",
|
||||
"sell": "Vendre",
|
||||
"myListings": "Mes annonces",
|
||||
"priceHistory": "Historique des prix",
|
||||
"fee": "2% de frais d'inscription"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventaire",
|
||||
"empty": "L'inventaire est vide",
|
||||
"equip": "Équiper",
|
||||
"use": "Utiliser",
|
||||
"drop": "Jeter",
|
||||
"slot": {
|
||||
"helmet": "Casque",
|
||||
"body": "Armure",
|
||||
"hands": "Gants",
|
||||
"pants": "Jambes",
|
||||
"boots": "Bottes",
|
||||
"backpack": "Sac à dos"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "Boutique",
|
||||
"gems": "Boutique de gemmes",
|
||||
"ships": "Vaisseaux",
|
||||
"weapons": "Armes",
|
||||
"armour": "Armure",
|
||||
"consumables": "Consommables"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"language": "Langue",
|
||||
"fontSize": "Taille de police",
|
||||
"colorBlind": "Mode daltonien",
|
||||
"reducedMotion": "Mouvement réduit",
|
||||
"volume": "Volume sonore",
|
||||
"reset": "Rétablir les paramètres"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "Aller au contenu principal",
|
||||
"closeModal": "Fermer la fenêtre",
|
||||
"openMenu": "Ouvrir le menu de navigation",
|
||||
"loading": "Chargement...",
|
||||
"toastRegion": "Notifications"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "ja",
|
||||
"name": "日本語",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"dungeons": "ダンジョン",
|
||||
"skills": "スキル",
|
||||
"base": "基地",
|
||||
"quests": "クエスト",
|
||||
"inventory": "インベントリ",
|
||||
"crafting": "クラフト",
|
||||
"shop": "ショップ",
|
||||
"fleet": "艦隊",
|
||||
"galaxy": "銀河",
|
||||
"research": "研究",
|
||||
"leaderboard": "ランキング",
|
||||
"missions": "ミッション",
|
||||
"alliance": "アライアンス",
|
||||
"market": "マーケット",
|
||||
"social": "ソーシャル",
|
||||
"settings": "設定"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 建物",
|
||||
"shipyard": "🚀 造船所",
|
||||
"starbase": "🌌 星基地",
|
||||
"overview": "📊 概要"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "金属",
|
||||
"gas": "ガス",
|
||||
"crystal": "クリスタル",
|
||||
"energyCells": "エネルギーセル",
|
||||
"darkMatter": "ダークマター",
|
||||
"credits": "クレジット",
|
||||
"gems": "ジェム"
|
||||
},
|
||||
"actions": {
|
||||
"build": "建設",
|
||||
"upgrade": "強化",
|
||||
"cancel": "キャンセル",
|
||||
"collect": "収集",
|
||||
"launch": "起動",
|
||||
"search": "検索",
|
||||
"join": "参加",
|
||||
"leave": "退出",
|
||||
"deposit": "預け入れ",
|
||||
"withdraw": "引き出し",
|
||||
"buy": "購入",
|
||||
"sell": "売却",
|
||||
"craft": "製作",
|
||||
"research": "研究",
|
||||
"repair": "修理",
|
||||
"equip": "装備",
|
||||
"save": "保存",
|
||||
"send": "送信",
|
||||
"speedUp": "加速",
|
||||
"collectAll": "全て収集",
|
||||
"unequip": "外す"
|
||||
},
|
||||
"status": {
|
||||
"online": "オンライン",
|
||||
"offline": "オフライン",
|
||||
"inProgress": "進行中",
|
||||
"completed": "完了",
|
||||
"locked": "ロック中",
|
||||
"maxLevel": "最大レベル",
|
||||
"ready": "準備完了!",
|
||||
"queued": "待機中",
|
||||
"failed": "失敗"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "ダンジョン",
|
||||
"selectPrompt": "ダンジョンを選択",
|
||||
"difficulty": "難易度",
|
||||
"minLevel": "最低レベル",
|
||||
"energyCost": "エネルギーコスト",
|
||||
"enter": "ダンジョンに入る",
|
||||
"easy": "簡単",
|
||||
"medium": "普通",
|
||||
"hard": "難しい",
|
||||
"extreme": "極難",
|
||||
"legendary": "伝説",
|
||||
"boss": "ボス",
|
||||
"room": "部屋",
|
||||
"rooms": "部屋数",
|
||||
"loot": "戦利品",
|
||||
"rewardMultiplier": "報酬倍率",
|
||||
"maxPlayers": "最大プレイヤー数",
|
||||
"clearBonus": "クリアボーナス"
|
||||
},
|
||||
"quests": {
|
||||
"title": "クエスト",
|
||||
"main": "メインストーリー",
|
||||
"daily": "デイリー",
|
||||
"weekly": "ウィークリー",
|
||||
"monthly": "マンスリー",
|
||||
"completed": "完了",
|
||||
"failed": "失敗",
|
||||
"resetsIn": "リセットまで",
|
||||
"claim": "報酬を受け取る",
|
||||
"objective": "目標",
|
||||
"reward": "報酬"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "クラフト",
|
||||
"queue": "キュー",
|
||||
"queueEmpty": "キューにアイテムがありません",
|
||||
"speedUpCost": "加速({n}ジェム)",
|
||||
"collectAll": "全て収集",
|
||||
"completes": "完了",
|
||||
"progress": "進捗"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "アライアンス",
|
||||
"create": "アライアンス作成",
|
||||
"find": "アライアンス検索",
|
||||
"chat": "アライアンスチャット",
|
||||
"research": "アライアンス研究",
|
||||
"warehouse": "アライアンス倉庫",
|
||||
"members": "メンバー",
|
||||
"deposit": "預け入れ",
|
||||
"withdraw": "引き出し"
|
||||
},
|
||||
"market": {
|
||||
"title": "プレイヤーマーケット",
|
||||
"browse": "閲覧",
|
||||
"sell": "売却",
|
||||
"myListings": "出品リスト",
|
||||
"priceHistory": "価格履歴",
|
||||
"fee": "2%出品手数料"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "インベントリ",
|
||||
"empty": "インベントリが空です",
|
||||
"equip": "装備",
|
||||
"use": "使用",
|
||||
"drop": "捨てる",
|
||||
"slot": {
|
||||
"helmet": "ヘルメット",
|
||||
"body": "ボディアーマー",
|
||||
"hands": "グローブ",
|
||||
"pants": "レッグ",
|
||||
"boots": "ブーツ",
|
||||
"backpack": "バックパック"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "ショップ",
|
||||
"gems": "ジェムストア",
|
||||
"ships": "艦船",
|
||||
"weapons": "武器",
|
||||
"armour": "防具",
|
||||
"consumables": "消耗品"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"language": "言語",
|
||||
"fontSize": "フォントサイズ",
|
||||
"colorBlind": "色覚補正モード",
|
||||
"reducedMotion": "アニメーション軽減",
|
||||
"volume": "音量",
|
||||
"reset": "デフォルトに戻す"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "メインコンテンツへスキップ",
|
||||
"closeModal": "モーダルを閉じる",
|
||||
"openMenu": "ナビゲーションメニューを開く",
|
||||
"loading": "読み込み中...",
|
||||
"toastRegion": "通知"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "ko",
|
||||
"name": "한국어",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "대시보드",
|
||||
"dungeons": "던전",
|
||||
"skills": "스킬",
|
||||
"base": "기지",
|
||||
"quests": "퀘스트",
|
||||
"inventory": "인벤토리",
|
||||
"crafting": "제작",
|
||||
"shop": "상점",
|
||||
"fleet": "함대",
|
||||
"galaxy": "은하",
|
||||
"research": "연구",
|
||||
"leaderboard": "순위",
|
||||
"missions": "임무",
|
||||
"alliance": "동맹",
|
||||
"market": "시장",
|
||||
"social": "소셜",
|
||||
"settings": "설정"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 건물",
|
||||
"shipyard": "🚀 조선소",
|
||||
"starbase": "🌌 항성기지",
|
||||
"overview": "📊 개요"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "금속",
|
||||
"gas": "가스",
|
||||
"crystal": "크리스탈",
|
||||
"energyCells": "에너지 셀",
|
||||
"darkMatter": "암흑물질",
|
||||
"credits": "크레딧",
|
||||
"gems": "젬"
|
||||
},
|
||||
"actions": {
|
||||
"build": "건설",
|
||||
"upgrade": "강화",
|
||||
"cancel": "취소",
|
||||
"collect": "수집",
|
||||
"launch": "발사",
|
||||
"search": "검색",
|
||||
"join": "참가",
|
||||
"leave": "나가기",
|
||||
"deposit": "예치",
|
||||
"withdraw": "출금",
|
||||
"buy": "구매",
|
||||
"sell": "판매",
|
||||
"craft": "제작",
|
||||
"research": "연구",
|
||||
"repair": "수리",
|
||||
"equip": "장착",
|
||||
"save": "저장",
|
||||
"send": "전송",
|
||||
"speedUp": "가속",
|
||||
"collectAll": "모두 수집",
|
||||
"unequip": "해제"
|
||||
},
|
||||
"status": {
|
||||
"online": "온라인",
|
||||
"offline": "오프라인",
|
||||
"inProgress": "진행 중",
|
||||
"completed": "완료",
|
||||
"locked": "잠금",
|
||||
"maxLevel": "최고 레벨",
|
||||
"ready": "준비 완료!",
|
||||
"queued": "대기 중",
|
||||
"failed": "실패"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "던전",
|
||||
"selectPrompt": "던전을 선택하세요",
|
||||
"difficulty": "난이도",
|
||||
"minLevel": "최소 레벨",
|
||||
"energyCost": "에너지 비용",
|
||||
"enter": "던전 입장",
|
||||
"easy": "쉬움",
|
||||
"medium": "보통",
|
||||
"hard": "어려움",
|
||||
"extreme": "극한",
|
||||
"legendary": "전설",
|
||||
"boss": "보스",
|
||||
"room": "방",
|
||||
"rooms": "방 수",
|
||||
"loot": "전리품",
|
||||
"rewardMultiplier": "보상 배율",
|
||||
"maxPlayers": "최대 플레이어",
|
||||
"clearBonus": "클리어 보너스"
|
||||
},
|
||||
"quests": {
|
||||
"title": "퀘스트",
|
||||
"main": "메인 스토리",
|
||||
"daily": "일일",
|
||||
"weekly": "주간",
|
||||
"monthly": "월간",
|
||||
"completed": "완료",
|
||||
"failed": "실패",
|
||||
"resetsIn": "초기화까지",
|
||||
"claim": "보상 받기",
|
||||
"objective": "목표",
|
||||
"reward": "보상"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "제작",
|
||||
"queue": "대기열",
|
||||
"queueEmpty": "대기열에 아이템 없음",
|
||||
"speedUpCost": "가속 ({n}젬)",
|
||||
"collectAll": "모두 수집",
|
||||
"completes": "완료",
|
||||
"progress": "진행도"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "동맹",
|
||||
"create": "동맹 창설",
|
||||
"find": "동맹 찾기",
|
||||
"chat": "동맹 채팅",
|
||||
"research": "동맹 연구",
|
||||
"warehouse": "동맹 창고",
|
||||
"members": "멤버",
|
||||
"deposit": "예치",
|
||||
"withdraw": "출금"
|
||||
},
|
||||
"market": {
|
||||
"title": "플레이어 마켓",
|
||||
"browse": "탐색",
|
||||
"sell": "판매",
|
||||
"myListings": "내 목록",
|
||||
"priceHistory": "가격 기록",
|
||||
"fee": "2% 등록 수수료"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "인벤토리",
|
||||
"empty": "인벤토리가 비어 있습니다",
|
||||
"equip": "장착",
|
||||
"use": "사용",
|
||||
"drop": "버리기",
|
||||
"slot": {
|
||||
"helmet": "투구",
|
||||
"body": "갑옷",
|
||||
"hands": "장갑",
|
||||
"pants": "하의",
|
||||
"boots": "부츠",
|
||||
"backpack": "배낭"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "상점",
|
||||
"gems": "젬 상점",
|
||||
"ships": "함선",
|
||||
"weapons": "무기",
|
||||
"armour": "방어구",
|
||||
"consumables": "소모품"
|
||||
},
|
||||
"settings": {
|
||||
"title": "설정",
|
||||
"language": "언어",
|
||||
"fontSize": "글꼴 크기",
|
||||
"colorBlind": "색맹 모드",
|
||||
"reducedMotion": "애니메이션 줄이기",
|
||||
"volume": "음량",
|
||||
"reset": "기본값으로 재설정"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "주요 콘텐츠로 건너뛰기",
|
||||
"closeModal": "모달 닫기",
|
||||
"openMenu": "내비게이션 메뉴 열기",
|
||||
"loading": "로딩 중...",
|
||||
"toastRegion": "알림"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "pt",
|
||||
"name": "Português",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Painel",
|
||||
"dungeons": "Masmorras",
|
||||
"skills": "Habilidades",
|
||||
"base": "Base",
|
||||
"quests": "Missões",
|
||||
"inventory": "Inventário",
|
||||
"crafting": "Fabricação",
|
||||
"shop": "Loja",
|
||||
"fleet": "Frota",
|
||||
"galaxy": "Galáxia",
|
||||
"research": "Pesquisa",
|
||||
"leaderboard": "Classificação",
|
||||
"missions": "Missões",
|
||||
"alliance": "Aliança",
|
||||
"market": "Mercado",
|
||||
"social": "Social",
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 Edifícios",
|
||||
"shipyard": "🚀 Estaleiro",
|
||||
"starbase": "🌌 Base estelar",
|
||||
"overview": "📊 Visão geral"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "Metal",
|
||||
"gas": "Gás",
|
||||
"crystal": "Cristal",
|
||||
"energyCells": "Células de energia",
|
||||
"darkMatter": "Matéria escura",
|
||||
"credits": "Créditos",
|
||||
"gems": "Gemas"
|
||||
},
|
||||
"actions": {
|
||||
"build": "Construir",
|
||||
"upgrade": "Melhorar",
|
||||
"cancel": "Cancelar",
|
||||
"collect": "Coletar",
|
||||
"launch": "Lançar",
|
||||
"search": "Pesquisar",
|
||||
"join": "Entrar",
|
||||
"leave": "Sair",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Sacar",
|
||||
"buy": "Comprar",
|
||||
"sell": "Vender",
|
||||
"craft": "Fabricar",
|
||||
"research": "Pesquisar",
|
||||
"repair": "Reparar",
|
||||
"equip": "Equipar",
|
||||
"save": "Salvar",
|
||||
"send": "Enviar",
|
||||
"speedUp": "Acelerar",
|
||||
"collectAll": "Coletar tudo",
|
||||
"unequip": "Desequipar"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"inProgress": "Em andamento",
|
||||
"completed": "Concluído",
|
||||
"locked": "Bloqueado",
|
||||
"maxLevel": "Nível máximo",
|
||||
"ready": "Pronto!",
|
||||
"queued": "Na fila",
|
||||
"failed": "Falhou"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "Masmorras",
|
||||
"selectPrompt": "Selecione uma masmorra",
|
||||
"difficulty": "Dificuldade",
|
||||
"minLevel": "Nível mín.",
|
||||
"energyCost": "Custo de energia",
|
||||
"enter": "Entrar na masmorra",
|
||||
"easy": "Fácil",
|
||||
"medium": "Médio",
|
||||
"hard": "Difícil",
|
||||
"extreme": "Extremo",
|
||||
"legendary": "Lendário",
|
||||
"boss": "Chefe",
|
||||
"room": "Sala",
|
||||
"rooms": "Salas",
|
||||
"loot": "Saque",
|
||||
"rewardMultiplier": "Multiplicador de recompensa",
|
||||
"maxPlayers": "Máx. jogadores",
|
||||
"clearBonus": "Bônus de limpeza"
|
||||
},
|
||||
"quests": {
|
||||
"title": "Missões",
|
||||
"main": "História principal",
|
||||
"daily": "Diário",
|
||||
"weekly": "Semanal",
|
||||
"monthly": "Mensal",
|
||||
"completed": "Concluído",
|
||||
"failed": "Falhou",
|
||||
"resetsIn": "Reinicia em",
|
||||
"claim": "Reivindicar recompensa",
|
||||
"objective": "Objetivo",
|
||||
"reward": "Recompensa"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "Fabricação",
|
||||
"queue": "Fila",
|
||||
"queueEmpty": "Sem itens na fila",
|
||||
"speedUpCost": "Acelerar ({n} gemas)",
|
||||
"collectAll": "Coletar todos",
|
||||
"completes": "Conclui",
|
||||
"progress": "Progresso"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "Aliança",
|
||||
"create": "Criar aliança",
|
||||
"find": "Encontrar aliança",
|
||||
"chat": "Chat da aliança",
|
||||
"research": "Pesquisa da aliança",
|
||||
"warehouse": "Armazém da aliança",
|
||||
"members": "Membros",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Sacar"
|
||||
},
|
||||
"market": {
|
||||
"title": "Mercado dos jogadores",
|
||||
"browse": "Explorar",
|
||||
"sell": "Vender",
|
||||
"myListings": "Meus anúncios",
|
||||
"priceHistory": "Histórico de preços",
|
||||
"fee": "2% de taxa de listagem"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventário",
|
||||
"empty": "Inventário vazio",
|
||||
"equip": "Equipar",
|
||||
"use": "Usar",
|
||||
"drop": "Soltar",
|
||||
"slot": {
|
||||
"helmet": "Capacete",
|
||||
"body": "Armadura",
|
||||
"hands": "Luvas",
|
||||
"pants": "Calças",
|
||||
"boots": "Botas",
|
||||
"backpack": "Mochila"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "Loja",
|
||||
"gems": "Loja de gemas",
|
||||
"ships": "Naves",
|
||||
"weapons": "Armas",
|
||||
"armour": "Armadura",
|
||||
"consumables": "Consumíveis"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"language": "Idioma",
|
||||
"fontSize": "Tamanho da fonte",
|
||||
"colorBlind": "Modo daltônico",
|
||||
"reducedMotion": "Movimento reduzido",
|
||||
"volume": "Volume",
|
||||
"reset": "Redefinir para padrão"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "Ir para o conteúdo principal",
|
||||
"closeModal": "Fechar janela",
|
||||
"openMenu": "Abrir menu de navegação",
|
||||
"loading": "Carregando...",
|
||||
"toastRegion": "Notificações"
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"lang": "zh",
|
||||
"name": "中文",
|
||||
"version": "1.2"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表板",
|
||||
"dungeons": "地牢",
|
||||
"skills": "技能",
|
||||
"base": "基地",
|
||||
"quests": "任务",
|
||||
"inventory": "背包",
|
||||
"crafting": "制作",
|
||||
"shop": "商店",
|
||||
"fleet": "舰队",
|
||||
"galaxy": "银河",
|
||||
"research": "研究",
|
||||
"leaderboard": "排行榜",
|
||||
"missions": "任务",
|
||||
"alliance": "联盟",
|
||||
"market": "市场",
|
||||
"social": "社交",
|
||||
"settings": "设置"
|
||||
},
|
||||
"base": {
|
||||
"buildings": "🏗 建筑",
|
||||
"shipyard": "🚀 造船厂",
|
||||
"starbase": "🌌 星际基地",
|
||||
"overview": "📊 概览"
|
||||
},
|
||||
"resources": {
|
||||
"metal": "金属",
|
||||
"gas": "气体",
|
||||
"crystal": "晶体",
|
||||
"energyCells": "能量电池",
|
||||
"darkMatter": "暗物质",
|
||||
"credits": "积分",
|
||||
"gems": "宝石"
|
||||
},
|
||||
"actions": {
|
||||
"build": "建造",
|
||||
"upgrade": "升级",
|
||||
"cancel": "取消",
|
||||
"collect": "收集",
|
||||
"launch": "发射",
|
||||
"search": "搜索",
|
||||
"join": "加入",
|
||||
"leave": "离开",
|
||||
"deposit": "存入",
|
||||
"withdraw": "取出",
|
||||
"buy": "购买",
|
||||
"sell": "出售",
|
||||
"craft": "制作",
|
||||
"research": "研究",
|
||||
"repair": "修理",
|
||||
"equip": "装备",
|
||||
"save": "保存",
|
||||
"send": "发送",
|
||||
"speedUp": "加速",
|
||||
"collectAll": "全部收集",
|
||||
"unequip": "卸下"
|
||||
},
|
||||
"status": {
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"inProgress": "进行中",
|
||||
"completed": "已完成",
|
||||
"locked": "已锁定",
|
||||
"maxLevel": "最高等级",
|
||||
"ready": "就绪!",
|
||||
"queued": "排队中",
|
||||
"failed": "失败"
|
||||
},
|
||||
"dungeons": {
|
||||
"title": "地牢",
|
||||
"selectPrompt": "选择一个地牢",
|
||||
"difficulty": "难度",
|
||||
"minLevel": "最低等级",
|
||||
"energyCost": "能量消耗",
|
||||
"enter": "进入地牢",
|
||||
"easy": "简单",
|
||||
"medium": "普通",
|
||||
"hard": "困难",
|
||||
"extreme": "极限",
|
||||
"legendary": "传奇",
|
||||
"boss": "首领",
|
||||
"room": "房间",
|
||||
"rooms": "房间数",
|
||||
"loot": "战利品",
|
||||
"rewardMultiplier": "奖励倍数",
|
||||
"maxPlayers": "最多玩家",
|
||||
"clearBonus": "清关奖励"
|
||||
},
|
||||
"quests": {
|
||||
"title": "任务",
|
||||
"main": "主线故事",
|
||||
"daily": "每日",
|
||||
"weekly": "每周",
|
||||
"monthly": "每月",
|
||||
"completed": "已完成",
|
||||
"failed": "失败",
|
||||
"resetsIn": "重置于",
|
||||
"claim": "领取奖励",
|
||||
"objective": "目标",
|
||||
"reward": "奖励"
|
||||
},
|
||||
"crafting": {
|
||||
"title": "制作",
|
||||
"queue": "队列",
|
||||
"queueEmpty": "队列中没有物品",
|
||||
"speedUpCost": "加速({n}宝石)",
|
||||
"collectAll": "全部收集",
|
||||
"completes": "完成",
|
||||
"progress": "进度"
|
||||
},
|
||||
"alliance": {
|
||||
"title": "联盟",
|
||||
"create": "创建联盟",
|
||||
"find": "查找联盟",
|
||||
"chat": "联盟聊天",
|
||||
"research": "联盟研究",
|
||||
"warehouse": "联盟仓库",
|
||||
"members": "成员",
|
||||
"deposit": "存入",
|
||||
"withdraw": "取出"
|
||||
},
|
||||
"market": {
|
||||
"title": "玩家市场",
|
||||
"browse": "浏览",
|
||||
"sell": "出售",
|
||||
"myListings": "我的列表",
|
||||
"priceHistory": "价格历史",
|
||||
"fee": "2%上架费"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "背包",
|
||||
"empty": "背包是空的",
|
||||
"equip": "装备",
|
||||
"use": "使用",
|
||||
"drop": "丢弃",
|
||||
"slot": {
|
||||
"helmet": "头盔",
|
||||
"body": "胸甲",
|
||||
"hands": "手套",
|
||||
"pants": "腿甲",
|
||||
"boots": "靴子",
|
||||
"backpack": "背包"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"title": "商店",
|
||||
"gems": "宝石商店",
|
||||
"ships": "飞船",
|
||||
"weapons": "武器",
|
||||
"armour": "护甲",
|
||||
"consumables": "消耗品"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"language": "语言",
|
||||
"fontSize": "字体大小",
|
||||
"colorBlind": "色盲模式",
|
||||
"reducedMotion": "减少动画",
|
||||
"volume": "音量",
|
||||
"reset": "恢复默认"
|
||||
},
|
||||
"a11y": {
|
||||
"skipToContent": "跳至主内容",
|
||||
"closeModal": "关闭窗口",
|
||||
"openMenu": "打开导航菜单",
|
||||
"loading": "加载中...",
|
||||
"toastRegion": "通知"
|
||||
}
|
||||
}
|
||||
4688
Client/package-lock.json
generated
4688
Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,131 +1,29 @@
|
||||
{
|
||||
"name": "galaxystrikeonline",
|
||||
"version": "1.0.0",
|
||||
"description": "Galaxy Strike Online - Space Idle MMORPG",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Korvarix Studios",
|
||||
"email": "contact@korvarixstudios.com"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"main": "electron-main.js",
|
||||
"homepage": "./",
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"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"
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [
|
||||
"game",
|
||||
"space",
|
||||
"mmorpg",
|
||||
"idle",
|
||||
"electron"
|
||||
],
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"express": "^4.22.1",
|
||||
"socket.io": "^4.8.3"
|
||||
"axios": "^1.13.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"socket.io-client": "^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",
|
||||
"maintainer": "Korvarix Studios <contact@korvarixstudios.com>"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
}
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,216 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Component Styles (mobile-first)
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
padding:.6rem 1.1rem;border:none;border-radius:8px;
|
||||
font-family:'Space Mono',monospace;font-weight:700;cursor:pointer;
|
||||
transition:all .25s;text-transform:uppercase;letter-spacing:1px;
|
||||
position:relative;overflow:hidden;font-size:.82rem;display:inline-flex;align-items:center;gap:.35rem;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);transition:left .5s}
|
||||
.btn:hover::before{left:100%}
|
||||
.btn-primary{background:var(--gradient-primary);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,212,255,.3)}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 5px 18px rgba(0,212,255,.4)}
|
||||
.btn-secondary{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color)}
|
||||
.btn-secondary:hover{border-color:var(--primary-color);background:var(--hover-bg)}
|
||||
.btn-success{background:linear-gradient(135deg,#00ff88,#00cc66);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,255,136,.3)}
|
||||
.btn-warning{background:linear-gradient(135deg,#ffaa00,#ff8800);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,170,0,.3)}
|
||||
.btn-danger,.btn-error{background:linear-gradient(135deg,#ff3366,#ff0033);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,51,102,.3)}
|
||||
.btn-info{background:linear-gradient(135deg,#4fc3f7,#0288d1);color:var(--bg-primary)}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none!important}
|
||||
/* Larger tap targets on touch */
|
||||
@media(pointer:coarse){.btn{min-height:42px;padding:.6rem 1.2rem}}
|
||||
|
||||
/* ── Health / Progress bars ──────────────────────────────────────────── */
|
||||
.health-bar{margin:.75rem 0;padding:.6rem;border-radius:8px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08)}
|
||||
.health-label{font-size:.82rem;font-weight:600;margin-bottom:.4rem;text-transform:uppercase;letter-spacing:1px}
|
||||
.ship-health .health-label{color:#4a9eff}
|
||||
.player-health .health-label{color:#4ade80}
|
||||
.ship-health-fill{background:linear-gradient(90deg,#4a9eff,#00d4ff);border-radius:4px;transition:width .3s}
|
||||
.player-health-fill{background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:4px;transition:width .3s}
|
||||
.health-bar span{display:block;text-align:center;margin-top:.4rem;font-size:.8rem;color:rgba(255,255,255,.75)}
|
||||
.progress-bar{width:100%;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;background:var(--gradient-primary);border-radius:4px;transition:width .3s;position:relative}
|
||||
.progress-fill::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:progressShine 2s infinite}
|
||||
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
|
||||
/* ── Ship stats ──────────────────────────────────────────────────────── */
|
||||
.current-ship-stats{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:.75rem;margin-bottom:.75rem}
|
||||
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:.4rem 0;border-bottom:1px solid rgba(255,255,255,.05)}
|
||||
.stat-row:last-child{border-bottom:none}
|
||||
.stat-row .stat-label{color:#4a9eff;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:.8rem}
|
||||
.stat-row .stat-value{color:#fff;font-weight:700;font-size:.85rem}
|
||||
|
||||
/* ── Modals ──────────────────────────────────────────────────────────── */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:flex-end;justify-content:center;z-index:1000;backdrop-filter:blur(4px)}
|
||||
.modal{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px 16px 0 0;width:100%;max-height:90dvh;overflow-y:auto;box-shadow:0 -10px 40px rgba(0,0,0,.5);animation:modalSlideUp .3s ease}
|
||||
@keyframes modalSlideUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
|
||||
.modal-header{padding:1rem 1.25rem;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center}
|
||||
.modal-header h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;font-size:.95rem}
|
||||
.modal-close{background:transparent;border:none;color:var(--text-secondary);font-size:1.4rem;cursor:pointer;padding:.4rem;transition:all .25s;border-radius:6px;-webkit-tap-highlight-color:transparent}
|
||||
.modal-close:hover{color:var(--error-color);transform:rotate(90deg)}
|
||||
.modal-body{padding:1rem 1.25rem}
|
||||
|
||||
/* Alert modal */
|
||||
.alert-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
|
||||
.alert-modal .alert-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
|
||||
.alert-modal .modal-footer{padding:0 1.25rem 1.25rem;text-align:center}
|
||||
.alert-modal .btn-alert{background:var(--gradient-primary);color:#fff;border:none;padding:.65rem 1.75rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.alert-modal.success .modal-header h3{color:var(--success-color)} .alert-modal.success .btn-alert{background:var(--success-color)}
|
||||
.alert-modal.error .modal-header h3{color:var(--error-color)} .alert-modal.error .btn-alert{background:var(--error-color)}
|
||||
.alert-modal.warning .modal-header h3{color:var(--warning-color)} .alert-modal.warning .btn-alert{background:var(--warning-color)}
|
||||
|
||||
/* Confirmation modal */
|
||||
.confirmation-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
|
||||
.confirmation-modal .confirm-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
|
||||
.confirmation-modal .modal-footer{padding:0 1.25rem 1.25rem;display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
|
||||
.confirmation-modal .btn-confirm{background:var(--error-color);color:#fff;border:none;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.confirmation-modal .btn-cancel{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.confirmation-modal .btn-cancel:hover{background:var(--hover-bg);border-color:var(--primary-color)}
|
||||
|
||||
/* Settings */
|
||||
.settings-menu{max-width:600px;margin:0 auto}
|
||||
.settings-section{margin-bottom:1.5rem;padding:1.1rem;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)}
|
||||
.settings-section h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:.75rem;font-size:1rem}
|
||||
.settings-section h4{color:var(--text-primary);font-family:'Orbitron',sans-serif;font-weight:600;margin-bottom:.4rem;font-size:.9rem}
|
||||
.settings-section p{color:var(--text-secondary);margin-bottom:.75rem;line-height:1.5;font-size:.88rem}
|
||||
.setting-group{margin-bottom:1.25rem}
|
||||
.setting-group label{display:block;color:var(--text-primary);font-weight:600;margin-bottom:.4rem;font-size:.88rem}
|
||||
.setting-select{width:100%;padding:.65rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;color:var(--text-primary);font-size:.88rem;cursor:pointer;transition:all .3s;-webkit-appearance:none;appearance:none}
|
||||
.setting-select:hover{border-color:var(--primary-color)}
|
||||
.setting-select:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(74,158,255,.2)}
|
||||
.setting-actions{display:flex;gap:.75rem;justify-content:flex-end;margin-top:.75rem;flex-wrap:wrap}
|
||||
.reset-options{display:flex;flex-direction:column;gap:.75rem}
|
||||
.reset-option{padding:.85rem;background:var(--bg-secondary);border-radius:8px;border:1px solid var(--border-color)}
|
||||
.reset-option h4{color:var(--warning-color);margin-bottom:.4rem}
|
||||
.reset-option ul{margin:.4rem 0;padding-left:1.25rem;color:var(--text-secondary);font-size:.82rem}
|
||||
.reset-option li{margin-bottom:.2rem}
|
||||
|
||||
/* ── Tooltips (desktop only — use title attr on mobile) ──────────────── */
|
||||
@media(min-width:1024px){
|
||||
.tooltip{position:relative;cursor:help}
|
||||
.tooltip::before{content:attr(data-tooltip);position:absolute;bottom:125%;left:50%;transform:translateX(-50%);background:var(--bg-tertiary);color:var(--text-primary);padding:.4rem .85rem;border-radius:6px;border:1px solid var(--border-color);font-size:.78rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .25s;z-index:1000}
|
||||
.tooltip::after{content:'';position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--border-color);opacity:0;pointer-events:none;transition:opacity .25s}
|
||||
.tooltip:hover::before,.tooltip:hover::after{opacity:1}
|
||||
}
|
||||
|
||||
/* ── Notifications ───────────────────────────────────────────────────── */
|
||||
.notification{
|
||||
position:fixed;top:12px;right:12px;left:12px;
|
||||
padding:.85rem 1.1rem;border-radius:10px;border:1px solid var(--border-color);
|
||||
background:var(--bg-secondary);color:var(--text-primary);
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:2000;
|
||||
animation:notifSlideIn .3s ease;font-size:.88rem;
|
||||
max-width:420px;margin:0 auto;
|
||||
}
|
||||
@keyframes notifSlideIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}
|
||||
.notification.success{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.12),rgba(0,204,102,.08))}
|
||||
.notification.warning{border-color:var(--warning-color);background:linear-gradient(135deg,rgba(255,170,0,.12),rgba(255,136,0,.08))}
|
||||
.notification.error {border-color:var(--error-color); background:linear-gradient(135deg,rgba(255,51,102,.12),rgba(255,0,51,.08))}
|
||||
.notification.info {border-color:var(--primary-color);background:linear-gradient(135deg,rgba(0,212,255,.12),rgba(0,153,204,.08))}
|
||||
@media(min-width:640px){
|
||||
.notification{left:auto;right:16px;top:16px;width:300px;margin:0}
|
||||
@keyframes notifSlideIn{from{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}
|
||||
}
|
||||
|
||||
/* ── Inventory slots & item cards ────────────────────────────────────── */
|
||||
.inventory-slot.starbase-bonus-slot::before{content:'+';position:absolute;top:2px;right:2px;background:var(--primary-color);color:#fff;width:12px;height:12px;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center;font-weight:bold}
|
||||
.item-card{width:100%;height:100%;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.4rem;cursor:pointer;transition:all .25s;position:relative;overflow:hidden;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.item-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--gradient-primary);transform:scaleX(0);transition:transform .3s}
|
||||
.item-card:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.item-card:hover::before{transform:scaleX(1)}
|
||||
.item-card.rare{border-color:#0088ff;box-shadow:0 0 8px rgba(0,136,255,.25)}
|
||||
.item-card.epic{border-color:#8833ff;box-shadow:0 0 10px rgba(136,51,255,.25)}
|
||||
.item-card.legendary{border-color:#ff8800;box-shadow:0 0 14px rgba(255,136,0,.25)}
|
||||
.item-icon{flex:1;display:flex;align-items:center;justify-content:center;margin-bottom:.35rem}
|
||||
.item-icon img,.item-icon i{width:50%;height:50%;min-width:50px;min-height:50px;max-width:120px;max-height:120px;object-fit:contain}
|
||||
.item-info{text-align:center;font-size:clamp(.58rem,1.4vw,.82rem);line-height:1.1}
|
||||
.item-name{font-weight:600;margin-bottom:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
|
||||
.item-rarity{font-size:clamp(.52rem,1.1vw,.7rem);opacity:.8}
|
||||
.item-quantity{position:absolute;top:3px;right:3px;background:var(--primary-color);color:#fff;font-size:clamp(.5rem,1.1vw,.68rem);font-weight:bold;padding:2px 5px;border-radius:5px;min-width:18px;text-align:center}
|
||||
|
||||
/* ── Skill items ─────────────────────────────────────────────────────── */
|
||||
.skill-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;cursor:pointer;transition:all .25s}
|
||||
.skill-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.skill-item.locked{opacity:.5;cursor:not-allowed}
|
||||
.skill-item.locked:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
|
||||
.skill-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem}
|
||||
.skill-name{font-weight:700;color:var(--text-primary);font-size:.9rem}
|
||||
.skill-level{color:var(--primary-color);font-weight:700;font-size:.9rem}
|
||||
.skill-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.skill-progress{margin-top:.4rem}
|
||||
|
||||
/* ── Quest items ─────────────────────────────────────────────────────── */
|
||||
.quest-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.9rem;cursor:pointer;transition:all .25s}
|
||||
.quest-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.quest-item.completed{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.08),rgba(0,204,102,.06))}
|
||||
.quest-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;gap:.3rem}
|
||||
.quest-title{font-weight:700;color:var(--text-primary);font-size:.9rem}
|
||||
.quest-reward{color:var(--warning-color);font-weight:700;font-size:.9rem}
|
||||
.quest-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.quest-progress{margin-top:.4rem}
|
||||
|
||||
/* ── Dungeon items ───────────────────────────────────────────────────── */
|
||||
.dungeon-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
|
||||
.dungeon-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.dungeon-item.selected{border-color:var(--primary-color);background:rgba(0,212,255,.15)}
|
||||
.dungeon-name{font-weight:700;color:var(--text-primary);margin-bottom:.2rem;font-size:.9rem}
|
||||
.dungeon-difficulty{font-size:.78rem;margin-bottom:.2rem}
|
||||
.dungeon-difficulty.easy{color:var(--success-color)} .dungeon-difficulty.medium{color:var(--warning-color)} .dungeon-difficulty.hard{color:var(--error-color)}
|
||||
.dungeon-rewards{font-size:.78rem;color:var(--text-secondary)}
|
||||
|
||||
/* Collapsible dungeon sections */
|
||||
.dungeon-section{margin-bottom:.75rem;border:1px solid var(--border-color);border-radius:8px;overflow:hidden;background:var(--bg-secondary)}
|
||||
.difficulty-header.collapsible{padding:.8rem 1rem;cursor:pointer;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;margin:0;border:none;border-radius:0}
|
||||
.difficulty-header.collapsible:hover{background:var(--bg-tertiary)}
|
||||
.header-content{display:flex;align-items:center;gap:.4rem}
|
||||
.header-content i{font-size:1.1rem}
|
||||
.header-content span{font-weight:600;font-size:1rem}
|
||||
.dungeon-count{background:rgba(255,255,255,.18);padding:.15rem .45rem;border-radius:10px;font-size:.75rem;font-weight:600;min-width:1.8rem;text-align:center}
|
||||
.collapse-indicator{transition:transform .3s;background:rgba(255,255,255,.08);width:1.8rem;height:1.8rem;border-radius:50%;display:flex;align-items:center;justify-content:center}
|
||||
.collapse-indicator i{font-size:.82rem;color:rgba(255,255,255,.85)}
|
||||
.dungeon-content{padding:0 .75rem .75rem;max-height:2000px;overflow:hidden;transition:all .3s;opacity:1}
|
||||
.dungeon-content.collapsed{max-height:0;padding:0 .75rem;opacity:0}
|
||||
.difficulty-header.tutorial{background:linear-gradient(135deg,#1a56db,#2c5aa0);color:#fff}
|
||||
.difficulty-header.easy{background:linear-gradient(135deg,var(--success-color),#27ae60);color:#fff}
|
||||
.difficulty-header.medium{background:linear-gradient(135deg,var(--warning-color),#f39c12);color:#fff}
|
||||
.difficulty-header.hard{background:linear-gradient(135deg,var(--error-color),#e74c3c);color:#fff}
|
||||
.difficulty-header.extreme{background:linear-gradient(135deg,#8e44ad,#9b59b6);color:#fff}
|
||||
.dungeon-content .dungeon-item{margin-bottom:.6rem;border-left:4px solid transparent}
|
||||
.dungeon-content .dungeon-item:hover{border-left-color:var(--primary-color);transform:translateX(3px)}
|
||||
.dungeon-content .dungeon-item:last-child{margin-bottom:0}
|
||||
|
||||
/* ── Shop items (legacy card style) ─────────────────────────────────── */
|
||||
.shop-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .25s}
|
||||
.shop-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.shop-item.purchased{opacity:.5;cursor:not-allowed}
|
||||
.shop-item.purchased:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
|
||||
.shop-name{font-weight:700;color:var(--text-primary);margin-bottom:.4rem;font-size:.9rem}
|
||||
.shop-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.shop-price{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.3rem}
|
||||
.shop-cost{color:var(--warning-color);font-weight:700;font-size:.9rem}
|
||||
|
||||
/* ── Loading states ──────────────────────────────────────────────────── */
|
||||
.loading{position:relative;overflow:hidden}
|
||||
.loading::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,212,255,.18),transparent);animation:loadingShine 1.5s infinite}
|
||||
@keyframes loadingShine{0%{left:-100%}100%{left:100%}}
|
||||
|
||||
/* ── Misc animations ─────────────────────────────────────────────────── */
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
||||
@keyframes rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
|
||||
@keyframes glow{0%,100%{box-shadow:0 0 5px rgba(0,212,255,.5)}50%{box-shadow:0 0 18px rgba(0,212,255,.8)}}
|
||||
.pulse{animation:pulse 2s infinite}
|
||||
.bounce{animation:bounce 2s infinite}
|
||||
.rotate{animation:rotate 2s linear infinite}
|
||||
.glow{animation:glow 2s infinite}
|
||||
|
||||
/* ── Desktop modal — centred (override bottom-sheet) ────────────────── */
|
||||
@media(min-width:640px){
|
||||
.modal-overlay{align-items:center}
|
||||
.modal{border-radius:14px;max-width:580px;width:90%;max-height:80dvh}
|
||||
@keyframes modalSlideUp{from{opacity:0;transform:translateY(-30px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
}
|
||||
@ -1,606 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Main Styles
|
||||
Mobile-first. Scales up to tablet (≥640px) and desktop (≥1024px).
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Reset ───────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--primary-color: #00d4ff;
|
||||
--secondary-color: #ff6b35;
|
||||
--accent-color: #ff00ff;
|
||||
--bg-primary: #0a0e1a;
|
||||
--bg-secondary: #151923;
|
||||
--bg-tertiary: #1e2433;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b8c5d6;
|
||||
--text-muted: #6b7c93;
|
||||
--border-color: #2a3241;
|
||||
--success-color: #00ff88;
|
||||
--warning-color: #ffaa00;
|
||||
--error-color: #ff3366;
|
||||
--card-bg: rgba(30,36,51,0.8);
|
||||
--hover-bg: rgba(0,212,255,0.1);
|
||||
--gradient-primary: linear-gradient(135deg,#00d4ff,#0099cc);
|
||||
--gradient-secondary: linear-gradient(135deg,#ff6b35,#ff4500);
|
||||
/* Layout tokens */
|
||||
--header-h: 52px;
|
||||
--nav-h: 56px;
|
||||
--pg: 0.75rem;
|
||||
--r: 10px;
|
||||
}
|
||||
|
||||
html { font-size: 14px; -webkit-text-size-adjust: 100%; }
|
||||
body {
|
||||
font-family: 'Space Mono', monospace;
|
||||
background: var(--bg-primary); color: var(--text-primary);
|
||||
overflow: hidden; height: 100dvh;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%,rgba(0,212,255,.08) 0%,transparent 50%),
|
||||
radial-gradient(circle at 80% 80%,rgba(255,107,53,.08) 0%,transparent 50%),
|
||||
radial-gradient(circle at 40% 20%,rgba(255,0,255,.04) 0%,transparent 50%);
|
||||
}
|
||||
h1,h2,h3,.logo,.section-title,.menu-title { font-family:'Orbitron',sans-serif; }
|
||||
.hidden { display:none!important; }
|
||||
.text-center{text-align:center} .w-full{width:100%}
|
||||
.flex{display:flex} .flex-column{flex-direction:column}
|
||||
.flex-center{align-items:center;justify-content:center}
|
||||
.flex-between{justify-content:space-between} .flex-wrap{flex-wrap:wrap}
|
||||
.mt-1{margin-top:.5rem} .mt-2{margin-top:1rem}
|
||||
.mb-1{margin-bottom:.5rem} .mb-2{margin-bottom:1rem}
|
||||
.p-1{padding:.5rem} .p-2{padding:1rem}
|
||||
|
||||
/* ── Scrollbars ──────────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar{width:5px;height:5px}
|
||||
::-webkit-scrollbar-track{background:var(--bg-tertiary);border-radius:3px}
|
||||
::-webkit-scrollbar-thumb{background:rgba(0,212,255,.35);border-radius:3px}
|
||||
::-webkit-scrollbar-thumb:hover{background:rgba(0,212,255,.6)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
LOADING SCREEN
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.loading-screen {
|
||||
position:fixed;inset:0;background:var(--bg-primary);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
z-index:9999;transition:opacity .5s ease;
|
||||
}
|
||||
.loading-content{text-align:center;width:90%;max-width:340px;padding:2rem}
|
||||
.game-title {
|
||||
font-family:'Orbitron',sans-serif;
|
||||
font-size:clamp(1.6rem,8vw,3rem);font-weight:900;
|
||||
background:var(--gradient-primary);
|
||||
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
|
||||
margin-bottom:2rem;text-transform:uppercase;letter-spacing:3px;
|
||||
}
|
||||
.loading-bar{width:100%;height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden;margin-bottom:1rem}
|
||||
.loading-progress{height:100%;background:var(--gradient-primary);width:0%;transition:width .3s ease;animation:loading-pulse 2s infinite}
|
||||
@keyframes loading-pulse{0%,100%{opacity:1}50%{opacity:.7}}
|
||||
.loading-text{color:var(--text-secondary);font-size:.8rem}
|
||||
.loading-indicator {
|
||||
position:fixed;top:0;left:0;width:100%;height:3px;
|
||||
background:linear-gradient(90deg,var(--primary-color) 0%,rgba(0,212,255,.3) 50%,var(--primary-color) 100%);
|
||||
background-size:200% 100%;animation:loading-gradient 2s ease-in-out infinite;
|
||||
z-index:10000;transition:opacity .3s ease;
|
||||
}
|
||||
.loading-indicator.hidden{opacity:0;pointer-events:none}
|
||||
.loading-indicator.complete{background:linear-gradient(90deg,#4CAF50,#45a049);animation:none}
|
||||
.loading-indicator.error{background:linear-gradient(90deg,#f44336,#d32f2f);animation:none}
|
||||
@keyframes loading-gradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
||||
.loading-status {
|
||||
position:fixed;top:8px;left:50%;transform:translateX(-50%);
|
||||
background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;
|
||||
padding:6px 14px;color:var(--text-primary);font-size:.82rem;z-index:10001;
|
||||
transition:all .3s ease;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
||||
}
|
||||
.loading-status.error{background:linear-gradient(90deg,#f44336,#d32f2f);color:#fff;border-color:#d32f2f}
|
||||
.loading-status.hidden{opacity:0;transform:translateX(-50%) translateY(-20px)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ELECTRON TITLE BAR
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.title-bar {
|
||||
position:fixed;top:0;left:0;right:0;height:32px;
|
||||
background:var(--bg-primary);border-bottom:1px solid var(--border-color);
|
||||
display:none;justify-content:space-between;align-items:center;padding:0 8px;
|
||||
z-index:10000;-webkit-app-region:drag;user-select:none;
|
||||
}
|
||||
body.electron-app .title-bar{display:flex}
|
||||
body.electron-app #app{margin-top:32px}
|
||||
.title-bar-title{font-size:13px;font-weight:600;color:var(--text-primary);font-family:'Orbitron',monospace}
|
||||
.title-bar-right{display:flex;gap:4px}
|
||||
.title-bar-btn{
|
||||
width:24px;height:24px;border:none;background:transparent;
|
||||
color:var(--text-primary);border-radius:4px;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:12px;-webkit-app-region:no-drag;transition:background .2s;
|
||||
}
|
||||
.title-bar-btn:hover{background:var(--bg-secondary)}
|
||||
.title-bar-btn.close-btn:hover{background:#e74c3c;color:#fff}
|
||||
body.fullscreen .title-bar{display:none}
|
||||
body.fullscreen #app{margin-top:0}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
MAIN MENU
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.main-menu {
|
||||
position:fixed;inset:0;background:var(--bg-primary);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow-y:auto;padding:1rem 0;
|
||||
background-image:radial-gradient(circle at 20% 50%,rgba(0,212,255,.1) 0%,transparent 50%),
|
||||
radial-gradient(circle at 80% 80%,rgba(255,107,53,.1) 0%,transparent 50%);
|
||||
}
|
||||
.menu-container {
|
||||
width:95%;max-width:800px;background:var(--card-bg);
|
||||
border-radius:16px;border:1px solid var(--border-color);
|
||||
box-shadow:0 20px 60px rgba(0,0,0,.5);backdrop-filter:blur(10px);overflow:hidden;margin:auto;
|
||||
}
|
||||
.menu-header{text-align:center;padding:clamp(1.5rem,5vw,2.5rem) 1rem 1.2rem;background:var(--gradient-primary)}
|
||||
.menu-title{font-size:clamp(1.4rem,6vw,3rem);font-weight:900;color:var(--text-primary);text-transform:uppercase;letter-spacing:3px;margin-bottom:.5rem;text-shadow:0 0 20px rgba(0,212,255,.5)}
|
||||
.menu-subtitle{font-size:clamp(.85rem,2.5vw,1.2rem);color:var(--text-secondary)}
|
||||
.menu-content{padding:clamp(1rem,4vw,2rem)}
|
||||
.menu-section{animation:fadeInUp .5s ease-out}
|
||||
.section-title{font-size:clamp(1.1rem,4vw,1.8rem);color:var(--primary-color);text-align:center;margin-bottom:1.5rem;text-transform:uppercase;letter-spacing:2px}
|
||||
.login-options{display:flex;flex-direction:column;gap:1rem;margin-bottom:1rem}
|
||||
.btn-large{
|
||||
padding:clamp(.9rem,3vw,1.25rem) clamp(1rem,4vw,2.5rem);
|
||||
font-size:clamp(.9rem,2.5vw,1.2rem);font-weight:600;border-radius:10px;
|
||||
transition:all .3s;width:100%;display:flex;align-items:center;justify-content:center;gap:.5rem;
|
||||
}
|
||||
.login-notice{text-align:center;padding:.75rem;background:rgba(0,212,255,.1);border:1px solid var(--primary-color);border-radius:8px;color:var(--text-secondary);font-size:.85rem}
|
||||
.login-notice i{color:var(--primary-color);margin-right:.4rem}
|
||||
|
||||
/* Server browser */
|
||||
.server-controls{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;gap:.75rem;flex-wrap:wrap}
|
||||
.server-filters{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.filter-select{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.5rem .75rem;color:var(--text-primary);font-family:'Space Mono',monospace;font-size:.85rem;cursor:pointer}
|
||||
.filter-select:focus{outline:none;border-color:var(--primary-color)}
|
||||
.server-list{max-height:min(55vh,360px);overflow-y:auto;border:1px solid var(--border-color);border-radius:10px;background:var(--bg-tertiary);margin-bottom:1rem}
|
||||
.server-item{display:flex;justify-content:space-between;align-items:center;padding:.85rem 1rem;border-bottom:1px solid var(--border-color);cursor:pointer;transition:background .2s,border-left .2s}
|
||||
.server-item:last-child{border-bottom:none}
|
||||
.server-item:hover{background:var(--hover-bg);border-left:4px solid var(--primary-color)}
|
||||
.server-name{font-size:1rem;font-weight:600;margin-bottom:3px}
|
||||
.server-details{font-size:.8rem;color:var(--text-secondary);display:flex;gap:.75rem;flex-wrap:wrap}
|
||||
.server-actions-right{display:flex;align-items:center;gap:.5rem;flex-shrink:0}
|
||||
.server-player-count,.server-region{background:var(--bg-secondary);padding:3px 8px;border-radius:4px;font-size:.75rem;color:var(--text-secondary);border:1px solid var(--border-color)}
|
||||
.server-type{padding:3px 8px;border-radius:4px;font-size:.75rem;font-weight:600;text-transform:uppercase}
|
||||
.server-type.public{background:rgba(0,255,136,.15);color:var(--success-color);border:1px solid var(--success-color)}
|
||||
.server-type.private{background:rgba(255,170,0,.15);color:var(--warning-color);border:1px solid var(--warning-color)}
|
||||
.server-loading,.server-empty{text-align:center;padding:3rem 1rem;color:var(--text-muted)}
|
||||
.server-loading i,.server-empty i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5;display:block}
|
||||
.server-loading i{animation:pulse 1.5s infinite}
|
||||
|
||||
/* Server/Save confirmation — mobile: stacked */
|
||||
.server-confirmation,.save-confirmation,.options-grid{display:flex;flex-direction:column;gap:1rem;margin-bottom:1.5rem}
|
||||
.server-preview,.save-preview,.save-info-display{background:rgba(0,212,255,.08);border:2px solid rgba(0,212,255,.3);border-radius:10px;padding:1rem;font-family:'Space Mono',monospace}
|
||||
.server-preview h3,.save-preview h3,.save-info-display h3{color:var(--primary-color);margin-bottom:.75rem;text-align:center;font-size:1rem}
|
||||
.server-details{color:var(--text-secondary);font-size:.9rem;line-height:1.6}
|
||||
.server-info{margin:6px 0;display:flex;justify-content:space-between;align-items:center}
|
||||
.server-info span{font-weight:600;color:var(--text-primary)}
|
||||
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{display:flex;flex-direction:row;gap:.75rem;flex-wrap:wrap}
|
||||
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{position:static;width:auto;flex:1;min-width:130px}
|
||||
.confirm-navigation,.options-actions,.save-actions{display:flex;justify-content:center;margin-top:1rem}
|
||||
.btn-join-server{background:linear-gradient(135deg,#00ff88,#00cc66)!important;color:#000!important;border:3px solid #00ff88!important;box-shadow:0 6px 20px rgba(0,255,136,.4)!important;font-weight:700!important}
|
||||
.btn-join-server:hover{background:linear-gradient(135deg,#00ffaa,#00ff88)!important;transform:scale(1.04) translateY(-2px);box-shadow:0 8px 25px rgba(0,255,136,.5)!important}
|
||||
.selected-server-info-center,.selected-save-info-center,.options-center{display:flex;flex-direction:column;justify-content:center;align-items:center;flex:1}
|
||||
.save-details{color:#fff;font-size:.9em;line-height:1.6;white-space:pre-wrap}
|
||||
.save-info{margin:3px 0;font-family:'Space Mono',monospace}
|
||||
#saveInfoDetails{color:#fff;font-size:.9em;line-height:1.4}
|
||||
.save-info-content{background:rgba(0,100,200,.2);border-radius:8px;padding:.75rem;margin:0}
|
||||
|
||||
/* Save slots */
|
||||
.save-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||||
.save-slot{background:var(--bg-secondary);border:2px solid var(--border-color);border-radius:10px;padding:1rem;cursor:pointer;transition:all .3s}
|
||||
.save-slot:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 8px 25px rgba(0,212,255,.2)}
|
||||
.slot-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}
|
||||
.slot-number{font-family:'Orbitron',sans-serif;font-weight:700}
|
||||
.slot-status{padding:3px 10px;border-radius:20px;font-size:.75rem;font-weight:600;text-transform:uppercase}
|
||||
.slot-status.empty{background:var(--bg-tertiary);color:var(--text-muted)}
|
||||
.slot-status.has-data{background:var(--success-color);color:var(--bg-primary)}
|
||||
.slot-name{font-weight:700;color:var(--text-primary);margin-bottom:4px}
|
||||
.slot-details{color:var(--text-muted);font-size:.9rem}
|
||||
.slot-btn{width:100%;padding:.55rem;background:var(--gradient-primary);border:none;border-radius:6px;color:var(--text-primary);font-weight:600;cursor:pointer;transition:all .3s}
|
||||
.slot-btn:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(0,212,255,.3)}
|
||||
|
||||
/* Menu footer */
|
||||
.menu-footer{padding:.9rem 1.5rem;background:var(--bg-secondary);border-top:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||
.version-text{color:var(--text-muted);font-size:.8rem}
|
||||
.footer-links{display:flex;gap:1rem}
|
||||
.link-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:.85rem;transition:color .3s}
|
||||
.link-btn:hover{color:var(--primary-color)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
GAME INTERFACE
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.game-interface{width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
|
||||
|
||||
/* Header */
|
||||
.game-header{
|
||||
height:var(--header-h);background:var(--bg-secondary);
|
||||
border-bottom:1px solid var(--border-color);
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:0 .75rem;backdrop-filter:blur(10px);flex-shrink:0;
|
||||
position:sticky;top:0;z-index:500;
|
||||
}
|
||||
.header-left{display:flex;align-items:center;gap:.6rem;min-width:0}
|
||||
.logo{font-size:clamp(1rem,4vw,1.5rem);font-weight:900;color:var(--primary-color);text-shadow:0 0 10px rgba(0,212,255,.5);white-space:nowrap}
|
||||
.player-info{display:flex;flex-direction:column;gap:.1rem;min-width:0}
|
||||
.player-info>div{display:flex;align-items:center;gap:.25rem;min-width:0}
|
||||
.player-name{font-weight:700;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:18ch}
|
||||
.player-title{font-size:.75rem;color:var(--text-secondary);display:none}
|
||||
.player-username{font-weight:600;color:var(--accent-color);font-size:.78rem}
|
||||
.player-level{font-size:.75rem;color:var(--primary-color);white-space:nowrap}
|
||||
|
||||
.header-center{flex:1;display:flex;justify-content:center;overflow:hidden;min-width:0;padding:0 .4rem}
|
||||
.resources{display:flex;gap:.35rem;align-items:center;overflow-x:auto;flex-wrap:nowrap;scrollbar-width:none;-ms-overflow-style:none;padding:.15rem 0}
|
||||
.resources::-webkit-scrollbar{display:none}
|
||||
.resource{display:flex;align-items:center;gap:.3rem;padding:.28rem .55rem;background:var(--bg-tertiary);border-radius:20px;border:1px solid var(--border-color);white-space:nowrap;font-size:.75rem;transition:border-color .2s;flex-shrink:0}
|
||||
.resource:hover{border-color:var(--primary-color)}
|
||||
.resource i{color:var(--primary-color);font-size:.78rem}
|
||||
|
||||
.header-right{display:flex;gap:.3rem;flex-shrink:0}
|
||||
.header-right .btn{padding:.4rem .55rem;font-size:.8rem;border-radius:7px}
|
||||
|
||||
/* ── TOP NAV — desktop only ──────────────────────────────────────────── */
|
||||
.main-nav{
|
||||
height:46px;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color);
|
||||
display:none;align-items:center;padding:0 .75rem;gap:.3rem;
|
||||
overflow-x:auto;position:sticky;top:var(--header-h);z-index:490;
|
||||
scrollbar-width:none;
|
||||
}
|
||||
.main-nav::-webkit-scrollbar{display:none}
|
||||
.nav-btn{
|
||||
display:flex;align-items:center;gap:.35rem;padding:.38rem .7rem;
|
||||
background:transparent;border:1px solid transparent;border-radius:7px;
|
||||
color:var(--text-secondary);cursor:pointer;transition:all .25s;
|
||||
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;
|
||||
}
|
||||
.nav-btn:hover{background:var(--hover-bg);color:var(--text-primary);border-color:var(--primary-color)}
|
||||
.nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent;font-weight:700}
|
||||
.nav-btn i{font-size:.85rem}
|
||||
|
||||
/* ── BOTTOM NAV — mobile primary navigation ──────────────────────────── */
|
||||
.bottom-nav{
|
||||
position:fixed;bottom:0;left:0;right:0;
|
||||
height:var(--nav-h);
|
||||
background:var(--bg-secondary);border-top:1px solid var(--border-color);
|
||||
display:flex;align-items:stretch;z-index:600;
|
||||
overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;
|
||||
overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px);
|
||||
}
|
||||
.bottom-nav::-webkit-scrollbar{display:none}
|
||||
.bottom-nav-btn{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:2px;padding:.28rem .4rem;background:transparent;border:none;
|
||||
color:var(--text-muted);cursor:pointer;transition:color .2s,background .2s;
|
||||
font-family:'Space Mono',monospace;font-size:.52rem;line-height:1.1;
|
||||
flex:1;min-width:48px;max-width:68px;white-space:nowrap;overflow:hidden;
|
||||
border-top:2px solid transparent;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.bottom-nav-btn i{font-size:1rem;display:block}
|
||||
.bottom-nav-btn.active{color:var(--primary-color);border-top-color:var(--primary-color);background:rgba(0,212,255,.06)}
|
||||
.bottom-nav-more{
|
||||
background:transparent;border:none;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:2px;padding:.28rem .4rem;min-width:48px;max-width:68px;flex-shrink:0;
|
||||
color:var(--text-muted);cursor:pointer;font-size:.52rem;border-top:2px solid transparent;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;font-family:'Space Mono',monospace;
|
||||
}
|
||||
.bottom-nav-more i{font-size:1rem;display:block}
|
||||
.bottom-nav-more:hover,.bottom-nav-more.open{color:var(--primary-color)}
|
||||
|
||||
/* ── NAV DRAWER ──────────────────────────────────────────────────────── */
|
||||
.nav-drawer{
|
||||
position:fixed;bottom:var(--nav-h);left:0;right:0;
|
||||
background:var(--bg-secondary);border-top:1px solid var(--border-color);
|
||||
border-radius:16px 16px 0 0;z-index:590;
|
||||
transform:translateY(100%);transition:transform .3s cubic-bezier(.4,0,.2,1);
|
||||
max-height:58dvh;overflow-y:auto;padding:.75rem 0;
|
||||
box-shadow:0 -8px 32px rgba(0,0,0,.4);
|
||||
}
|
||||
.nav-drawer.open{transform:translateY(0)}
|
||||
.nav-drawer-handle{width:36px;height:4px;background:var(--border-color);border-radius:2px;margin:0 auto .75rem;cursor:pointer}
|
||||
.nav-drawer-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.25rem;padding:0 .75rem .5rem}
|
||||
.nav-drawer-btn{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:4px;padding:.6rem .4rem;
|
||||
background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:10px;
|
||||
color:var(--text-secondary);cursor:pointer;font-size:.68rem;line-height:1.2;
|
||||
transition:all .2s;text-align:center;font-family:'Space Mono',monospace;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.nav-drawer-btn i{font-size:1.1rem;color:var(--text-muted)}
|
||||
.nav-drawer-btn:hover,.nav-drawer-btn.active{background:var(--hover-bg);border-color:var(--primary-color);color:var(--primary-color)}
|
||||
.nav-drawer-btn.active i{color:var(--primary-color)}
|
||||
.nav-drawer-overlay{position:fixed;inset:0;z-index:580;background:rgba(0,0,0,.45);display:none}
|
||||
.nav-drawer-overlay.open{display:block}
|
||||
|
||||
/* ── MAIN CONTENT ────────────────────────────────────────────────────── */
|
||||
.main-content{
|
||||
flex:1;overflow-y:auto;overflow-x:hidden;
|
||||
padding:var(--pg);background:var(--bg-primary);
|
||||
padding-bottom:calc(var(--nav-h) + var(--pg) + env(safe-area-inset-bottom,0px));
|
||||
-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;
|
||||
}
|
||||
.tab-content{display:none;animation:fadeIn .2s ease}
|
||||
.tab-content.active{display:block}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes fadeInUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* ── DASHBOARD ───────────────────────────────────────────────────────── */
|
||||
.dashboard-grid{display:grid;grid-template-columns:1fr;gap:.75rem;max-width:1200px;margin:0 auto}
|
||||
.card{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;backdrop-filter:blur(10px);transition:border-color .3s,box-shadow .3s}
|
||||
.card:hover{border-color:var(--primary-color);box-shadow:0 0 18px rgba(0,212,255,.15)}
|
||||
.card h3{color:var(--primary-color);margin-bottom:.75rem;font-family:'Orbitron',sans-serif;font-size:.88rem;font-weight:700;text-transform:uppercase;letter-spacing:1px}
|
||||
.fleet-info,.idle-stats,.quick-actions{display:flex;flex-direction:column;gap:.6rem}
|
||||
.ship-status{display:flex;align-items:center;gap:.75rem}
|
||||
.ship-status i{font-size:1.6rem;color:var(--secondary-color)}
|
||||
.player-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:.35rem}
|
||||
.stat{display:flex;justify-content:space-between;padding:.35rem 0;border-bottom:1px solid var(--border-color);font-size:.8rem}
|
||||
.stat:last-child{border-bottom:none}
|
||||
.stat-label{color:var(--text-secondary)} .stat-value{color:var(--primary-color);font-weight:700}
|
||||
|
||||
/* ── DUNGEONS ────────────────────────────────────────────────────────── */
|
||||
.dungeons-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.dungeon-selector{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
|
||||
.dungeon-list{display:flex;flex-direction:column;gap:.4rem}
|
||||
.dungeon-view{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;min-height:180px;display:flex;align-items:center;justify-content:center}
|
||||
.dungeon-placeholder{text-align:center;color:var(--text-muted)}
|
||||
.dungeon-placeholder i{font-size:2.8rem;margin-bottom:.75rem;opacity:.5;display:block}
|
||||
|
||||
/* ── SKILLS ──────────────────────────────────────────────────────────── */
|
||||
.skills-container{max-width:1200px;margin:0 auto}
|
||||
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{
|
||||
display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap;overflow-x:auto;
|
||||
scrollbar-width:none;padding-bottom:.2rem;
|
||||
}
|
||||
.skill-categories::-webkit-scrollbar,.quest-tabs::-webkit-scrollbar,.crafting-categories::-webkit-scrollbar,.shop-categories::-webkit-scrollbar{display:none}
|
||||
.skill-cat-btn,.quest-tab-btn,.crafting-cat-btn,.shop-cat-btn{
|
||||
padding:.4rem .85rem;background:var(--bg-tertiary);border:1px solid var(--border-color);
|
||||
border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;
|
||||
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;flex-shrink:0;
|
||||
}
|
||||
.skill-cat-btn:hover,.quest-tab-btn:hover,.crafting-cat-btn:hover,.shop-cat-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
|
||||
.skill-cat-btn.active,.quest-tab-btn.active,.crafting-cat-btn.active,.shop-cat-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
|
||||
.skills-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.skills-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
|
||||
.skills-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
|
||||
.skill-points-display{background:var(--bg-tertiary);padding:.4rem .75rem;border-radius:7px;border:1px solid var(--border-color)}
|
||||
.skill-points{color:var(--warning-color);font-weight:600;font-size:.95rem}
|
||||
|
||||
/* ── BASE ────────────────────────────────────────────────────────────── */
|
||||
.base-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.base-view,.base-upgrades{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
|
||||
.base-navigation{display:flex;gap:.35rem;margin-bottom:1rem;flex-wrap:wrap}
|
||||
.base-nav-btn{padding:.4rem .8rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;font-size:.78rem;white-space:nowrap;font-family:'Space Mono',monospace}
|
||||
.base-nav-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
|
||||
.base-nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
|
||||
.base-rooms{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:.5rem}
|
||||
.room-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.55rem .4rem;cursor:pointer;transition:all .25s;text-align:center;min-height:70px;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.room-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
|
||||
.room-item i{font-size:1.2rem;margin-bottom:.3rem;color:var(--primary-color)}
|
||||
.room-item h4{margin:0;color:var(--text-primary);font-size:.7rem;font-weight:600}
|
||||
.room-item p{margin:.15rem 0 0;color:var(--text-secondary);font-size:.62rem}
|
||||
|
||||
.starbases-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.starbase-section{display:flex;flex-direction:column}
|
||||
.starbase-section h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
.starbase-list,.starbase-shop{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:45dvh;overflow-y:auto}
|
||||
.starbase-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:.5rem;cursor:pointer;transition:all .25s;max-height:140px;overflow-y:auto}
|
||||
.starbase-item:hover{border-color:var(--primary-color)}
|
||||
.starbase-item h4{margin:0 0 .35rem;color:var(--text-primary);font-size:.85rem}
|
||||
.starbase-item p{margin:0;color:var(--text-secondary);font-size:.78rem}
|
||||
.starbase-item .level{color:var(--primary-color);font-weight:600}
|
||||
.starbase-item .description{margin-top:.25rem;line-height:1.3;max-height:55px;overflow-y:auto;padding-right:.4rem}
|
||||
.starbase-purchase-list{overflow-y:auto;max-height:40dvh}
|
||||
|
||||
.base-visualization-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
#baseCanvas{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);width:100%;height:min(260px,38dvh)}
|
||||
.base-info-overlay{display:flex;flex-direction:column;gap:.5rem}
|
||||
.base-stats-overlay{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:220px;overflow-y:auto}
|
||||
.base-stats-overlay h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
#baseInfoDisplay{color:var(--text-secondary);font-size:.88rem;line-height:1.4}
|
||||
|
||||
.upgrade-list{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;max-height:380px;overflow-y:auto;padding:.3rem}
|
||||
.upgrade-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
|
||||
.upgrade-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
|
||||
.upgrade-item h4{margin:0 0 .3rem;color:var(--text-primary);font-size:.82rem}
|
||||
.upgrade-item p{margin:0;color:var(--text-secondary);font-size:.75rem}
|
||||
.upgrade-item .cost{margin-top:.3rem;color:var(--primary-color);font-weight:600;font-size:.8rem}
|
||||
|
||||
/* ── QUESTS ──────────────────────────────────────────────────────────── */
|
||||
.quests-container{max-width:1200px;margin:0 auto}
|
||||
.quest-list{display:flex;flex-direction:column;gap:.75rem}
|
||||
.quest-difficulty{display:flex;gap:2px;font-size:.88rem;color:#ffd700;margin-right:.75rem}
|
||||
.difficulty-1{color:#4ade80} .difficulty-2{color:#60a5fa} .difficulty-3{color:#f59e0b} .difficulty-4{color:#ef4444}
|
||||
.quest-header-info{display:flex;align-items:center;gap:.75rem}
|
||||
.all-objectives-completed{color:#4ade80;font-weight:600;padding:.4rem;background:rgba(74,222,128,.1);border-radius:4px;text-align:center;font-size:.85rem}
|
||||
.completion-time{font-size:.78rem;color:var(--text-secondary);margin-top:.4rem;text-align:right}
|
||||
.daily-countdown,.weekly-countdown{margin-bottom:1rem;padding:.65rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px}
|
||||
.countdown-container{display:flex;align-items:center;gap:.4rem;justify-content:center;color:var(--text-primary);font-size:.85rem}
|
||||
.countdown-container i{color:var(--primary-color)}
|
||||
|
||||
/* ── INVENTORY ───────────────────────────────────────────────────────── */
|
||||
.inventory-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.inventory-grid{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;overflow-y:auto;max-height:50dvh}
|
||||
.item-details{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;min-height:110px}
|
||||
.inventory-main{display:flex;gap:1rem;flex:1;min-height:0;flex-direction:column}
|
||||
.inventory-section{flex:1;min-height:0;display:flex;flex-direction:column}
|
||||
.inventory-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
#inventoryGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:.5rem;padding:.35rem}
|
||||
.inventory-slot{width:100%;aspect-ratio:1;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;position:relative}
|
||||
.inventory-slot.starbase-bonus-slot{border:2px solid var(--primary-color);background:rgba(0,212,255,.08)}
|
||||
|
||||
/* Equipment */
|
||||
.equipment-section{margin-bottom:1.5rem;padding:.75rem;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)}
|
||||
.equipment-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
.equipment-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:.75rem}
|
||||
.equipment-slot{display:flex;flex-direction:column;align-items:center;text-align:center}
|
||||
.slot-label{font-size:.82rem;color:var(--text-secondary);margin-bottom:.4rem;font-weight:500}
|
||||
.slot-container{width:70px;height:70px;border:2px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary);transition:all .3s;cursor:pointer}
|
||||
.slot-container:hover{border-color:var(--primary-color);box-shadow:0 0 10px rgba(0,123,255,.3)}
|
||||
|
||||
/* ── SHOP ────────────────────────────────────────────────────────────── */
|
||||
.shop-container{max-width:1200px;margin:0 auto}
|
||||
.shop-items{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.shop-item.legacy{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .3s}
|
||||
.shop-item.legacy:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 5px 15px rgba(0,212,255,.2)}
|
||||
.shop-item-content{display:flex!important;align-items:flex-start!important;gap:.75rem!important}
|
||||
.shop-item-image{flex-shrink:0!important;width:64px!important;height:64px!important;border-radius:6px!important;overflow:hidden!important;background:rgba(0,0,0,.3)!important;display:flex!important;align-items:center!important;justify-content:center!important}
|
||||
.shop-item-name{font-size:1rem!important;font-weight:600!important;color:#00d4ff!important;margin-bottom:4px!important}
|
||||
.shop-item-description{color:#fff!important;font-size:.85rem!important;margin-bottom:.5rem!important;line-height:1.4!important}
|
||||
.shop-item-stats{display:flex!important;flex-wrap:wrap!important;gap:.4rem!important;margin-bottom:.5rem!important}
|
||||
.shop-item-price{font-size:.95rem!important;font-weight:600!important;color:#ffd700!important;margin-bottom:.25rem!important}
|
||||
.shop-item-rarity{display:inline-block!important;padding:2px 8px!important;border-radius:4px!important;font-size:.75rem!important;font-weight:600!important;text-transform:uppercase!important;margin-bottom:.5rem!important}
|
||||
.shop-item-rarity.common{background:rgba(128,128,128,.2)!important;color:#808080!important;border:1px solid rgba(128,128,128,.4)!important}
|
||||
.shop-item-rarity.uncommon{background:rgba(0,255,0,.2)!important;color:#00ff00!important;border:1px solid rgba(0,255,0,.4)!important}
|
||||
.shop-item-rarity.rare{background:rgba(0,100,255,.2)!important;color:#0064ff!important;border:1px solid rgba(0,100,255,.4)!important}
|
||||
.shop-item-rarity.epic{background:rgba(128,0,255,.2)!important;color:#8000ff!important;border:1px solid rgba(128,0,255,.4)!important}
|
||||
.shop-item-rarity.legendary{background:rgba(255,128,0,.2)!important;color:#ff8000!important;border:1px solid rgba(255,128,0,.4)!important}
|
||||
.shop-item-purchase-btn{width:100%!important;padding:.5rem 1rem!important;background:var(--gradient-primary)!important;color:#fff!important;border:none!important;border-radius:4px!important;cursor:pointer!important;font-weight:600!important;transition:all .3s!important;margin-top:.5rem!important}
|
||||
.shop-item-purchase-btn:hover:not(.disabled){background:linear-gradient(135deg,#00ffcc,#00ccaa)!important;transform:translateY(-1px)!important}
|
||||
.shop-item-purchase-btn.disabled{background:rgba(100,100,100,.3)!important;color:#666!important;cursor:not-allowed!important}
|
||||
.shop-refresh-info{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||
.refresh-info-left{display:flex;flex-direction:column;gap:.35rem}
|
||||
.refresh-countdown,.purchase-limit-info{display:flex;align-items:center;gap:.4rem;color:var(--text-secondary);font-size:.82rem}
|
||||
.refresh-countdown i{color:var(--primary-color)}
|
||||
.purchase-limit-info i{color:#ffd700}
|
||||
.refresh-shop-btn{background:var(--gradient-primary);color:#fff;border:none;border-radius:6px;padding:.4rem .85rem;font-size:.82rem;cursor:pointer;transition:all .3s;display:flex;align-items:center;gap:.4rem;white-space:nowrap}
|
||||
.refresh-shop-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,212,255,.3)}
|
||||
|
||||
/* ── CRAFTING ────────────────────────────────────────────────────────── */
|
||||
.crafting-container{max-width:1200px;margin:0 auto}
|
||||
.crafting-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
|
||||
.crafting-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
|
||||
.crafting-info{display:flex;gap:.75rem;align-items:center;flex-wrap:wrap}
|
||||
.crafting-level,.crafting-experience{background:var(--bg-tertiary);padding:.38rem .7rem;border-radius:7px;border:1px solid var(--border-color);display:flex;align-items:center;gap:.4rem;color:var(--text-primary);font-size:.78rem}
|
||||
.crafting-level i,.crafting-experience i{color:var(--primary-color)}
|
||||
.crafting-content{display:flex;flex-direction:column;gap:.75rem}
|
||||
.crafting-sidebar{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
|
||||
.crafting-categories h3{color:var(--primary-color);font-size:1rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
|
||||
.crafting-main{display:flex;flex-direction:column;gap:.75rem}
|
||||
.recipe-list{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem;overflow-y:auto;max-height:45dvh}
|
||||
.crafting-details{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
|
||||
.crafting-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.selected-recipe{display:flex;flex-direction:column;align-items:center;text-align:center;color:var(--text-muted)}
|
||||
.selected-recipe i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5}
|
||||
.selected-recipe h3{margin-bottom:.5rem;color:var(--text-secondary)}
|
||||
|
||||
/* Recipe items */
|
||||
.recipe-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.85rem;margin-bottom:.75rem;cursor:pointer;transition:all .3s;position:relative;overflow:hidden}
|
||||
.recipe-item:hover{background:var(--hover-bg);border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.2)}
|
||||
.recipe-item.selected{background:var(--gradient-primary);border-color:var(--primary-color);box-shadow:0 4px 12px rgba(0,212,255,.3)}
|
||||
.recipe-item.locked{opacity:.55;cursor:not-allowed}
|
||||
.recipe-item.locked:hover{transform:none;border-color:var(--border-color)}
|
||||
.recipe-item.can-craft{border-color:var(--success-color)}
|
||||
.recipe-item.can-craft:hover{box-shadow:0 4px 12px rgba(0,255,136,.2)}
|
||||
.recipe-item.missing-materials{opacity:.7;border-color:var(--warning-color)}
|
||||
.recipe-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||
.recipe-header h4{color:var(--text-primary);font-size:.95rem}
|
||||
.recipe-level{background:var(--bg-primary);color:var(--warning-color);padding:3px 7px;border-radius:4px;font-size:.75rem;font-weight:600}
|
||||
.recipe-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.6rem}
|
||||
.recipe-materials{display:flex;gap:.4rem;flex-wrap:wrap}
|
||||
.material-tag{background:var(--bg-primary);color:var(--text-muted);padding:2px 6px;border-radius:4px;font-size:.72rem}
|
||||
.material-item{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.08);font-size:.82rem}
|
||||
.material-item:last-child{border-bottom:none}
|
||||
.material-item.missing{color:var(--error-color)}
|
||||
.material-name{color:var(--text-secondary)} .material-quantity{font-size:.82rem;font-weight:600;color:var(--text-primary)}
|
||||
.material-item.missing .material-quantity{color:var(--error-color);font-weight:600}
|
||||
.missing-materials-text{color:var(--error-color);font-size:.78rem;margin-top:.5rem;padding:.5rem;background:rgba(255,51,102,.1);border-radius:4px;border:1px solid rgba(255,51,102,.3)}
|
||||
.recipe-time{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);font-size:.78rem;margin-top:.4rem}
|
||||
.recipe-time i{color:var(--primary-color)}
|
||||
|
||||
/* ── CONSOLE WINDOW ──────────────────────────────────────────────────── */
|
||||
.console-window{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(580px,95vw);height:min(380px,75dvh);background:var(--bg-secondary);border:2px solid var(--primary-color);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.8);display:none;flex-direction:column;z-index:10000;font-family:'Courier New',monospace}
|
||||
.console-header{background:var(--gradient-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:18px;cursor:pointer;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background .3s}
|
||||
.console-close:hover{background:rgba(255,255,255,.2)}
|
||||
.console-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||||
.console-output{flex:1;padding:12px;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-line{margin-bottom:4px;word-wrap:break-word}
|
||||
.console-line.console-info{color:var(--primary-color)} .console-line.console-success{color:var(--success-color)} .console-line.console-error{color:var(--error-color)} .console-line.console-warning{color:var(--warning-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:7px 11px;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,.2)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
ANIMATIONS
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
TABLET ≥ 640px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:640px){
|
||||
:root{--header-h:56px;--pg:1rem}
|
||||
html{font-size:15px}
|
||||
.player-title{display:inline}
|
||||
.player-name{max-width:22ch}
|
||||
.dashboard-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.skills-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.shop-items{grid-template-columns:repeat(2,1fr)}
|
||||
.crafting-grid{grid-template-columns:repeat(2,1fr)}
|
||||
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr))}
|
||||
.starbases-container{flex-direction:row}
|
||||
.starbase-section{flex:1}
|
||||
.base-rooms{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
|
||||
.upgrade-list{grid-template-columns:repeat(3,1fr)}
|
||||
.nav-drawer-grid{grid-template-columns:repeat(5,1fr)}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ≥ 1024px — switch to top nav
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1024px){
|
||||
:root{--header-h:60px;--pg:1rem}
|
||||
html{font-size:15px}
|
||||
.main-nav{display:flex}
|
||||
.bottom-nav,.bottom-nav-more,.nav-drawer,.nav-drawer-overlay{display:none!important}
|
||||
.main-content{padding-bottom:var(--pg)}
|
||||
.player-title{display:inline}
|
||||
.player-name{max-width:26ch;font-size:.95rem}
|
||||
.resource{font-size:.85rem}
|
||||
.dashboard-grid{grid-template-columns:repeat(3,1fr);gap:1rem}
|
||||
.dungeons-container{flex-direction:row}
|
||||
.dungeon-selector{width:270px;flex-shrink:0;max-height:calc(100dvh - 160px);overflow-y:auto}
|
||||
.dungeon-view{flex:1;min-height:300px}
|
||||
.base-container{flex-direction:row}
|
||||
.base-view{flex:1}
|
||||
.base-upgrades{width:270px;flex-shrink:0}
|
||||
.skills-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.shop-items{grid-template-columns:repeat(3,1fr)}
|
||||
.crafting-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.crafting-content{flex-direction:row}
|
||||
.crafting-sidebar{width:210px;flex-shrink:0}
|
||||
.crafting-main{flex-direction:row;flex:1}
|
||||
.recipe-list{flex:1;max-height:none}
|
||||
.crafting-details{width:290px;flex-shrink:0}
|
||||
.inventory-container{flex-direction:row}
|
||||
.inventory-main{flex-direction:row}
|
||||
.inventory-grid{flex:1;max-height:none}
|
||||
.item-details{width:270px;flex-shrink:0}
|
||||
.starbases-container{flex-direction:row}
|
||||
.starbase-list,.starbase-shop{max-height:calc(100dvh - 250px)}
|
||||
.base-visualization-container{flex-direction:row}
|
||||
#baseCanvas{flex:1;height:auto;min-height:280px}
|
||||
.base-info-overlay{width:270px;flex-shrink:0}
|
||||
.base-stats-overlay{max-height:calc(100dvh - 310px)}
|
||||
.upgrade-list{grid-template-columns:repeat(auto-fill,minmax(170px,1fr))}
|
||||
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}
|
||||
.server-confirmation,.save-confirmation,.options-grid{flex-direction:row;gap:2rem}
|
||||
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{flex-direction:column;gap:.75rem;min-width:175px}
|
||||
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{width:100%;flex:none;min-width:auto}
|
||||
.base-navigation{justify-content:center}
|
||||
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{justify-content:center;flex-wrap:nowrap}
|
||||
.player-stats-grid{grid-template-columns:repeat(2,1fr)}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
WIDE ≥ 1280px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1280px){
|
||||
.dashboard-grid{grid-template-columns:repeat(4,1fr)}
|
||||
.skills-grid{grid-template-columns:repeat(4,1fr)}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Table & Ship Styles (mobile-first)
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── 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,.25);
|
||||
margin:8px 0;font-size:.8rem;
|
||||
}
|
||||
.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:10px 12px;text-align:left;font-weight:600;font-size:.78rem;
|
||||
border-bottom:2px solid rgba(255,255,255,.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:10px 12px;border-bottom:1px solid rgba(255,255,255,.07);
|
||||
color:#e0e0e0;font-size:.78rem;
|
||||
}
|
||||
.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,.1);transition:background .25s;
|
||||
}
|
||||
|
||||
/* Difficulty colors */
|
||||
.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 */
|
||||
.skills-table .skill-level{font-weight:bold;color:#667eea}
|
||||
.skills-table .skill-progress{width:80px;height:6px;background:rgba(255,255,255,.1);border-radius:3px;overflow:hidden}
|
||||
.skills-table .progress-fill{height:100%;background:linear-gradient(90deg,#667eea,#764ba2);transition:width .3s}
|
||||
|
||||
/* Base tables */
|
||||
.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}
|
||||
|
||||
/* Action buttons in tables */
|
||||
.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:5px 10px;border:none;border-radius:4px;cursor:pointer;
|
||||
font-size:.72rem;font-weight:600;transition:all .25s;text-transform:uppercase;
|
||||
min-height:32px;
|
||||
}
|
||||
.btn-action.btn-primary{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}
|
||||
.btn-action.btn-primary:hover{background:linear-gradient(135deg,#764ba2,#667eea);transform:translateY(-1px)}
|
||||
.btn-action.btn-secondary{background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2)}
|
||||
.btn-action.btn-secondary:hover{background:rgba(255,255,255,.2);transform:translateY(-1px)}
|
||||
.btn-action.btn-success{background:linear-gradient(135deg,#00ff00,#00cc00);color:#fff}
|
||||
.btn-action.btn-danger{background:linear-gradient(135deg,#ff0000,#cc0000);color:#fff}
|
||||
|
||||
/* Specialty table cells */
|
||||
.starbase-management-table .starbase-level{font-weight:bold;color:#667eea}
|
||||
.starbase-shop-table .starbase-cost{font-weight:bold;color:#ffd700}
|
||||
.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:#888}
|
||||
.quests-table .quest-type-failed{color:#ff0000}
|
||||
.quests-table .quest-progress,.inventory-table .item-stats{font-size:.72rem}
|
||||
.inventory-table .item-rarity-common{color:#888} .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}
|
||||
.shop-table .item-price{font-weight:bold;color:#ffd700}
|
||||
.shop-table .item-description{font-size:.72rem;color:#ccc;max-width:180px}
|
||||
|
||||
/* ── Ship grid ───────────────────────────────────────────────────────── */
|
||||
.ship-grid{display:grid;grid-template-columns:1fr;gap:12px;padding:12px 0}
|
||||
.ship-card{
|
||||
background:var(--card-bg);border-radius:12px;padding:12px;
|
||||
border:2px solid var(--primary-color);box-shadow:0 4px 20px rgba(0,0,0,.3);
|
||||
transition:all .25s;position:relative;overflow:hidden;
|
||||
}
|
||||
.ship-card:hover{transform:translateY(-4px);box-shadow:0 8px 28px rgba(0,212,255,.35)}
|
||||
.ship-card.active{border-color:var(--success-color);box-shadow:0 6px 22px rgba(0,255,136,.18)}
|
||||
.ship-card.active::before{content:"ACTIVE";position:absolute;top:8px;right:8px;background:var(--gradient-secondary);color:var(--text-primary);padding:3px 7px;border-radius:4px;font-size:9px;font-weight:bold;text-transform:uppercase;letter-spacing:.5px}
|
||||
|
||||
.ship-card-header{display:flex;flex-direction:row;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.ship-card-image{width:70px;height:70px;border-radius:8px;object-fit:cover;border:2px solid var(--primary-color);flex-shrink:0}
|
||||
.ship-card-info{flex:1;min-width:0}
|
||||
.ship-card-rarity{color:var(--text-secondary);font-size:.72rem;font-weight:bold;text-transform:uppercase;letter-spacing:.5px;padding:3px 7px;border-radius:4px;background:var(--hover-bg);border:1px solid var(--border-color);display:inline-block}
|
||||
.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:6px;margin-bottom:10px}
|
||||
.ship-card-stat{display:flex;justify-content:space-between;align-items:center;padding:5px 8px;background:rgba(255,255,255,.04);border-radius:4px;border:1px solid rgba(255,255,255,.08)}
|
||||
.ship-card-stat .stat-label{color:var(--text-muted);font-size:.68rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.ship-card-stat .stat-value{color:var(--text-secondary);font-weight:bold;font-size:.72rem}
|
||||
.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:8px}
|
||||
.ship-card-actions .btn-action{flex:1;padding:7px 10px;border:none;border-radius:6px;cursor:pointer;font-size:.72rem;font-weight:600;transition:all .25s;text-transform:uppercase;min-height:36px;-webkit-tap-highlight-color:transparent}
|
||||
.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,.btn-action.btn-repair{background:var(--gradient-secondary);color:var(--text-primary)}
|
||||
.btn-action.btn-upgrade:hover,.btn-action.btn-repair:hover{background:var(--gradient-primary);transform:translateY(-2px)}
|
||||
|
||||
/* Ship layout (current ship + grid) */
|
||||
.ship-layout{display:flex;flex-direction:column;gap:1rem;margin-top:.75rem}
|
||||
.current-ship-section{background:var(--card-bg);border-radius:8px;padding:1rem;border:2px solid var(--primary-color)}
|
||||
.ship-grid-section{flex:1;min-width:0}
|
||||
.ship-grid-section h4,.current-ship-section h4{color:var(--primary-color);margin-bottom:.75rem;font-size:.92rem;text-transform:uppercase;letter-spacing:1px}
|
||||
.current-ship-section h4{text-align:center}
|
||||
.current-ship-display{display:flex;flex-direction:column;align-items:center;gap:1rem;text-align:center}
|
||||
.current-ship-image img{width:100px;height:100px;object-fit:cover;border-radius:8px;border:2px solid var(--primary-color);box-shadow:0 4px 15px rgba(0,212,255,.3)}
|
||||
.current-ship-details{flex:1;text-align:center;min-width:0}
|
||||
.current-ship-details h5{color:var(--text-primary);margin-bottom:.75rem;font-size:1.1rem;font-weight:bold}
|
||||
.current-ship-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.ship-stat{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--hover-bg);border-radius:4px;border:1px solid var(--border-color)}
|
||||
.ship-stat .stat-label{color:var(--text-muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.ship-stat .stat-value{color:var(--text-secondary);font-weight:bold;font-size:.82rem}
|
||||
.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:1.5rem}
|
||||
.ship-table-section h4{color:#667eea;margin-bottom:.75rem;font-size:.92rem;text-transform:uppercase;letter-spacing:1px}
|
||||
|
||||
/* ── Console window ──────────────────────────────────────────────────── */
|
||||
.console-window{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(580px,96vw);height:min(380px,70dvh);background:var(--bg-secondary);border:2px solid var(--primary-color);border-radius:8px;box-shadow:0 8px 30px rgba(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:13px}
|
||||
.console-close{background:none;border:none;color:var(--text-primary);font-size:18px;cursor:pointer;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background .2s}
|
||||
.console-close:hover{background:rgba(255,255,255,.2)}
|
||||
.console-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||||
.console-output{flex:1;padding:12px;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-line{margin-bottom:4px;word-wrap:break-word}
|
||||
.console-error{color:var(--error-color)} .console-success{color:var(--success-color)}
|
||||
.console-warning{color:var(--warning-color)} .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:7px 11px;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,.2)}
|
||||
.console-input::placeholder{color:var(--text-muted)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
TABLET ≥ 640px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:640px){
|
||||
.ship-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.ship-layout{flex-direction:row}
|
||||
.current-ship-section{flex:0 0 320px}
|
||||
.current-ship-display{flex-direction:row;align-items:flex-start;text-align:left}
|
||||
.current-ship-details{text-align:left}
|
||||
.current-ship-details h5{text-align:left}
|
||||
.current-ship-stats{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ≥ 1024px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1024px){
|
||||
.ship-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
|
||||
.ship-card-header{flex-direction:column;align-items:center;text-align:center}
|
||||
.ship-card-image{width:80px;height:80px}
|
||||
.current-ship-section{flex:0 0 380px}
|
||||
.current-ship-stats{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
|
||||
/* ── Responsive table fallback — horizontal scroll ───────────────────── */
|
||||
@media(max-width:639px){
|
||||
.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{
|
||||
display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;white-space:nowrap;
|
||||
}
|
||||
.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:.72rem}
|
||||
.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:.72rem}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
# Galaxy Strike Online - Game Server Environment
|
||||
# Copy to .env and fill in values
|
||||
|
||||
# Server
|
||||
PORT=3002
|
||||
NODE_ENV=development
|
||||
SERVER_URL=http://localhost:3002
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
|
||||
# API Server URL (for cross-service communication)
|
||||
API_SERVER_URL=http://localhost:3001
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 188 B |
@ -1,17 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
console.log('[DATABASE] Connecting to MongoDB:', process.env.MONGODB_URI);
|
||||
const conn = await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
|
||||
console.log(`[DATABASE] MongoDB Connected: ${conn.connection.host}`);
|
||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||
} catch (error) {
|
||||
console.error('[DATABASE] Database connection failed:', error);
|
||||
logger.error('Database connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
@ -1,191 +0,0 @@
|
||||
/**
|
||||
* Create placeholder images for existing items in ItemSystem
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Create directories if they don't exist
|
||||
const directories = [
|
||||
'assets/images/ships',
|
||||
'assets/images/weapons',
|
||||
'assets/images/armors',
|
||||
'assets/images/items/materials',
|
||||
'assets/images/items/consumables',
|
||||
'assets/images/items/cosmetics',
|
||||
'assets/images/ui'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
const fullPath = path.join(__dirname, dir);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
fs.mkdirSync(fullPath, { recursive: true });
|
||||
console.log(`Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a simple colored rectangle as placeholder
|
||||
function createPlaceholder(width, height, color, text) {
|
||||
// Create a simple 1x1 pixel colored PNG (base64 encoded)
|
||||
// This is a minimal PNG with transparency
|
||||
const createColorPNG = (r, g, b) => {
|
||||
return Buffer.from([
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
|
||||
0x49, 0x48, 0x44, 0x52, // IHDR
|
||||
width >> 24, width >> 16, width >> 8, width, // width
|
||||
height >> 24, height >> 16, height >> 8, height, // height
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, compression, filter, interlace
|
||||
0x4B, 0x6D, 0x29, 0xDC, // CRC
|
||||
0x00, 0x00, 0x00, 0x0C, // IDAT chunk length
|
||||
0x49, 0x44, 0x41, 0x54, // IDAT
|
||||
0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, // compressed data
|
||||
0x00, 0x00, 0x00, 0x00, // IEND chunk length
|
||||
0x49, 0x45, 0x4E, 0x44, // IEND
|
||||
0xAE, 0x42, 0x60, 0x82 // CRC
|
||||
]);
|
||||
};
|
||||
|
||||
// For simplicity, let's copy existing images or create basic placeholders
|
||||
return createColorPNG(
|
||||
color === 'blue' ? 52 : color === 'red' ? 231 : color === 'green' ? 39 : 149,
|
||||
color === 'blue' ? 152 : color === 'red' ? 76 : color === 'green' ? 174 : 165,
|
||||
color === 'blue' ? 219 : color === 'red' ? 60 : color === 'green' ? 96 : 166
|
||||
);
|
||||
}
|
||||
|
||||
// Items from your ItemSystem
|
||||
const items = {
|
||||
ships: [
|
||||
'starter_cruiser_common',
|
||||
'starter_cruiser_uncommon',
|
||||
'starter_cruiser_rare',
|
||||
'interceptor_common',
|
||||
'interceptor_uncommon'
|
||||
],
|
||||
weapons: [
|
||||
'laser_pistol_common',
|
||||
'laser_pistol_uncommon',
|
||||
'laser_pistol_rare',
|
||||
'plasma_rifle_common',
|
||||
'plasma_rifle_uncommon',
|
||||
'plasma_rifle_rare'
|
||||
],
|
||||
armors: [
|
||||
'light_armor_common',
|
||||
'light_armor_uncommon',
|
||||
'light_armor_rare',
|
||||
'medium_armor_common',
|
||||
'medium_armor_uncommon',
|
||||
'medium_armor_rare'
|
||||
],
|
||||
materials: [
|
||||
'steel_plating',
|
||||
'energy_crystal',
|
||||
'rare_metal',
|
||||
'quantum_core',
|
||||
'nanomaterials',
|
||||
'dark_matter_fragment'
|
||||
],
|
||||
consumables: [
|
||||
'health_pack',
|
||||
'energy_boost',
|
||||
'shield_recharge',
|
||||
'repair_kit',
|
||||
'ammo_pack',
|
||||
'experience_boost'
|
||||
],
|
||||
cosmetics: [
|
||||
'cool_paint_job',
|
||||
'neon_lights',
|
||||
'custom_decal',
|
||||
'golden_trim',
|
||||
'carbon_fiber',
|
||||
'chrome_finish'
|
||||
]
|
||||
};
|
||||
|
||||
// Copy existing images or create placeholders
|
||||
console.log('Creating placeholder images...');
|
||||
|
||||
// For ships - copy existing or create placeholder
|
||||
items.ships.forEach(shipId => {
|
||||
const existingPath = path.join(__dirname, `assets/images/ships/starter_cruiser.png`);
|
||||
const targetPath = path.join(__dirname, `assets/images/ships/${shipId}.png`);
|
||||
|
||||
if (fs.existsSync(existingPath) && !fs.existsSync(targetPath)) {
|
||||
fs.copyFileSync(existingPath, targetPath);
|
||||
console.log(`Copied ship: ${shipId}.png`);
|
||||
} else if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(80, 80, 'blue', shipId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created ship placeholder: ${shipId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// For weapons - copy existing or create placeholder
|
||||
items.weapons.forEach(weaponId => {
|
||||
const existingPath = path.join(__dirname, `assets/images/weapons/starter_blaster.png`);
|
||||
const targetPath = path.join(__dirname, `assets/images/weapons/${weaponId}.png`);
|
||||
|
||||
if (fs.existsSync(existingPath) && !fs.existsSync(targetPath)) {
|
||||
fs.copyFileSync(existingPath, targetPath);
|
||||
console.log(`Copied weapon: ${weaponId}.png`);
|
||||
} else if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(64, 64, 'red', weaponId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created weapon placeholder: ${weaponId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// For armors - create placeholders
|
||||
items.armors.forEach(armorId => {
|
||||
const targetPath = path.join(__dirname, `assets/images/armors/${armorId}.png`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(64, 64, 'green', armorId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created armor placeholder: ${armorId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// For materials - create placeholders
|
||||
items.materials.forEach(materialId => {
|
||||
const targetPath = path.join(__dirname, `assets/images/items/materials/${materialId}.png`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(48, 48, 'gray', materialId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created material placeholder: ${materialId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// For consumables - create placeholders
|
||||
items.consumables.forEach(consumableId => {
|
||||
const targetPath = path.join(__dirname, `assets/images/items/consumables/${consumableId}.png`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(48, 48, 'purple', consumableId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created consumable placeholder: ${consumableId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// For cosmetics - create placeholders
|
||||
items.cosmetics.forEach(cosmeticId => {
|
||||
const targetPath = path.join(__dirname, `assets/images/items/cosmetics/${cosmeticId}.png`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
const placeholder = createPlaceholder(64, 64, 'gold', cosmeticId);
|
||||
fs.writeFileSync(targetPath, placeholder);
|
||||
console.log(`Created cosmetic placeholder: ${cosmeticId}.png`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create UI placeholder
|
||||
const placeholderPath = path.join(__dirname, 'assets/images/ui/placeholder.png');
|
||||
if (!fs.existsSync(placeholderPath)) {
|
||||
const placeholder = createPlaceholder(80, 80, 'gray', 'placeholder');
|
||||
fs.writeFileSync(placeholderPath, placeholder);
|
||||
console.log('Created UI placeholder.png');
|
||||
}
|
||||
|
||||
console.log('\n🎉 Placeholder images created successfully!');
|
||||
console.log('📦 Your shop should now display items with placeholder images!');
|
||||
console.log('🔄 Replace these with actual images as you create them.');
|
||||
@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"tiers": [
|
||||
{
|
||||
"tier": 1,
|
||||
"techs": [
|
||||
{ "id": "alliance_storage_1", "name": "Expanded Warehouse I", "desc": "Increase alliance warehouse cap by 10,000 per resource.", "cost": {"credits":5000,"metal":2000}, "effect": {"warehouseCap":10000}, "prereq": [], "icon": "fa-warehouse", "maxLevel": 3 },
|
||||
{ "id": "alliance_xp_1", "name": "Shared Knowledge I", "desc": "+5% XP for all members.", "cost": {"credits":8000,"crystal":500}, "effect": {"memberXpBonus":0.05}, "prereq": [], "icon": "fa-graduation-cap", "maxLevel": 3 },
|
||||
{ "id": "alliance_credits_1", "name": "Trade Network I", "desc": "Reduce market listing fee by 0.5% for all members.", "cost": {"credits":6000,"gas":1000}, "effect": {"marketFeeReduction":0.005}, "prereq": [], "icon": "fa-coins", "maxLevel": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"tier": 2,
|
||||
"techs": [
|
||||
{ "id": "alliance_storage_2", "name": "Expanded Warehouse II", "desc": "Increase warehouse cap by an additional 25,000.", "cost": {"credits":15000,"metal":8000}, "effect": {"warehouseCap":25000}, "prereq": ["alliance_storage_1"], "icon": "fa-warehouse", "maxLevel": 1 },
|
||||
{ "id": "alliance_craft_1", "name": "Collective Crafting", "desc": "All members gain -10% craft time.", "cost": {"credits":12000,"crystal":1500},"effect": {"craftTimeReduction":0.10}, "prereq": ["alliance_xp_1"], "icon": "fa-hammer", "maxLevel": 2 },
|
||||
{ "id": "alliance_defense_1", "name": "Defensive Grid I", "desc": "+10% PvP defense for all members.", "cost": {"credits":10000,"metal":5000}, "effect": {"pvpDefenseBonus":0.10}, "prereq": ["alliance_credits_1"],"icon": "fa-shield-alt", "maxLevel": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"tier": 3,
|
||||
"techs": [
|
||||
{ "id": "alliance_dm_1", "name": "Void Tap", "desc": "+15% dark matter production for all members.", "cost": {"credits":25000,"darkMatter":50},"effect": {"darkMatterBonus":0.15}, "prereq": ["alliance_storage_2","alliance_craft_1"], "icon": "fa-atom", "maxLevel": 1 },
|
||||
{ "id": "alliance_fleet_1", "name": "Fleet Coordination", "desc": "+20% fleet mission rewards for all members.", "cost": {"credits":20000,"gas":5000}, "effect": {"fleetRewardBonus":0.20}, "prereq": ["alliance_defense_1"],"icon": "fa-fighter-jet","maxLevel": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
{
|
||||
"tabId": "daily",
|
||||
"title": "Dailies",
|
||||
"description": "Resets daily. Small rewards, fast goals.",
|
||||
"quests": [
|
||||
{
|
||||
"id": "d_craft_3",
|
||||
"name": "Quick Craft"
|
||||
},
|
||||
{
|
||||
"id": "d_dungeons_3",
|
||||
"name": "Dungeon Hopping"
|
||||
},
|
||||
{
|
||||
"id": "d_explore_30",
|
||||
"name": "Daily Survey"
|
||||
},
|
||||
{
|
||||
"id": "d_login_5",
|
||||
"name": "Frequent Flyer"
|
||||
},
|
||||
{
|
||||
"id": "d_smelt_5",
|
||||
"name": "Smelting Run"
|
||||
},
|
||||
{
|
||||
"id": "d_kill_pirates_10",
|
||||
"name": "Pirate Bounty"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
{
|
||||
"tabId": "main_story",
|
||||
"title": "Main Story",
|
||||
"description": "The core story of Galaxy Strike Online.",
|
||||
"quests": [
|
||||
{
|
||||
"id": "ms_ch1_001",
|
||||
"name": "First Steps Into the Stars"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch1_002",
|
||||
"name": "Into the Unknown"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch1_003",
|
||||
"name": "The Art of Craft"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch2_001",
|
||||
"name": "Alien Incursion"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch2_002",
|
||||
"name": "Power Core"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch2_003",
|
||||
"name": "The Void Calls"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch3_001",
|
||||
"name": "Dreadnought Rising"
|
||||
},
|
||||
{
|
||||
"id": "ms_ch3_002",
|
||||
"name": "Galaxy Strike"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"tabId": "monthly",
|
||||
"title": "Monthlies",
|
||||
"description": "The ultimate monthly challenges.",
|
||||
"quests": [
|
||||
{
|
||||
"id": "m_raids_20",
|
||||
"name": "Monthly Raider"
|
||||
},
|
||||
{
|
||||
"id": "m_dungeons_50",
|
||||
"name": "Legendary Dungeon Diver"
|
||||
},
|
||||
{
|
||||
"id": "m_craft_100",
|
||||
"name": "Master Craftsman"
|
||||
},
|
||||
{
|
||||
"id": "m_void_run",
|
||||
"name": "Void Conqueror"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"tabId": "weekly",
|
||||
"title": "Weeklies",
|
||||
"description": "Resets weekly. Bigger challenges, better rewards.",
|
||||
"quests": [
|
||||
{
|
||||
"id": "w_raids_3",
|
||||
"name": "Raid Week"
|
||||
},
|
||||
{
|
||||
"id": "w_craft_20",
|
||||
"name": "Crafting Spree"
|
||||
},
|
||||
{
|
||||
"id": "w_dungeons_10",
|
||||
"name": "Dungeon Crawler"
|
||||
},
|
||||
{
|
||||
"id": "w_survive_hardcore_7",
|
||||
"name": "Hardcore Survivor"
|
||||
},
|
||||
{
|
||||
"id": "w_gather_50",
|
||||
"name": "Resource Rush"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
{
|
||||
"id": "alien_ruins",
|
||||
"name": "Alien Ruins",
|
||||
"category": "exploration",
|
||||
"description": "Ancient alien structures filled with mysterious technology",
|
||||
"difficulty": "medium",
|
||||
"minLevel": 3,
|
||||
"roomCount": [
|
||||
5,
|
||||
8
|
||||
],
|
||||
"energyCost": 20,
|
||||
"rewardMultiplier": 1.2,
|
||||
"enemyPool": [
|
||||
"alien_guardian",
|
||||
"ancient_drone",
|
||||
"crystal_golem"
|
||||
],
|
||||
"bossId": "crystal_golem",
|
||||
"rewards": {
|
||||
"creditsMin": 200,
|
||||
"creditsMax": 500,
|
||||
"experienceMin": 150,
|
||||
"experienceMax": 300
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-monument",
|
||||
"color": "#4488cc"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "wp_alien_ruins",
|
||||
"weight": 8,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "energy_boost_small",
|
||||
"weight": 60,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 3
|
||||
},
|
||||
{
|
||||
"itemId": "health_kit_small",
|
||||
"weight": 60,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
{
|
||||
"id": "asteroid_mine",
|
||||
"name": "Asteroid Mine",
|
||||
"category": "exploration",
|
||||
"description": "Abandoned mining facilities in asteroid fields",
|
||||
"difficulty": "easy",
|
||||
"minLevel": 2,
|
||||
"roomCount": [
|
||||
4,
|
||||
7
|
||||
],
|
||||
"energyCost": 10,
|
||||
"rewardMultiplier": 0.8,
|
||||
"enemyPool": [
|
||||
"mining_drone",
|
||||
"rock_creature",
|
||||
"space_pirate"
|
||||
],
|
||||
"bossId": "mining_drone",
|
||||
"rewards": {
|
||||
"creditsMin": 60,
|
||||
"creditsMax": 180,
|
||||
"experienceMin": 50,
|
||||
"experienceMax": 120
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-meteor",
|
||||
"color": "#888844"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "wp_arctic_station",
|
||||
"weight": 8,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "fuel_cell",
|
||||
"weight": 60,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 5
|
||||
},
|
||||
{
|
||||
"itemId": "health_kit_small",
|
||||
"weight": 50,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
{
|
||||
"id": "corrupted_vault",
|
||||
"name": "Corrupted AI Vault",
|
||||
"category": "combat",
|
||||
"description": "Malfunctioning AI facilities with corrupted security systems",
|
||||
"difficulty": "hard",
|
||||
"minLevel": 5,
|
||||
"roomCount": [
|
||||
6,
|
||||
9
|
||||
],
|
||||
"energyCost": 25,
|
||||
"rewardMultiplier": 1.5,
|
||||
"enemyPool": [
|
||||
"security_drone",
|
||||
"corrupted_ai",
|
||||
"virus_program"
|
||||
],
|
||||
"bossId": "corrupted_ai",
|
||||
"rewards": {
|
||||
"creditsMin": 400,
|
||||
"creditsMax": 900,
|
||||
"experienceMin": 300,
|
||||
"experienceMax": 600
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-microchip",
|
||||
"color": "#44cc44"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "room_vault",
|
||||
"weight": 12,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "wp_void_black",
|
||||
"weight": 10,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "credit_multiplier",
|
||||
"weight": 40,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "xp_booster",
|
||||
"weight": 40,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "pirate_lair",
|
||||
"name": "Pirate Lair",
|
||||
"description": "Dangerous pirate hideouts with valuable loot",
|
||||
"difficulty": "easy",
|
||||
"minLevel": 1,
|
||||
"maxPlayers": 4,
|
||||
"roomCount": [4, 6],
|
||||
"energyCost": 15,
|
||||
"rewardMultiplier": 1.0,
|
||||
"enemyPool": ["space_pirate", "pirate_captain", "defense_turret"],
|
||||
"lootTable": [
|
||||
{ "itemId": "iron_ore", "weight": 40, "qtyMin": 1, "qtyMax": 5 },
|
||||
{ "itemId": "copper_ore", "weight": 30, "qtyMin": 1, "qtyMax": 3 },
|
||||
{ "itemId": "bandage", "weight": 20, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "pistol_standard_common", "weight": 10, "qtyMin": 1, "qtyMax": 1 }
|
||||
],
|
||||
"ui": { "icon": "fa-skull-crossbones", "color": "#e67e22" }
|
||||
},
|
||||
{
|
||||
"id": "asteroid_mine",
|
||||
"name": "Asteroid Mine",
|
||||
"description": "Abandoned mining facilities rich in ore deposits",
|
||||
"difficulty": "easy",
|
||||
"minLevel": 2,
|
||||
"maxPlayers": 4,
|
||||
"roomCount": [4, 7],
|
||||
"energyCost": 10,
|
||||
"rewardMultiplier": 0.8,
|
||||
"enemyPool": ["mining_drone", "rock_creature", "explosive_asteroid"],
|
||||
"lootTable": [
|
||||
{ "itemId": "iron_ore", "weight": 35, "qtyMin": 2, "qtyMax": 8 },
|
||||
{ "itemId": "copper_ore", "weight": 30, "qtyMin": 1, "qtyMax": 6 },
|
||||
{ "itemId": "titanium_ore", "weight": 20, "qtyMin": 1, "qtyMax": 3 },
|
||||
{ "itemId": "gold_ore", "weight": 15, "qtyMin": 1, "qtyMax": 2 }
|
||||
],
|
||||
"ui": { "icon": "fa-meteor", "color": "#95a5a6" }
|
||||
},
|
||||
{
|
||||
"id": "alien_ruins",
|
||||
"name": "Alien Ruins",
|
||||
"description": "Ancient alien structures filled with mysterious technology",
|
||||
"difficulty": "medium",
|
||||
"minLevel": 3,
|
||||
"maxPlayers": 4,
|
||||
"roomCount": [5, 8],
|
||||
"energyCost": 20,
|
||||
"rewardMultiplier": 1.2,
|
||||
"enemyPool": ["alien_guardian", "ancient_drone", "crystal_golem"],
|
||||
"lootTable": [
|
||||
{ "itemId": "flux_crystal", "weight": 30, "qtyMin": 1, "qtyMax": 3 },
|
||||
{ "itemId": "alien_fauna_sample", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "basic_circuit", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "assault_rifle_common", "weight": 20, "qtyMin": 1, "qtyMax": 1 }
|
||||
],
|
||||
"ui": { "icon": "fa-monument", "color": "#8e44ad" }
|
||||
},
|
||||
{
|
||||
"id": "corrupted_vault",
|
||||
"name": "Corrupted AI Vault",
|
||||
"description": "Malfunctioning AI facilities with corrupted security systems",
|
||||
"difficulty": "hard",
|
||||
"minLevel": 5,
|
||||
"maxPlayers": 4,
|
||||
"roomCount": [6, 9],
|
||||
"energyCost": 25,
|
||||
"rewardMultiplier": 1.5,
|
||||
"enemyPool": ["security_drone", "corrupted_ai", "virus_program"],
|
||||
"lootTable": [
|
||||
{ "itemId": "logic_circuit", "weight": 30, "qtyMin": 1, "qtyMax": 3 },
|
||||
{ "itemId": "power_circuit", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "control_circuit", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "sniper_rare", "weight": 20, "qtyMin": 1, "qtyMax": 1 }
|
||||
],
|
||||
"ui": { "icon": "fa-robot", "color": "#e74c3c" }
|
||||
},
|
||||
{
|
||||
"id": "nebula_anomaly",
|
||||
"name": "Nebula Anomaly",
|
||||
"description": "Strange energy anomalies in the deep void of space",
|
||||
"difficulty": "extreme",
|
||||
"minLevel": 8,
|
||||
"maxPlayers": 4,
|
||||
"roomCount": [7, 10],
|
||||
"energyCost": 30,
|
||||
"rewardMultiplier": 2.0,
|
||||
"enemyPool": ["energy_being", "phase_shifter", "quantum_entity"],
|
||||
"lootTable": [
|
||||
{ "itemId": "quantum_circuit", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "void_crystal", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "phase_crystal", "weight": 25, "qtyMin": 1, "qtyMax": 2 },
|
||||
{ "itemId": "plasma_cutter_epic", "weight": 25, "qtyMin": 1, "qtyMax": 1 }
|
||||
],
|
||||
"ui": { "icon": "fa-star", "color": "#2980b9" }
|
||||
}
|
||||
]
|
||||
@ -1,15 +0,0 @@
|
||||
[
|
||||
{ "id": "space_pirate", "name": "Space Pirate", "health": 25, "attack": 10, "defense": 3, "speed": 8, "experience": 15, "credits": 12, "rarity": "common" },
|
||||
{ "id": "pirate_captain", "name": "Pirate Captain", "health": 40, "attack": 15, "defense": 6, "speed": 12, "experience": 30, "credits": 20, "rarity": "uncommon" },
|
||||
{ "id": "defense_turret", "name": "Defense Turret", "health": 35, "attack": 14, "defense": 8, "speed": 0, "experience": 20, "credits": 15, "rarity": "common" },
|
||||
{ "id": "mining_drone", "name": "Mining Drone", "health": 20, "attack": 8, "defense": 3, "speed": 5, "experience": 12, "credits": 8, "rarity": "common" },
|
||||
{ "id": "rock_creature", "name": "Rock Creature", "health": 45, "attack": 6, "defense": 12, "speed": 3, "experience": 18, "credits": 10, "rarity": "common" },
|
||||
{ "id": "alien_guardian", "name": "Alien Guardian", "health": 50, "attack": 8, "defense": 5, "speed": 6, "experience": 25, "credits": 15, "rarity": "common" },
|
||||
{ "id": "ancient_drone", "name": "Ancient Drone", "health": 30, "attack": 12, "defense": 2, "speed": 10, "experience": 20, "credits": 10, "rarity": "uncommon" },
|
||||
{ "id": "crystal_golem", "name": "Crystal Golem", "health": 80, "attack": 6, "defense": 10, "speed": 4, "experience": 35, "credits": 25, "rarity": "rare" },
|
||||
{ "id": "security_drone", "name": "Security Drone", "health": 35, "attack": 14, "defense": 4, "speed": 10, "experience": 22, "credits": 15, "rarity": "uncommon" },
|
||||
{ "id": "corrupted_ai", "name": "Corrupted AI", "health": 60, "attack": 20, "defense": 2, "speed": 15, "experience": 40, "credits": 30, "rarity": "rare" },
|
||||
{ "id": "energy_being", "name": "Energy Being", "health": 55, "attack": 22, "defense": 3, "speed": 18, "experience": 45, "credits": 35, "rarity": "epic" },
|
||||
{ "id": "phase_shifter", "name": "Phase Shifter", "health": 50, "attack": 25, "defense": 5, "speed": 20, "experience": 50, "credits": 40, "rarity": "epic" },
|
||||
{ "id": "quantum_entity", "name": "Quantum Entity", "health": 70, "attack": 35, "defense": 5, "speed": 20, "experience": 60, "credits": 50, "rarity": "legendary" }
|
||||
]
|
||||
@ -1,50 +0,0 @@
|
||||
{
|
||||
"id": "nebula_anomaly",
|
||||
"name": "Nebula Anomaly",
|
||||
"category": "endgame",
|
||||
"description": "Strange energy anomalies deep in uncharted space",
|
||||
"difficulty": "extreme",
|
||||
"minLevel": 8,
|
||||
"roomCount": [
|
||||
7,
|
||||
10
|
||||
],
|
||||
"energyCost": 30,
|
||||
"rewardMultiplier": 2.0,
|
||||
"enemyPool": [
|
||||
"energy_being",
|
||||
"phase_shifter",
|
||||
"quantum_entity"
|
||||
],
|
||||
"bossId": "quantum_entity",
|
||||
"rewards": {
|
||||
"creditsMin": 800,
|
||||
"creditsMax": 2000,
|
||||
"experienceMin": 600,
|
||||
"experienceMax": 1200
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-star",
|
||||
"color": "#cc44ff"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "wp_nebula_dawn",
|
||||
"weight": 10,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "wp_deep_ocean",
|
||||
"weight": 8,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "xp_booster",
|
||||
"weight": 60,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
{
|
||||
"id": "pirate_lair",
|
||||
"name": "Pirate Lair",
|
||||
"category": "combat",
|
||||
"description": "Dangerous pirate hideouts packed with loot",
|
||||
"difficulty": "easy",
|
||||
"minLevel": 1,
|
||||
"roomCount": [
|
||||
4,
|
||||
6
|
||||
],
|
||||
"energyCost": 15,
|
||||
"rewardMultiplier": 1.0,
|
||||
"enemyPool": [
|
||||
"space_pirate",
|
||||
"pirate_captain",
|
||||
"security_drone"
|
||||
],
|
||||
"bossId": "pirate_captain",
|
||||
"rewards": {
|
||||
"creditsMin": 100,
|
||||
"creditsMax": 300,
|
||||
"experienceMin": 80,
|
||||
"experienceMax": 160
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-skull-crossbones",
|
||||
"color": "#cc4444"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "wp_lava_forge",
|
||||
"weight": 6,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "health_kit_large",
|
||||
"weight": 40,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
},
|
||||
{
|
||||
"itemId": "fuel_cell",
|
||||
"weight": 50,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
{
|
||||
"id": "void_rift",
|
||||
"name": "Void Rift",
|
||||
"category": "endgame",
|
||||
"description": "A tear in space-time inhabited by entities beyond comprehension",
|
||||
"difficulty": "extreme",
|
||||
"minLevel": 12,
|
||||
"roomCount": [
|
||||
8,
|
||||
12
|
||||
],
|
||||
"energyCost": 40,
|
||||
"rewardMultiplier": 2.5,
|
||||
"enemyPool": [
|
||||
"quantum_entity",
|
||||
"energy_being",
|
||||
"phase_shifter"
|
||||
],
|
||||
"bossId": "quantum_entity",
|
||||
"rewards": {
|
||||
"creditsMin": 1500,
|
||||
"creditsMax": 4000,
|
||||
"experienceMin": 1000,
|
||||
"experienceMax": 2000
|
||||
},
|
||||
"ui": {
|
||||
"icon": "fa-circle-radiation",
|
||||
"color": "#ff4488"
|
||||
},
|
||||
"lootTable": [
|
||||
{
|
||||
"itemId": "wp_void_rift",
|
||||
"weight": 5,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "room_vault",
|
||||
"weight": 3,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "xp_booster",
|
||||
"weight": 50,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
},
|
||||
{
|
||||
"itemId": "credit_multiplier",
|
||||
"weight": 30,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "quantum_hull_plate",
|
||||
"weight": 2,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "void_capacitor",
|
||||
"weight": 8,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 2
|
||||
},
|
||||
{
|
||||
"itemId": "advanced_circuit_board",
|
||||
"weight": 12,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 3
|
||||
},
|
||||
{
|
||||
"itemId": "phase_alloy",
|
||||
"weight": 20,
|
||||
"qtyMin": 2,
|
||||
"qtyMax": 5
|
||||
},
|
||||
{
|
||||
"itemId": "chrono_alloy",
|
||||
"weight": 15,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 3
|
||||
},
|
||||
{
|
||||
"itemId": "sniper_epic",
|
||||
"weight": 4,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "laser_bow_epic",
|
||||
"weight": 4,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "helmet_tactical_epic",
|
||||
"weight": 6,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
},
|
||||
{
|
||||
"itemId": "body_exosuit_epic",
|
||||
"weight": 3,
|
||||
"qtyMin": 1,
|
||||
"qtyMax": 1
|
||||
}
|
||||
],
|
||||
"bossGuaranteedRare": true
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "alien_guardian",
|
||||
"name": "Alien Guardian",
|
||||
"rarity": "common",
|
||||
"health": 50,
|
||||
"attack": 8,
|
||||
"defense": 5,
|
||||
"speed": 6,
|
||||
"experience": 25,
|
||||
"credits": 15,
|
||||
"dropTable": [
|
||||
"alien_fauna_sample",
|
||||
"iron_ore"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "ancient_drone",
|
||||
"name": "Ancient Drone",
|
||||
"rarity": "uncommon",
|
||||
"health": 30,
|
||||
"attack": 12,
|
||||
"defense": 2,
|
||||
"speed": 9,
|
||||
"experience": 20,
|
||||
"credits": 10,
|
||||
"dropTable": [
|
||||
"basic_circuit",
|
||||
"alien_fauna_sample"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "corrupted_ai",
|
||||
"name": "Corrupted AI",
|
||||
"rarity": "rare",
|
||||
"health": 60,
|
||||
"attack": 20,
|
||||
"defense": 2,
|
||||
"speed": 15,
|
||||
"experience": 40,
|
||||
"credits": 30,
|
||||
"dropTable": [
|
||||
"processor_circuit",
|
||||
"logic_circuit"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "crystal_golem",
|
||||
"name": "Crystal Golem",
|
||||
"rarity": "rare",
|
||||
"health": 80,
|
||||
"attack": 6,
|
||||
"defense": 10,
|
||||
"speed": 4,
|
||||
"experience": 35,
|
||||
"credits": 25,
|
||||
"dropTable": [
|
||||
"flux_crystal",
|
||||
"titanium_ore"
|
||||
]
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "defense_turret",
|
||||
"name": "Defense Turret",
|
||||
"health": 35,
|
||||
"attack": 14,
|
||||
"defense": 8,
|
||||
"speed": 0,
|
||||
"experience": 20,
|
||||
"credits": 15,
|
||||
"rarity": "common"
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "energy_being",
|
||||
"name": "Energy Being",
|
||||
"rarity": "epic",
|
||||
"health": 55,
|
||||
"attack": 22,
|
||||
"defense": 3,
|
||||
"speed": 18,
|
||||
"experience": 45,
|
||||
"credits": 35,
|
||||
"dropTable": [
|
||||
"void_crystal",
|
||||
"quantum_circuit"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "mining_drone",
|
||||
"name": "Mining Drone",
|
||||
"rarity": "common",
|
||||
"health": 20,
|
||||
"attack": 8,
|
||||
"defense": 3,
|
||||
"speed": 5,
|
||||
"experience": 12,
|
||||
"credits": 8,
|
||||
"dropTable": [
|
||||
"iron_ore",
|
||||
"copper_ore"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "phase_shifter",
|
||||
"name": "Phase Shifter",
|
||||
"rarity": "epic",
|
||||
"health": 50,
|
||||
"attack": 25,
|
||||
"defense": 4,
|
||||
"speed": 22,
|
||||
"experience": 55,
|
||||
"credits": 45,
|
||||
"dropTable": [
|
||||
"phase_crystal",
|
||||
"void_crystal"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "pirate_captain",
|
||||
"name": "Pirate Captain",
|
||||
"rarity": "uncommon",
|
||||
"health": 40,
|
||||
"attack": 15,
|
||||
"defense": 6,
|
||||
"speed": 12,
|
||||
"experience": 30,
|
||||
"credits": 20,
|
||||
"dropTable": [
|
||||
"steel_alloy",
|
||||
"iron_ore"
|
||||
]
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"id": "quantum_entity",
|
||||
"name": "Quantum Entity",
|
||||
"rarity": "legendary",
|
||||
"health": 70,
|
||||
"attack": 35,
|
||||
"defense": 5,
|
||||
"speed": 20,
|
||||
"experience": 60,
|
||||
"credits": 50,
|
||||
"dropTable": [
|
||||
"void_crystal",
|
||||
"neutronium_shard",
|
||||
"dimensional_logic_circuit"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "rock_creature",
|
||||
"name": "Rock Creature",
|
||||
"rarity": "common",
|
||||
"health": 30,
|
||||
"attack": 7,
|
||||
"defense": 8,
|
||||
"speed": 3,
|
||||
"experience": 14,
|
||||
"credits": 9,
|
||||
"dropTable": [
|
||||
"iron_ore",
|
||||
"carbon_ingot"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "security_drone",
|
||||
"name": "Security Drone",
|
||||
"rarity": "uncommon",
|
||||
"health": 35,
|
||||
"attack": 14,
|
||||
"defense": 4,
|
||||
"speed": 10,
|
||||
"experience": 22,
|
||||
"credits": 15,
|
||||
"dropTable": [
|
||||
"basic_circuit",
|
||||
"copper_ore"
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "space_pirate",
|
||||
"name": "Space Pirate",
|
||||
"rarity": "common",
|
||||
"health": 25,
|
||||
"attack": 10,
|
||||
"defense": 3,
|
||||
"speed": 8,
|
||||
"experience": 15,
|
||||
"credits": 12,
|
||||
"dropTable": [
|
||||
"iron_ore",
|
||||
"bandage"
|
||||
]
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "training_drone",
|
||||
"name": "Training Drone",
|
||||
"rarity": "common",
|
||||
"health": 10,
|
||||
"attack": 5,
|
||||
"defense": 2,
|
||||
"speed": 6,
|
||||
"experience": 5,
|
||||
"credits": 3,
|
||||
"dropTable": []
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "virus_program",
|
||||
"name": "Virus Program",
|
||||
"rarity": "rare",
|
||||
"health": 45,
|
||||
"attack": 18,
|
||||
"defense": 1,
|
||||
"speed": 20,
|
||||
"experience": 38,
|
||||
"credits": 28,
|
||||
"dropTable": [
|
||||
"processor_circuit",
|
||||
"stealth_circuit"
|
||||
]
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
{
|
||||
"templates": {
|
||||
"armour": {
|
||||
"id": "backpack_basic_common",
|
||||
"name": "Basic Backpack",
|
||||
"type": "armour",
|
||||
"slot": "backpack",
|
||||
"rarity": "common",
|
||||
"price": 500,
|
||||
"currency": "credits",
|
||||
"description": "Lightweight backpack with extra inventory slots",
|
||||
"texture": "assets/gso/textures/armour/backpack/backpack_basic_common.png",
|
||||
"stats": {
|
||||
"health": 10,
|
||||
"resistance": 0.02,
|
||||
"defenceRating": 1,
|
||||
"reflectChance": 0.0,
|
||||
"extraSlots": 4
|
||||
},
|
||||
"categories": [
|
||||
"shop",
|
||||
"dungeon_loot"
|
||||
],
|
||||
"requirements": {
|
||||
"level": 1
|
||||
},
|
||||
"stackable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
{
|
||||
"templates": {
|
||||
"armour": {
|
||||
"id": "backpack_field_rare",
|
||||
"texture": "assets/gso/textures/armour/backpack/backpack_field_rare.png",
|
||||
"stats": {
|
||||
"health": 40,
|
||||
"resistance": 0.08,
|
||||
"defenceRating": 3,
|
||||
"reflectChance": 0.01
|
||||
},
|
||||
"name": "Backpack Field Rare",
|
||||
"type": "armour",
|
||||
"rarity": "rare",
|
||||
"price": 3000,
|
||||
"currency": "credits",
|
||||
"description": "Backpack Field Rare.",
|
||||
"categories": [
|
||||
"shop",
|
||||
"dungeon_loot"
|
||||
],
|
||||
"requirements": {
|
||||
"level": 5
|
||||
},
|
||||
"stackable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
{
|
||||
"templates": {
|
||||
"armour": {
|
||||
"id": "backpack_reactor_epic",
|
||||
"texture": "assets/gso/textures/armour/backpack/backpack_reactor_epic.png",
|
||||
"stats": {
|
||||
"health": 70,
|
||||
"resistance": 0.15,
|
||||
"defenceRating": 6,
|
||||
"reflectChance": 0.03
|
||||
},
|
||||
"name": "Backpack Reactor Epic",
|
||||
"type": "armour",
|
||||
"rarity": "epic",
|
||||
"price": 8000,
|
||||
"currency": "credits",
|
||||
"description": "Backpack Reactor Epic.",
|
||||
"categories": [
|
||||
"shop",
|
||||
"dungeon_loot"
|
||||
],
|
||||
"requirements": {
|
||||
"level": 10
|
||||
},
|
||||
"stackable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"templates": {
|
||||
"armour": {
|
||||
"id": "body_basic_common",
|
||||
"name": "Basic Body Armour",
|
||||
"type": "armour",
|
||||
"slot": "body",
|
||||
"rarity": "common",
|
||||
"price": 600,
|
||||
"currency": "credits",
|
||||
"description": "Standard-issue protection for rookie pilots",
|
||||
"texture": "assets/gso/textures/armour/body/body_basic_common.png",
|
||||
"stats": {
|
||||
"health": 60,
|
||||
"resistance": 0.1,
|
||||
"defenceRating": 4,
|
||||
"reflectChance": 0.0
|
||||
},
|
||||
"categories": [
|
||||
"shop",
|
||||
"dungeon_loot"
|
||||
],
|
||||
"requirements": {
|
||||
"level": 1
|
||||
},
|
||||
"stackable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user