updated more files

This commit is contained in:
Robert MacRae 2026-03-10 11:20:02 -03:00
parent 1495c41e43
commit 35eafd6026
639 changed files with 4338 additions and 67480 deletions

21
API/.env.example Normal file
View File

@ -0,0 +1,21 @@
# 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

View File

@ -37,8 +37,22 @@ 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: ["https://galaxystrike.online", "https://api.korvarix.com", "http://api.korvarix.com:3001", "https://dev.gameserver.galaxystrike.online"],
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' }));

File diff suppressed because it is too large Load Diff

View File

@ -169,6 +169,13 @@ class GameInitializer {
// Player stat update events
this.socket.on('player_stat_update', (data) => {
// Check for level-up
const oldLevel = this.serverPlayerData?.stats?.level || 0;
const newLevel = data.stats?.level || data.level || 0;
if (newLevel > oldLevel && oldLevel > 0) {
this.showNotification(`🎉 Level Up! Commander Level ${newLevel}`, 'success');
if (window.GSO_Dashboard) GSO_Dashboard.refresh(this.serverPlayerData);
}
console.log('[GAME INITIALIZER] Player stat update:', data);
this.onPlayerStatUpdate(data);
});
@ -201,10 +208,117 @@ class GameInitializer {
this.onShopItemsReceived(data);
});
this.socket.on('season_data', (data) => {
if (!data?.active) return;
const s = data.season;
const banner = document.getElementById('season-banner');
if (banner) {
banner.style.display = 'flex';
const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; };
set('season-icon', s.icon||'🌑');
set('season-name', `Season ${s.id}: ${s.name}`);
set('season-desc', s.description||'');
set('season-eta', `${s.daysLeft} days remaining (${s.progress}%)`);
set('season-score', data.myScore ? `Your score: ${data.myScore.toLocaleString()}` : '');
}
});
this.socket.on('galaxy_event_data', (data) => {
if (!data?.active) return;
const ev = data.event;
const banner = document.getElementById('galaxy-event-banner');
if (banner) {
banner.style.display = 'flex';
const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; };
set('gev-icon', ev.icon||'👾');
set('gev-name', ev.name||'Galaxy Event');
set('gev-desc', ev.desc||'');
set('gev-eta', ev.etaLabel||'');
set('gev-participants', `${ev.participantCount||0} commanders participating`);
}
});
this.socket.on('resource_update', (data) => {
// Keep serverPlayerData.resources in sync
if (this.serverPlayerData && data.resources) {
this.serverPlayerData.resources = data.resources;
}
if (window.GSO_Resources) GSO_Resources.update(data);
});
this.socket.on('itemDetailsReceived', (data) => {
console.log('[GAME INITIALIZER] Item details received:', data);
this.onItemDetailsReceived(data);
});
// ── Crafting (GDD §11) ────────────────────────────────────────────
this.socket.on('recipes_data', (data) => {
console.log('[CRAFTING] Recipes received:', Array.isArray(data) ? data.length : '(map)');
if (typeof GSO_Crafting !== 'undefined') GSO_Crafting.onRecipesData(data);
});
this.socket.on('craft_result', (data) => {
if (typeof GSO_Crafting !== 'undefined') GSO_Crafting.onCraftResult(data);
else if (data.success) {
if (typeof showNotification === 'function') showNotification('Item crafted!', 'success');
} else {
if (typeof showNotification === 'function') showNotification(data.error || 'Craft failed', 'error');
}
});
this.socket.on('inventory_update', (data) => {
if (this.serverPlayerData) {
this.serverPlayerData.inventory = data;
}
if (typeof GSO_Inventory !== 'undefined' && GSO_Inventory.render) {
GSO_Inventory.render(this.serverPlayerData);
}
});
// ── Ship Modules (GDD §7.3) ───────────────────────────────────────
this.socket.on('ship_modules_data', (data) => {
if (typeof GSO_Modules !== 'undefined') GSO_Modules.onModulesData(data);
});
this.socket.on('equip_result', (data) => {
if (typeof GSO_Modules !== 'undefined') GSO_Modules.onEquipResult(data);
else if (data.success) {
if (typeof showNotification === 'function') showNotification('Module equipped!', 'success');
} else {
if (typeof showNotification === 'function') showNotification(data.error || 'Equip failed', 'error');
}
});
// ── PvP Challenges (GDD §9.4) ─────────────────────────────────────
this.socket.on('pvp_challenge_received', (data) => {
if (typeof GSO_PvP !== 'undefined') GSO_PvP.onChallengeReceived(data);
else {
const accept = confirm(`${data.challengerName} challenges you to PvP! Accept?`);
if (accept) this.socket.emit('pvp_accept', { challengeId: data.challengeId });
else this.socket.emit('pvp_decline', { challengeId: data.challengeId });
}
});
this.socket.on('pvp_challenge_sent', (data) => {
if (typeof showNotification === 'function') showNotification(data.message || 'Challenge sent!', 'info');
});
this.socket.on('pvp_result', (data) => {
if (typeof GSO_PvP !== 'undefined') GSO_PvP.onPvpResult(data);
else if (data.youWon) {
if (typeof showNotification === 'function') showNotification(`PvP Victory! +${data.creditsWon} credits`, 'success');
} else if (!data.declined) {
if (typeof showNotification === 'function') showNotification('PvP Defeat!', 'error');
}
});
this.socket.on('pvp_challenge_expired', (data) => {
if (typeof showNotification === 'function') showNotification('PvP challenge expired.', 'warning');
});
this.socket.on('pvp_declined', (data) => {
if (typeof showNotification === 'function') showNotification(`${data.targetName} declined your challenge.`, 'warning');
});
// ── Season Leaderboard (GDD §20.3) ────────────────────────────────
this.socket.on('season_leaderboard_data', (data) => {
if (typeof GSO_Leaderboard !== 'undefined' && GSO_Leaderboard.onSeasonData) {
GSO_Leaderboard.onSeasonData(data);
}
});
}
onSocketConnected() {

View File

@ -1051,7 +1051,7 @@ class UIManager {
const oldTab = this.currentTab;
// Update navigation buttons
const navButtons = document.querySelectorAll('.nav-btn');
const navButtons = document.querySelectorAll('.nav-btn, .bottom-nav-btn, .nav-drawer-btn');
let navButtonsUpdated = 0;
navButtons.forEach(btn => {

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

View File

@ -1,228 +0,0 @@
name: Build Client for All Platforms
on:
push:
branches: [ main, develop ]
paths:
- 'Client/**'
pull_request:
branches: [ main ]
paths:
- 'Client/**'
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: Client/package-lock.json
- name: Install system dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxcomposite-dev libxdamage-dev libxrandr-dev libgbm-dev libxss1 libasound2-dev
- name: Install system dependencies (macOS)
if: matrix.os == 'macos-latest'
run: |
# Install Xcode command line tools if not present
xcode-select --install 2>/dev/null || true
- name: Install system dependencies (Windows)
if: matrix.os == 'windows-latest'
run: |
# Windows builds typically have required dependencies pre-installed
# Ensure chocolatey is available for any additional packages
choco --version || echo "Chocolatey not available"
- name: Install dependencies
working-directory: ./Client
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
npm ci --include=dev
else
npm ci
fi
shell: bash
- name: Clean previous builds (Windows)
if: matrix.os == 'windows-latest'
working-directory: ./Client
run: if exist dist rmdir /s /q dist
shell: cmd
- name: Clean previous builds (Unix)
if: matrix.os != 'windows-latest'
working-directory: ./Client
run: rm -rf dist
shell: bash
- name: Build for Linux
if: matrix.os == 'ubuntu-latest'
working-directory: ./Client
run: npm run build-linux
- name: Build for Windows
if: matrix.os == 'windows-latest'
working-directory: ./Client
run: npm run build-win
- name: Build for macOS
if: matrix.os == 'macos-latest'
working-directory: ./Client
run: npm run build-mac
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: client-${{ matrix.os }}
path: |
Client/dist/*.exe
Client/dist/*.msi
Client/dist/*.dmg
Client/dist/*.zip
Client/dist/*.AppImage
Client/dist/*.deb
Client/dist/*.rpm
Client/dist/*.app
retention-days: 30
continue-on-error: true
package:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create release directory
run: mkdir -p release
- name: Organize executables by platform
run: |
# Create platform directories
mkdir -p release/Windows
mkdir -p release/macOS
mkdir -p release/Linux
# Windows executables - copy directly from dist
if [ -d "artifacts/client-windows-latest" ]; then
cp artifacts/client-windows-latest/*.exe release/Windows/ 2>/dev/null || true
cp artifacts/client-windows-latest/*.msi release/Windows/ 2>/dev/null || true
fi
# macOS executables - copy directly from dist
if [ -d "artifacts/client-macos-latest" ]; then
cp artifacts/client-macos-latest/*.dmg release/macOS/ 2>/dev/null || true
cp artifacts/client-macos-latest/*.zip release/macOS/ 2>/dev/null || true
# Handle .app bundles (directories)
if [ -d "artifacts/client-macos-latest" ]; then
find artifacts/client-macos-latest -maxdepth 1 -name "*.app" -type d -exec cp -r {} release/macOS/ \; 2>/dev/null || true
fi
fi
# Linux executables - copy directly from dist
if [ -d "artifacts/client-ubuntu-latest" ]; then
cp artifacts/client-ubuntu-latest/*.AppImage release/Linux/ 2>/dev/null || true
cp artifacts/client-ubuntu-latest/*.deb release/Linux/ 2>/dev/null || true
cp artifacts/client-ubuntu-latest/*.rpm release/Linux/ 2>/dev/null || true
fi
# List what we actually got
echo "=== Executables Found ==="
find release -type f -name "*" -o -name "*.app" | head -20 || true
# Create platform info file
cat > release/README.txt << EOF
Galaxy Strike Online - Multi-Platform Client
==========================================
Windows:
- Run the .exe installer or portable executable
macOS:
- Open the .dmg file and drag to Applications
- Or extract the .zip and run the .app
Linux:
- Make the .AppImage executable: chmod +x *.AppImage
- Or install the .deb package: sudo dpkg -i *.deb
Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
Commit: ${{ github.sha }}
EOF
- name: Create all-in-one zip
run: |
cd release
zip -r ../Galaxy-Strike-Online-Client-${{ github.sha }}.zip .
- name: Upload all-in-one zip
uses: actions/upload-artifact@v4
with:
name: galaxy-strike-online-client-all-platforms
path: Galaxy-Strike-Online-Client-${{ github.sha }}.zip
retention-days: 90
- name: Create Release (Main Branch)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: softprops/action-gh-release@v1
with:
files: Galaxy-Strike-Online-Client-${{ github.sha }}.zip
name: Galaxy Strike Online Client - Latest
body: |
Latest multi-platform client release for Galaxy Strike Online.
Includes:
- Windows (NSIS installer + Portable)
- macOS (DMG + ZIP)
- Linux (AppImage + Debian package)
Commit: ${{ github.sha }}
Build Date: ${{ github.event.head_commit.timestamp }}
**Download the all-in-one zip file below for all platforms.**
draft: false
prerelease: false
tag_name: latest
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release (Tags)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: Galaxy-Strike-Online-Client-${{ github.sha }}.zip
name: Galaxy Strike Online Client - ${{ github.ref_name }}
body: |
Multi-platform client release for Galaxy Strike Online.
Includes:
- Windows (NSIS installer + Portable)
- macOS (DMG + ZIP)
- Linux (AppImage + Debian package)
Commit: ${{ github.sha }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,139 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# 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-*

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
{
"name": "galaxystrikeonline-server",
"version": "1.0.0",
"description": "Galaxy Strike Online - Server Backend",
"license": "MIT",
"author": "Korvarix Studios",
"type": "commonjs",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"debug": "node --inspect server.js",
"test": "jest",
"migrate": "node scripts/migrate.js",
"seed": "node scripts/seed.js"
},
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
"dependencies": {
"express": "^4.18.2",
"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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,234 +0,0 @@
const express = require('express');
const http = require('http');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('rate-limiter-flexible');
require('dotenv').config();
const logger = require('./utils/logger');
const connectDB = require('./config/database');
const authRoutes = require('./routes/auth');
const serverRoutes = require('./routes/servers');
const { 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());
app.use(cors({
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://api.korvarix.com:3001", "https://dev.gameserver.galaxystrike.online"],
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Static file serving
app.use(express.static('../Website/dist'));
// Rate limiting (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);
// 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 };

View File

@ -1,272 +0,0 @@
const logger = require('../utils/logger');
const { getGameSystem } = require('../systems/GameSystem');
const Player = require('../models/Player');
class SocketHandlers {
constructor(io) {
this.io = io;
this.connectedUsers = new Map(); // userId -> socket.id
this.userSockets = new Map(); // socket.id -> userId
}
handleConnection(socket) {
logger.info(`Client connected: ${socket.id}`);
// Authentication
socket.on('authenticate', async (token) => {
try {
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
const player = await Player.findOne({ userId: decoded.userId });
if (!player) {
socket.emit('auth_error', { error: 'Player not found' });
return;
}
// Store user connection
this.connectedUsers.set(decoded.userId, socket.id);
this.userSockets.set(socket.id, decoded.userId);
socket.userId = decoded.userId;
socket.emit('authenticated', { userId: decoded.userId });
logger.info(`User authenticated: ${decoded.userId}`);
// Join user to their current server if any
if (player.currentServer) {
socket.join(player.currentServer);
this.broadcastToServer(player.currentServer, 'user_joined', {
userId: decoded.userId,
username: player.username
});
}
} catch (error) {
logger.error('Authentication error:', error);
socket.emit('auth_error', { error: 'Invalid token' });
}
});
// Server management
socket.on('join_server', async (data) => {
try {
if (!socket.userId) {
socket.emit('error', { error: 'Not authenticated' });
return;
}
const gameSystem = getGameSystem();
const server = await gameSystem.joinServer(data.serverId, socket.userId);
// Update player's current server
await Player.findOneAndUpdate(
{ userId: socket.userId },
{ currentServer: data.serverId }
);
// Join socket room
socket.join(data.serverId);
socket.emit('server_joined', { server });
this.broadcastToServer(data.serverId, 'user_joined', {
userId: socket.userId,
serverId: data.serverId
});
logger.info(`User ${socket.userId} joined server ${data.serverId}`);
} catch (error) {
logger.error('Error joining server:', error);
socket.emit('error', { error: error.message });
}
});
socket.on('leave_server', async (data) => {
try {
if (!socket.userId) {
socket.emit('error', { error: 'Not authenticated' });
return;
}
const gameSystem = getGameSystem();
const server = await gameSystem.leaveServer(data.serverId, socket.userId);
// Update player's current server
await Player.findOneAndUpdate(
{ userId: socket.userId },
{ currentServer: null }
);
// Leave socket room
socket.leave(data.serverId);
socket.emit('server_left', { server });
this.broadcastToServer(data.serverId, 'user_left', {
userId: socket.userId,
serverId: data.serverId
});
logger.info(`User ${socket.userId} left server ${data.serverId}`);
} catch (error) {
logger.error('Error leaving server:', error);
socket.emit('error', { error: error.message });
}
});
// Game actions
socket.on('game_action', async (data) => {
try {
if (!socket.userId) {
socket.emit('error', { error: 'Not authenticated' });
return;
}
const gameSystem = getGameSystem();
const result = await gameSystem.processGameAction(socket.userId, data);
socket.emit('action_result', { action: data.type, result });
// Broadcast relevant actions to server
if (data.broadcast && socket.userId) {
const player = await Player.findOne({ userId: socket.userId });
if (player && player.currentServer) {
this.broadcastToServer(player.currentServer, 'user_action', {
userId: socket.userId,
username: player.username,
action: data.type,
result
});
}
}
} catch (error) {
logger.error('Error processing game action:', error);
socket.emit('error', { error: error.message });
}
});
// Chat functionality
socket.on('send_message', async (data) => {
try {
if (!socket.userId) {
socket.emit('error', { error: 'Not authenticated' });
return;
}
const player = await Player.findOne({ userId: socket.userId });
if (!player || !player.currentServer) {
socket.emit('error', { error: 'Not in a server' });
return;
}
const message = {
userId: socket.userId,
username: player.username,
message: data.message,
timestamp: new Date(),
type: data.type || 'chat'
};
// Broadcast to server
this.broadcastToServer(player.currentServer, 'new_message', message);
logger.info(`Chat message from ${socket.userId} in server ${player.currentServer}`);
} catch (error) {
logger.error('Error sending message:', error);
socket.emit('error', { error: error.message });
}
});
// Real-time updates
socket.on('request_server_status', async () => {
try {
if (!socket.userId) {
socket.emit('error', { error: 'Not authenticated' });
return;
}
const player = await Player.findOne({ userId: socket.userId });
if (!player || !player.currentServer) {
socket.emit('server_status', { server: null });
return;
}
const gameSystem = getGameSystem();
const server = gameSystem.servers.get(player.currentServer);
if (server) {
const players = await Player.find({
userId: { $in: server.players }
}).select('userId username info.stats.level');
socket.emit('server_status', {
server: {
id: server.id,
name: server.name,
currentPlayers: server.players.length,
maxPlayers: server.maxPlayers,
players: players.map(p => ({
userId: p.userId,
username: p.username,
level: p.info.stats.level
}))
}
});
}
} catch (error) {
logger.error('Error getting server status:', error);
socket.emit('error', { error: error.message });
}
});
// Disconnection
socket.on('disconnect', async () => {
logger.info(`Client disconnected: ${socket.id}`);
const userId = this.userSockets.get(socket.id);
if (userId) {
// Remove from tracking
this.connectedUsers.delete(userId);
this.userSockets.delete(socket.id);
// Notify server if user was in one
const player = await Player.findOne({ userId });
if (player && player.currentServer) {
this.broadcastToServer(player.currentServer, 'user_disconnected', {
userId,
username: player.username
});
}
}
});
}
broadcastToServer(serverId, event, data) {
this.io.to(serverId).emit(event, data);
}
sendToUser(userId, event, data) {
const socketId = this.connectedUsers.get(userId);
if (socketId) {
this.io.to(socketId).emit(event, data);
}
}
broadcastToAll(event, data) {
this.io.emit(event, data);
}
getConnectedUsers() {
return Array.from(this.connectedUsers.keys());
}
getUserCount() {
return this.connectedUsers.size;
}
}
module.exports = SocketHandlers;

View File

@ -1,385 +0,0 @@
const logger = require('../utils/logger');
class EconomySystem {
constructor() {
this.shopItems = {
ships: [],
weapons: [],
armors: [],
materials: [],
consumables: []
};
this.dailyRewards = {
baseReward: 100,
consecutiveBonus: 50,
maxConsecutiveDays: 30
};
}
async initialize() {
logger.info('Initializing Economy System...');
// Initialize shop items
await this.initializeShopItems();
logger.info('Economy System initialized successfully');
}
async initializeShopItems() {
// Ships
this.shopItems.ships = [
// Starter Cruiser Variants
{
id: 'starter_cruiser_common',
name: 'Starter Cruiser',
type: 'ship',
rarity: 'common',
price: 5000,
currency: 'credits',
description: 'Reliable starter cruiser for new pilots',
texture: 'assets/textures/ships/starter_cruiser.png',
stats: { attack: 15, speed: 10, defense: 12, hull: 100 }
},
{
id: 'starter_cruiser_uncommon',
name: 'Starter Cruiser II',
type: 'ship',
rarity: 'uncommon',
price: 12000,
currency: 'credits',
description: 'Upgraded starter cruiser with enhanced systems',
texture: 'assets/textures/ships/starter_cruiser.png',
stats: { attack: 18, speed: 12, defense: 15, hull: 120 }
},
{
id: 'starter_cruiser_rare',
name: 'Starter Cruiser III',
type: 'ship',
rarity: 'rare',
price: 25000,
currency: 'credits',
description: 'Elite starter cruiser with advanced weaponry',
texture: 'assets/textures/ships/starter_cruiser.png',
stats: { attack: 22, speed: 14, defense: 18, hull: 140 }
},
{
id: 'starter_cruiser_epic',
name: 'Starter Cruiser IV',
type: 'ship',
rarity: 'epic',
price: 50000,
currency: 'credits',
description: 'Master starter cruiser with elite modifications',
texture: 'assets/textures/ships/starter_cruiser.png',
stats: { attack: 28, speed: 16, defense: 22, hull: 160 }
},
{
id: 'starter_cruiser_legendary',
name: 'Starter Cruiser V',
type: 'ship',
rarity: 'legendary',
price: 100000,
currency: 'credits',
description: 'Legendary starter cruiser with unparalleled performance',
texture: 'assets/textures/ships/starter_cruiser.png',
stats: { attack: 35, speed: 18, defense: 28, hull: 180 }
}
];
// Weapons
this.shopItems.weapons = [
// Starter Blaster Variants
{
id: 'starter_blaster_common',
name: 'Common Blaster',
type: 'weapon',
rarity: 'common',
price: 1000,
currency: 'credits',
description: 'Basic blaster for new pilots',
texture: 'assets/textures/weapons/starter_blaster.png',
stats: { damage: 10, fireRate: 2, range: 5, energy: 5 }
},
{
id: 'starter_blaster_uncommon',
name: 'Starter Blaster II',
type: 'weapon',
rarity: 'uncommon',
price: 2500,
currency: 'credits',
description: 'Improved blaster with better damage output',
texture: 'assets/textures/weapons/starter_blaster.png',
stats: { damage: 12, fireRate: 2.2, range: 5.5, energy: 6 }
},
{
id: 'starter_blaster_rare',
name: 'Starter Blaster III',
type: 'weapon',
rarity: 'rare',
price: 5000,
currency: 'credits',
description: 'Advanced blaster with enhanced capabilities',
texture: 'assets/textures/weapons/starter_blaster.png',
stats: { damage: 15, fireRate: 2.5, range: 6, energy: 7 }
},
{
id: 'starter_blaster_epic',
name: 'Starter Blaster IV',
type: 'weapon',
rarity: 'epic',
price: 10000,
currency: 'credits',
description: 'Elite blaster with superior performance',
texture: 'assets/textures/weapons/starter_blaster.png',
stats: { damage: 18, fireRate: 3, range: 6.5, energy: 8 }
},
{
id: 'starter_blaster_legendary',
name: 'Starter Blaster V',
type: 'weapon',
rarity: 'legendary',
price: 20000,
currency: 'credits',
description: 'Legendary starter blaster with ultimate power',
texture: 'assets/textures/weapons/starter_blaster.png',
stats: { damage: 22, fireRate: 4, range: 7, energy: 10 }
}
];
// Armors
this.shopItems.armors = [
// Basic Armor Variants
{
id: 'basic_armor_common',
name: 'Basic Armor',
type: 'armor',
rarity: 'common',
price: 1500,
currency: 'credits',
description: 'Light protection for beginners',
texture: 'assets/textures/armors/basic_armor.png',
stats: { defense: 5, durability: 20, weight: 2, energyShield: 0 }
},
{
id: 'basic_armor_uncommon',
name: 'Basic Armor II',
type: 'armor',
rarity: 'uncommon',
price: 4000,
currency: 'credits',
description: 'Improved basic armor with better durability',
texture: 'assets/textures/armors/basic_armor.png',
stats: { defense: 7, durability: 25, weight: 2.2, energyShield: 2 }
},
{
id: 'basic_armor_rare',
name: 'Basic Armor III',
type: 'armor',
rarity: 'rare',
price: 8000,
currency: 'credits',
description: 'Enhanced armor with energy shielding',
texture: 'assets/textures/armors/basic_armor.png',
stats: { defense: 10, durability: 30, weight: 2.5, energyShield: 5 }
},
{
id: 'basic_armor_epic',
name: 'Basic Armor IV',
type: 'armor',
rarity: 'epic',
price: 15000,
currency: 'credits',
description: 'Elite armor with advanced protection systems',
texture: 'assets/textures/armors/basic_armor.png',
stats: { defense: 15, durability: 35, weight: 3, energyShield: 10 }
},
{
id: 'basic_armor_legendary',
name: 'Basic Armor V',
type: 'armor',
rarity: 'legendary',
price: 30000,
currency: 'credits',
description: 'Legendary armor with ultimate protection',
texture: 'assets/textures/armors/basic_armor.png',
stats: { defense: 20, durability: 40, weight: 3.5, energyShield: 15 }
}
];
// Materials
this.shopItems.materials = [
{
id: 'iron_ore',
name: 'Iron Ore',
type: 'material',
rarity: 'common',
price: 50,
currency: 'credits',
description: 'Raw iron ore used for crafting basic weapons and armor',
stackable: true
},
{
id: 'copper_wire',
name: 'Copper Wire',
type: 'material',
rarity: 'common',
price: 75,
currency: 'credits',
description: 'Copper wiring used in electronic components',
stackable: true
},
{
id: 'energy_crystal',
name: 'Energy Crystal',
type: 'material',
rarity: 'uncommon',
price: 200,
currency: 'credits',
description: 'Crystallized energy used for powered equipment',
stackable: true
},
{
id: 'rare_metal',
name: 'Rare Metal',
type: 'material',
rarity: 'rare',
price: 500,
currency: 'credits',
description: 'Rare metallic alloy used for high-end crafting',
stackable: true
},
{
id: 'advanced_components',
name: 'Advanced Components',
type: 'material',
rarity: 'rare',
price: 1000,
currency: 'credits',
description: 'Sophisticated electronic components for advanced ship systems',
stackable: true
}
];
// Consumables
this.shopItems.consumables = [
{
id: 'health_kit',
name: 'Health Kit',
type: 'consumable',
rarity: 'common',
price: 100,
currency: 'credits',
description: 'A medical kit that restores health',
consumable: true,
effect: { health: 50 }
},
{
id: 'energy_pack',
name: 'Energy Pack',
type: 'consumable',
rarity: 'common',
price: 150,
currency: 'credits',
description: 'A pack that restores energy',
consumable: true,
effect: { energy: 25 }
},
{
id: 'repair_kit',
name: 'Repair Kit',
type: 'consumable',
rarity: 'uncommon',
price: 300,
currency: 'credits',
description: 'A kit that repairs ship damage',
consumable: true,
effect: { health: 100 }
}
];
logger.info(`Shop initialized with ${this.getTotalShopItems()} items`);
}
getTotalShopItems() {
return Object.values(this.shopItems).reduce((total, category) => total + category.length, 0);
}
getShopItems(category = null) {
if (category && this.shopItems[category]) {
return this.shopItems[category];
}
return this.shopItems;
}
getItem(itemId) {
for (const category of Object.values(this.shopItems)) {
const item = category.find(item => item.id === itemId);
if (item) return item;
}
return null;
}
purchaseItem(userId, itemId, quantity = 1) {
const item = this.getItem(itemId);
if (!item) {
throw new Error('Item not found in shop');
}
const totalCost = item.price * quantity;
return {
item,
quantity,
totalCost,
currency: item.currency
};
}
calculateDailyReward(consecutiveDays) {
const bonusMultiplier = Math.min(consecutiveDays - 1, this.dailyRewards.maxConsecutiveDays - 1);
const bonusAmount = bonusMultiplier * this.dailyRewards.consecutiveBonus;
const totalReward = this.dailyRewards.baseReward + bonusAmount;
return {
baseReward: this.dailyRewards.baseReward,
consecutiveBonus: bonusAmount,
totalReward,
consecutiveDays
};
}
getRandomShopItems(category, count = 6) {
const items = this.shopItems[category] || [];
const shuffled = [...items].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(count, items.length));
}
refreshShopInventory() {
logger.info('Refreshing shop inventory...');
// This would typically involve database operations
// For now, we'll just log the refresh
return true;
}
getShopStats() {
const stats = {
totalItems: this.getTotalShopItems(),
itemsByCategory: {},
averagePriceByCategory: {}
};
for (const [category, items] of Object.entries(this.shopItems)) {
stats.itemsByCategory[category] = items.length;
if (items.length > 0) {
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
stats.averagePriceByCategory[category] = Math.round(totalPrice / items.length);
}
}
return stats;
}
}
module.exports = EconomySystem;

View File

@ -1,293 +0,0 @@
const logger = require('../utils/logger');
const Player = require('../models/Player');
const Ship = require('../models/Ship');
const Inventory = require('../models/Inventory');
const Economy = require('./EconomySystem');
class GameSystem {
constructor() {
this.players = new Map();
this.servers = new Map();
this.economy = new Economy();
}
async initializeGameSystems() {
logger.info('Initializing server-side game systems...');
// Initialize economy system
await this.economy.initialize();
logger.info('Game systems initialized successfully');
}
// Player management
async createPlayer(userId, playerData) {
try {
const player = new Player({
userId,
...playerData,
createdAt: new Date(),
lastLogin: new Date()
});
await player.save();
this.players.set(userId, player);
logger.info(`Created new player for user: ${userId}`);
return player;
} catch (error) {
logger.error('Error creating player:', error);
throw error;
}
}
async loadPlayer(userId) {
try {
let player = this.players.get(userId);
if (!player) {
player = await Player.findOne({ userId }).populate('ships inventory');
if (player) {
this.players.set(userId, player);
}
}
return player;
} catch (error) {
logger.error('Error loading player:', error);
throw error;
}
}
async savePlayer(userId) {
try {
const player = this.players.get(userId);
if (player) {
await player.save();
logger.info(`Saved player data for user: ${userId}`);
}
} catch (error) {
logger.error('Error saving player:', error);
throw error;
}
}
// Ship management
async addShipToPlayer(userId, shipData) {
try {
const player = await this.loadPlayer(userId);
if (!player) {
throw new Error('Player not found');
}
const ship = new Ship({
...shipData,
userId,
acquiredAt: new Date()
});
await ship.save();
player.ships.push(ship._id);
await player.save();
logger.info(`Added ship ${ship.name} to player ${userId}`);
return ship;
} catch (error) {
logger.error('Error adding ship to player:', error);
throw error;
}
}
async equipShip(userId, shipId) {
try {
const player = await this.loadPlayer(userId);
if (!player) {
throw new Error('Player not found');
}
const ship = await Ship.findOne({ _id: shipId, userId });
if (!ship) {
throw new Error('Ship not found');
}
// Unequip current ship
if (player.currentShip) {
await Ship.findByIdAndUpdate(player.currentShip, { isEquipped: false });
}
// Equip new ship
ship.isEquipped = true;
await ship.save();
player.currentShip = ship._id;
await player.save();
logger.info(`Equipped ship ${ship.name} for player ${userId}`);
return ship;
} catch (error) {
logger.error('Error equipping ship:', error);
throw error;
}
}
// Server management
async createServer(serverData) {
try {
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const server = {
id: serverId,
...serverData,
createdAt: new Date(),
players: [],
status: 'active'
};
this.servers.set(serverId, server);
logger.info(`Created new server: ${serverId}`);
return server;
} catch (error) {
logger.error('Error creating server:', error);
throw error;
}
}
async joinServer(serverId, userId) {
try {
const server = this.servers.get(serverId);
if (!server) {
throw new Error('Server not found');
}
if (server.players.length >= server.maxPlayers) {
throw new Error('Server is full');
}
if (!server.players.includes(userId)) {
server.players.push(userId);
}
logger.info(`Player ${userId} joined server ${serverId}`);
return server;
} catch (error) {
logger.error('Error joining server:', error);
throw error;
}
}
async leaveServer(serverId, userId) {
try {
const server = this.servers.get(serverId);
if (!server) {
throw new Error('Server not found');
}
server.players = server.players.filter(id => id !== userId);
if (server.players.length === 0) {
this.servers.delete(serverId);
logger.info(`Server ${serverId} deleted (no players)`);
}
logger.info(`Player ${userId} left server ${serverId}`);
return server;
} catch (error) {
logger.error('Error leaving server:', error);
throw error;
}
}
getServerList() {
return Array.from(this.servers.values()).map(server => ({
id: server.id,
name: server.name,
type: server.type,
maxPlayers: server.maxPlayers,
currentPlayers: server.players.length,
status: server.status,
region: server.region,
createdAt: server.createdAt
}));
}
// Game actions
async processGameAction(userId, actionData) {
try {
const player = await this.loadPlayer(userId);
if (!player) {
throw new Error('Player not found');
}
switch (actionData.type) {
case 'dungeon_enter':
return await this.handleDungeonEnter(player, actionData);
case 'ship_upgrade':
return await this.handleShipUpgrade(player, actionData);
case 'item_purchase':
return await this.handleItemPurchase(player, actionData);
case 'daily_reward':
return await this.handleDailyReward(player, actionData);
default:
throw new Error('Unknown action type');
}
} catch (error) {
logger.error('Error processing game action:', error);
throw error;
}
}
async handleDungeonEnter(player, data) {
// Dungeon logic will be implemented here
logger.info(`Player ${player.userId} entering dungeon`);
return { success: true, message: 'Dungeon entered' };
}
async handleShipUpgrade(player, data) {
// Ship upgrade logic will be implemented here
logger.info(`Player ${player.userId} upgrading ship`);
return { success: true, message: 'Ship upgraded' };
}
async handleItemPurchase(player, data) {
// Item purchase logic will be implemented here
logger.info(`Player ${player.userId} purchasing item`);
return { success: true, message: 'Item purchased' };
}
async handleDailyReward(player, data) {
// Daily reward logic will be implemented here
logger.info(`Player ${player.userId} claiming daily reward`);
return { success: true, message: 'Daily reward claimed' };
}
}
// Singleton instance
let gameSystem = null;
async function initializeGameSystems() {
if (!gameSystem) {
gameSystem = new GameSystem();
try {
await gameSystem.initializeGameSystems();
} catch (error) {
logger.error('Failed to initialize game systems:', error);
throw error;
}
}
return gameSystem;
}
function getGameSystem() {
if (!gameSystem) {
logger.warn('Game system not initialized. Call initializeGameSystems() first.');
return null;
}
return gameSystem;
}
module.exports = {
GameSystem,
initializeGameSystems,
getGameSystem
};

View File

@ -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');
});
});
});

View File

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

View File

@ -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 }
}

View File

@ -1,451 +0,0 @@
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const logger = require('./js/core/Logger');
console.log('[MAIN PROCESS] Electron main process starting...');
console.log('[MAIN PROCESS] Node.js version:', process.version);
console.log('[MAIN PROCESS] Electron version:', process.versions.electron);
console.log('[MAIN PROCESS] Platform:', process.platform);
console.log('[MAIN PROCESS] Current working directory:', process.cwd());
// Keep a global reference of the window object
let mainWindow;
function createWindow() {
console.log('[MAIN PROCESS] createWindow() called');
try {
console.log('[MAIN PROCESS] Creating BrowserWindow...');
// Create the browser window
mainWindow = new BrowserWindow({
width: 1200,
height: 832, // 800 + 32px for custom title bar
minWidth: 1200,
minHeight: 832,
maxWidth: 1200,
maxHeight: 832,
resizable: false,
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
webSecurity: true
},
icon: path.join(__dirname, 'assets/icon.png'),
show: false, // Don't show until ready-to-show
title: 'Galaxy Strike Online'
});
console.log('[MAIN PROCESS] BrowserWindow created successfully');
console.log('[MAIN PROCESS] Loading index.html...');
// Load the index.html file
mainWindow.loadFile('index.html');
console.log('[MAIN PROCESS] index.html loaded, setting up electronAPI...');
// Set up electronAPI after DOM is ready
mainWindow.webContents.on('dom-ready', () => {
console.log('[MAIN PROCESS] DOM is ready, setting up electronAPI...');
mainWindow.webContents.executeJavaScript(`
console.log('[RENDERER] Setting up electronAPI...');
window.electronAPI = {
minimizeWindow: () => require('electron').ipcRenderer.send('minimize-window'),
closeWindow: () => require('electron').ipcRenderer.send('close-window'),
toggleFullscreen: () => require('electron').ipcRenderer.send('toggle-fullscreen'),
log: (level, message, data) => require('electron').ipcRenderer.send('log-message', { level, message, data }),
createSaveFolders: (saveSlots) => require('electron').ipcRenderer.invoke('create-save-folders', saveSlots),
testFileAccess: (slotPath) => require('electron').ipcRenderer.invoke('test-file-access', slotPath),
saveGame: (slot, saveData) => require('electron').ipcRenderer.invoke('save-game', slot, saveData),
loadGame: (slot) => require('electron').ipcRenderer.invoke('load-game', slot),
getPath: (name) => require('electron').ipcRenderer.invoke('get-path', name),
deleteSaveFile: (slot) => require('electron').ipcRenderer.invoke('delete-save-file', slot)
};
console.log('[RENDERER] electronAPI setup completed');
`).then(() => {
console.log('[MAIN PROCESS] electronAPI setup completed');
}).catch((error) => {
console.error('[MAIN PROCESS] Failed to setup electronAPI:', error);
});
});
// Show window when ready
mainWindow.once('ready-to-show', () => {
console.log('[MAIN PROCESS] Window ready-to-show event fired');
mainWindow.show();
});
// Open DevTools in development
if (process.argv.includes('--dev')) {
console.log('[MAIN PROCESS] Opening DevTools...');
mainWindow.webContents.openDevTools();
}
// Handle window closed
mainWindow.on('closed', () => {
console.log('[MAIN PROCESS] Window closed event fired');
mainWindow = null;
});
// Handle renderer process crashes
mainWindow.webContents.on('render-process-gone', (event, details) => {
console.error('[MAIN PROCESS] Renderer process crashed:', details);
console.error('[MAIN PROCESS] Crash reason:', details.reason);
console.error('[MAIN PROCESS] Exit code:', details.exitCode);
});
// Handle renderer process unresponsive
mainWindow.webContents.on('unresponsive', () => {
console.warn('[MAIN PROCESS] Renderer process unresponsive');
});
// Handle renderer process responsive again
mainWindow.webContents.on('responsive', () => {
console.log('[MAIN PROCESS] Renderer process responsive again');
});
// Handle console messages from renderer
mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
console.log(`[RENDERER CONSOLE] [${level}] ${message} (line: ${line}, source: ${sourceId})`);
});
// Handle page load errors
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
console.error('[MAIN PROCESS] Page failed to load:', errorCode, errorDescription, validatedURL);
});
// Handle page load success
mainWindow.webContents.on('did-finish-load', () => {
console.log('[MAIN PROCESS] Page finished loading');
});
// Handle DOM ready
mainWindow.webContents.on('dom-ready', () => {
console.log('[MAIN PROCESS] DOM is ready');
});
// Handle external links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
console.log('[MAIN PROCESS] External link requested:', url);
shell.openExternal(url);
return { action: 'deny' };
});
console.log('[MAIN PROCESS] createWindow() completed successfully');
} catch (error) {
console.error('[MAIN PROCESS] Error in createWindow():', error);
console.error('[MAIN PROCESS] Error stack:', error.stack);
}
}
// IPC handlers for save operations
ipcMain.handle('create-save-folders', async (event, saveSlots) => {
console.log('[MAIN PROCESS] create-save-folders called with saveSlots:', saveSlots);
try {
const userDataPath = app.getPath('userData');
console.log('[MAIN PROCESS] userDataPath:', userDataPath);
const savesDir = path.join(userDataPath, 'saves');
console.log('[MAIN PROCESS] savesDir:', savesDir);
// Create main saves directory
if (!fs.existsSync(savesDir)) {
console.log('[MAIN PROCESS] Creating saves directory:', savesDir);
fs.mkdirSync(savesDir, { recursive: true });
console.log('[MAIN PROCESS] Saves directory created successfully');
} else {
console.log('[MAIN PROCESS] Saves directory already exists');
}
const paths = {
base: savesDir,
slots: []
};
// Create save slot directories
for (let i = 1; i <= saveSlots; i++) {
const slotDir = path.join(savesDir, `slot${i}`);
console.log(`[MAIN PROCESS] Checking/creating slot ${i} directory:`, slotDir);
if (!fs.existsSync(slotDir)) {
console.log(`[MAIN PROCESS] Creating slot ${i} directory`);
fs.mkdirSync(slotDir, { recursive: true });
// Create initial save info file
const saveInfo = {
slot: i,
created: new Date().toISOString(),
version: '1.0.0',
exists: false
};
const infoPath = path.join(slotDir, 'saveinfo.json');
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
console.log(`[MAIN PROCESS] Created save info for slot ${i}`);
} else {
console.log(`[MAIN PROCESS] Slot ${i} directory already exists`);
}
paths.slots.push(slotDir);
}
console.log('[MAIN PROCESS] Save folders created successfully, returning paths:', paths);
return { success: true, paths };
} catch (error) {
console.error('[MAIN PROCESS] Failed to create save folders:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('test-file-access', async (event, slotPath) => {
try {
const testFile = path.join(slotPath, 'access_test.txt');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('save-game', async (event, slot, saveData) => {
try {
const userDataPath = app.getPath('userData');
const savesDir = path.join(userDataPath, 'saves');
const slotDir = path.join(savesDir, `slot${slot}`);
// Save game data
const saveFilePath = path.join(slotDir, 'save.json');
fs.writeFileSync(saveFilePath, JSON.stringify(saveData, null, 2));
// Update save info
const infoPath = path.join(slotDir, 'saveinfo.json');
const saveInfo = {
slot: slot,
created: new Date().toISOString(),
lastSaved: new Date().toISOString(),
version: '1.0.0',
exists: true,
playTime: saveData.gameTime || 0
};
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
return { success: true };
} catch (error) {
console.error('Failed to save game:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('load-game', async (event, slot) => {
try {
const userDataPath = app.getPath('userData');
const savesDir = path.join(userDataPath, 'saves');
const slotDir = path.join(savesDir, `slot${slot}`);
const saveFilePath = path.join(slotDir, 'save.json');
if (fs.existsSync(saveFilePath)) {
const saveContent = fs.readFileSync(saveFilePath, 'utf8');
const saveData = JSON.parse(saveContent);
return { success: true, data: saveData };
} else {
return { success: false, error: 'Save file not found' };
}
} catch (error) {
console.error('Failed to load game:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-path', async (event, name) => {
try {
return app.getPath(name);
} catch (error) {
return null;
}
});
ipcMain.handle('delete-save-file', async (event, slot) => {
console.log('[MAIN PROCESS] delete-save-file called for slot:', slot);
try {
const userDataPath = app.getPath('userData');
const savesDir = path.join(userDataPath, 'saves');
const slotDir = path.join(savesDir, `slot${slot}`);
const saveFilePath = path.join(slotDir, 'save.json');
const infoFilePath = path.join(slotDir, 'saveinfo.json');
console.log('[MAIN PROCESS] Attempting to delete save files from:', slotDir);
let deletedFiles = [];
// Delete save file if it exists
if (fs.existsSync(saveFilePath)) {
console.log('[MAIN PROCESS] Deleting save file:', saveFilePath);
fs.unlinkSync(saveFilePath);
deletedFiles.push('save.json');
}
// Delete save info file if it exists
if (fs.existsSync(infoFilePath)) {
console.log('[MAIN PROCESS] Deleting save info file:', infoFilePath);
fs.unlinkSync(infoFilePath);
deletedFiles.push('saveinfo.json');
}
// Create empty save info file to indicate slot is empty
const saveInfo = {
slot: slot,
created: new Date().toISOString(),
version: '1.0.0',
exists: false,
deleted: new Date().toISOString()
};
fs.writeFileSync(infoFilePath, JSON.stringify(saveInfo, null, 2));
console.log('[MAIN PROCESS] Successfully deleted save files for slot', slot, ':', deletedFiles);
return { success: true, deletedFiles };
} catch (error) {
console.error('[MAIN PROCESS] Failed to delete save file:', error);
return { success: false, error: error.message };
}
});
// IPC handlers for window controls
// Handle logging from renderer process
ipcMain.on('log-message', async (event, { level, message, data }) => {
try {
switch (level) {
case 'error':
await logger.error(message, data);
break;
case 'warn':
await logger.warn(message, data);
break;
case 'info':
await logger.info(message, data);
break;
case 'debug':
await logger.debug(message, data);
break;
default:
await logger.info(message, data);
}
} catch (error) {
console.error('Failed to log message from renderer:', error);
// Fallback to console logging to prevent infinite loops
console.log(`[${level}] ${message}`, data || '');
}
});
ipcMain.on('minimize-window', () => {
if (mainWindow) {
mainWindow.minimize();
}
});
ipcMain.on('close-window', () => {
if (mainWindow) {
mainWindow.close();
}
});
ipcMain.on('toggle-fullscreen', () => {
if (mainWindow) {
const isFullscreen = mainWindow.isFullScreen();
if (isFullscreen) {
mainWindow.setFullScreen(false);
mainWindow.setSize(1200, 832);
mainWindow.center();
} else {
mainWindow.setFullScreen(true);
}
}
});
// This method will be called when Electron has finished initialization
app.whenReady().then(async () => {
console.log('[MAIN PROCESS] Electron app ready, starting initialization...');
try {
// Initialize logger with app data path
console.log('[MAIN PROCESS] Initializing logger...');
await logger.initialize(app.getPath('userData'));
console.log('[MAIN PROCESS] Logger initialized');
await logger.info('Galaxy Strike Online application starting');
console.log('[MAIN PROCESS] Logger info message sent');
console.log('[MAIN PROCESS] Creating main window...');
createWindow();
app.on('activate', () => {
console.log('[MAIN PROCESS] Activate event fired');
// On macOS it's common to re-create a window in the app when the dock icon is clicked
if (BrowserWindow.getAllWindows().length === 0) {
console.log('[MAIN PROCESS] No windows exist, creating new window');
createWindow();
}
});
console.log('[MAIN PROCESS] App initialization completed successfully');
} catch (error) {
console.error('[MAIN PROCESS] Error during app initialization:', error);
console.error('[MAIN PROCESS] Error stack:', error.stack);
}
}).catch((error) => {
console.error('[MAIN PROCESS] Error in app.whenReady():', error);
console.error('[MAIN PROCESS] Error stack:', error.stack);
});
// Quit when all windows are closed
app.on('window-all-closed', () => {
// On macOS it's common for applications and their menu bar to stay active
if (process.platform !== 'darwin') {
logger.info('Application shutting down');
app.quit();
}
});
// Handle uncaught exceptions
process.on('uncaughtException', async (error) => {
console.error('[MAIN PROCESS] Uncaught Exception:', error);
console.error('[MAIN PROCESS] Uncaught Exception stack:', error.stack);
try {
if (logger && typeof logger.errorEvent === 'function') {
await logger.errorEvent(error, 'Uncaught Exception in Main Process');
}
} catch (logError) {
console.error('[MAIN PROCESS] Failed to log uncaught exception:', logError);
}
console.error('[MAIN PROCESS] Application will continue running despite uncaught exception');
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('[MAIN PROCESS] Unhandled Promise Rejection at:', promise, 'reason:', reason);
console.error('[MAIN PROCESS] Rejection reason stack:', reason.stack);
});
// Handle unhandled rejections
process.on('unhandledRejection', async (reason, promise) => {
// Avoid logging the logging system's own errors to prevent infinite loops
if (reason && reason.message && reason.message.includes('object could not be cloned')) {
console.warn('IPC cloning error detected - this is expected during logger initialization');
return;
}
await logger.error('Unhandled Rejection', { reason: reason.toString(), promise: promise.toString() });
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// Security: Prevent new window creation
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', (event, navigationUrl) => {
event.preventDefault();
shell.openExternal(navigationUrl);
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,131 +0,0 @@
{
"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": "./",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"debug": "cross-env DEBUG=* electron .",
"debug-verbose": "cross-env DEBUG=* VERBOSE=true electron .",
"debug-boot": "cross-env DEBUG=boot* electron .",
"debug-renderer": "cross-env DEBUG=renderer* electron .",
"debug-main": "cross-env DEBUG=main* electron .",
"debug-windows": "set DEBUG=boot* && electron .",
"debug-windows-verbose": "set DEBUG=* && set VERBOSE=true && electron .",
"build": "electron-builder",
"build-win": "electron-builder --win",
"build-mac": "electron-builder --mac",
"build-linux": "electron-builder --linux",
"dist": "npm run build",
"pack": "electron-builder --dir",
"postinstall": "electron-builder install-app-deps"
},
"keywords": [
"game",
"space",
"mmorpg",
"idle",
"electron"
],
"dependencies": {
"cors": "^2.8.6",
"express": "^4.22.1",
"socket.io": "^4.8.3"
},
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^40.0.0",
"electron-builder": "^23.0.6"
},
"build": {
"appId": "com.korvarixstudios.galaxystrikeonline",
"productName": "Galaxy Strike Online",
"directories": {
"output": "dist"
},
"files": [
"**/*",
"!node_modules",
"!dist",
"!*.md"
],
"extraResources": [
{
"from": "assets",
"to": "assets"
}
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
},
{
"target": "portable",
"arch": [
"x64",
"ia32"
]
}
],
"icon": "assets/icon.ico"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "assets/icon.icns",
"category": "public.app-category.games"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"icon": "assets/icon.png",
"category": "Game",
"maintainer": "Korvarix Studios <contact@korvarixstudios.com>"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

View File

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

View File

@ -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)}}
}

View File

@ -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)}
}

View File

@ -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}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

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