updated more files
21
API/.env.example
Normal 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
|
||||
@ -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' }));
|
||||
|
||||
2133
Client/index.html
@ -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() {
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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 }}
|
||||
139
Galaxy-Strike-Online-main/.gitignore
vendored
@ -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-*
|
||||
@ -1,19 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline';
|
||||
|
||||
const conn = await mongoose.connect(mongoUri, {
|
||||
// Remove deprecated options for newer MongoDB versions
|
||||
});
|
||||
|
||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||
} catch (error) {
|
||||
logger.error('Database connection error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
@ -1,131 +0,0 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const productionConfig = {
|
||||
// Server settings
|
||||
port: process.env.PORT || 3001,
|
||||
|
||||
// Database settings
|
||||
database: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
}
|
||||
},
|
||||
|
||||
// JWT settings
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET,
|
||||
expiresIn: '24h',
|
||||
refreshExpiresIn: '7d'
|
||||
},
|
||||
|
||||
// Redis settings (for sessions and caching)
|
||||
redis: {
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
options: {
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3,
|
||||
}
|
||||
},
|
||||
|
||||
// CORS settings
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
},
|
||||
|
||||
// Socket.IO settings
|
||||
socketio: {
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
maxHttpBufferSize: 1e8, // 100 MB
|
||||
},
|
||||
|
||||
// Logging settings
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: process.env.NODE_ENV === 'production' ? 'json' : 'simple',
|
||||
file: {
|
||||
enabled: true,
|
||||
filename: 'logs/app.log',
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 5,
|
||||
}
|
||||
},
|
||||
|
||||
// Security settings
|
||||
security: {
|
||||
helmet: {
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true
|
||||
}
|
||||
},
|
||||
compression: {
|
||||
level: 6,
|
||||
threshold: 1024,
|
||||
}
|
||||
},
|
||||
|
||||
// Game settings
|
||||
game: {
|
||||
maxPlayersPerServer: 50,
|
||||
serverCleanupInterval: 300000, // 5 minutes
|
||||
inactivePlayerTimeout: 1800000, // 30 minutes
|
||||
autoSaveInterval: 60000, // 1 minute
|
||||
}
|
||||
};
|
||||
|
||||
// Validate required environment variables
|
||||
const validateConfig = () => {
|
||||
const required = ['JWT_SECRET'];
|
||||
const missing = required.filter(key => !process.env[key]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
logger.error(`Missing required environment variables: ${missing.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const prodRequired = ['MONGODB_URI', 'CLIENT_URL'];
|
||||
const prodMissing = prodRequired.filter(key => !process.env[key]);
|
||||
|
||||
if (prodMissing.length > 0) {
|
||||
logger.error(`Missing required production environment variables: ${prodMissing.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...productionConfig,
|
||||
validateConfig
|
||||
};
|
||||
@ -1,134 +0,0 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Custom error classes
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(message) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationError extends AppError {
|
||||
constructor(message = 'Authentication failed') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationError extends AppError {
|
||||
constructor(message = 'Access denied') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message = 'Resource not found') {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Resource conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message = 'Database operation failed') {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
logger.error({
|
||||
error: err,
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
userId: req.userId
|
||||
}
|
||||
});
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message).join(', ');
|
||||
error = new ValidationError(message);
|
||||
}
|
||||
|
||||
// Mongoose duplicate key error
|
||||
if (err.code === 11000) {
|
||||
const field = Object.keys(err.keyValue)[0];
|
||||
const value = err.keyValue[field];
|
||||
error = new ConflictError(`${field} '${value}' already exists`);
|
||||
}
|
||||
|
||||
// Mongoose cast error
|
||||
if (err.name === 'CastError') {
|
||||
error = new ValidationError(`Invalid ${err.path}: ${err.value}`);
|
||||
}
|
||||
|
||||
// JWT errors
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
error = new AuthenticationError('Invalid token');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
error = new AuthenticationError('Token expired');
|
||||
}
|
||||
|
||||
// Default error
|
||||
if (!error.isOperational) {
|
||||
error = new AppError('Something went wrong', 500);
|
||||
}
|
||||
|
||||
res.status(error.statusCode || 500).json({
|
||||
status: error.status || 'error',
|
||||
message: error.message,
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
stack: error.stack,
|
||||
error: err
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// 404 handler
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
errorHandler,
|
||||
catchAsync,
|
||||
notFound
|
||||
};
|
||||
@ -1,134 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const gameServerSchema = new mongoose.Schema({
|
||||
serverId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['public', 'private'],
|
||||
default: 'public'
|
||||
},
|
||||
region: {
|
||||
type: String,
|
||||
default: 'us-east'
|
||||
},
|
||||
maxPlayers: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 100
|
||||
},
|
||||
currentPlayers: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
owner: {
|
||||
userId: { type: String, required: true },
|
||||
username: { type: String, required: true }
|
||||
},
|
||||
settings: {
|
||||
password: { type: String, default: null },
|
||||
description: { type: String, default: '' },
|
||||
tags: [{ type: String }]
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['waiting', 'active', 'full', 'offline'],
|
||||
default: 'waiting'
|
||||
},
|
||||
gameServerUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastActivity: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance (only for non-unique fields)
|
||||
gameServerSchema.index({ type: 1 });
|
||||
gameServerSchema.index({ region: 1 });
|
||||
gameServerSchema.index({ status: 1 });
|
||||
gameServerSchema.index({ 'owner.userId': 1 });
|
||||
|
||||
// Methods
|
||||
gameServerSchema.methods.addPlayer = function() {
|
||||
if (this.currentPlayers < this.maxPlayers) {
|
||||
this.currentPlayers += 1;
|
||||
this.lastActivity = new Date();
|
||||
|
||||
if (this.currentPlayers >= this.maxPlayers) {
|
||||
this.status = 'full';
|
||||
} else if (this.currentPlayers > 0) {
|
||||
this.status = 'active';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.removePlayer = function() {
|
||||
if (this.currentPlayers > 0) {
|
||||
this.currentPlayers -= 1;
|
||||
this.lastActivity = new Date();
|
||||
|
||||
if (this.currentPlayers === 0) {
|
||||
this.status = 'waiting';
|
||||
} else if (this.currentPlayers < this.maxPlayers) {
|
||||
this.status = 'active';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.isFull = function() {
|
||||
return this.currentPlayers >= this.maxPlayers;
|
||||
};
|
||||
|
||||
gameServerSchema.methods.canJoin = function() {
|
||||
return this.status !== 'offline' && !this.isFull();
|
||||
};
|
||||
|
||||
// Static methods
|
||||
gameServerSchema.statics.findAvailableServers = function(filters = {}) {
|
||||
const query = { status: { $ne: 'offline' } };
|
||||
|
||||
if (filters.type) {
|
||||
query.type = filters.type;
|
||||
}
|
||||
|
||||
if (filters.region) {
|
||||
query.region = filters.region;
|
||||
}
|
||||
|
||||
return this.find(query).sort({ lastActivity: -1 });
|
||||
};
|
||||
|
||||
gameServerSchema.statics.cleanupOldServers = function(maxAge = 24 * 60 * 60 * 1000) { // 24 hours
|
||||
const cutoffTime = new Date(Date.now() - maxAge);
|
||||
return this.deleteMany({
|
||||
$or: [
|
||||
{ lastActivity: { $lt: cutoffTime }, currentPlayers: 0 },
|
||||
{ status: 'offline', lastActivity: { $lt: cutoffTime } }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('GameServer', gameServerSchema);
|
||||
@ -1,306 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const inventorySchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'Player'
|
||||
},
|
||||
|
||||
// Inventory settings
|
||||
maxSlots: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
|
||||
// Items array
|
||||
items: [{
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['weapon', 'armor', 'material', 'consumable']
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
|
||||
default: 'common'
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1
|
||||
},
|
||||
|
||||
// Item stats (for weapons/armor)
|
||||
stats: {
|
||||
attack: { type: Number, default: 0 },
|
||||
defense: { type: Number, default: 0 },
|
||||
speed: { type: Number, default: 0 },
|
||||
criticalChance: { type: Number, default: 0 },
|
||||
criticalDamage: { type: Number, default: 1.5 },
|
||||
damage: { type: Number, default: 0 },
|
||||
fireRate: { type: Number, default: 0 },
|
||||
range: { type: Number, default: 0 },
|
||||
energy: { type: Number, default: 0 },
|
||||
health: { type: Number, default: 0 },
|
||||
maxHealth: { type: Number, default: 0 },
|
||||
durability: { type: Number, default: 0 },
|
||||
weight: { type: Number, default: 0 },
|
||||
energyShield: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Item properties
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Equipment properties
|
||||
equipable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
slot: {
|
||||
type: String,
|
||||
enum: ['weapon', 'armor', 'engine', 'shield', 'special'],
|
||||
default: null
|
||||
},
|
||||
isEquipped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Consumable properties
|
||||
consumable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
effect: {
|
||||
health: { type: Number, default: 0 },
|
||||
energy: { type: Number, default: 0 },
|
||||
attack: { type: Number, default: 0 },
|
||||
defense: { type: Number, default: 0 },
|
||||
speed: { type: Number, default: 0 },
|
||||
duration: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Stackable items
|
||||
stackable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
acquiredAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
default: null
|
||||
}
|
||||
}],
|
||||
|
||||
// Equipped items
|
||||
equippedItems: {
|
||||
weapon: { type: String, default: null },
|
||||
armor: { type: String, default: null },
|
||||
engine: { type: String, default: null },
|
||||
shield: { type: String, default: null },
|
||||
special: { type: String, default: null }
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance
|
||||
inventorySchema.index({ userId: 1 });
|
||||
inventorySchema.index({ 'items.id': 1 });
|
||||
inventorySchema.index({ 'items.type': 1 });
|
||||
|
||||
// Methods
|
||||
inventorySchema.methods.addItem = function(itemData) {
|
||||
// Check if item already exists and is stackable
|
||||
const existingItem = this.items.find(item =>
|
||||
item.id === itemData.id &&
|
||||
item.type === itemData.type &&
|
||||
item.stackable
|
||||
);
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity += itemData.quantity || 1;
|
||||
} else {
|
||||
// Add new item
|
||||
const newItem = {
|
||||
...itemData,
|
||||
quantity: itemData.quantity || 1,
|
||||
acquiredAt: new Date()
|
||||
};
|
||||
|
||||
this.items.push(newItem);
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.removeItem = function(itemId, quantity = 1) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === itemId);
|
||||
|
||||
if (itemIndex === -1) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
const item = this.items[itemIndex];
|
||||
|
||||
if (item.quantity > quantity) {
|
||||
item.quantity -= quantity;
|
||||
} else {
|
||||
// Remove item completely
|
||||
this.items.splice(itemIndex, 1);
|
||||
|
||||
// Unequip if it was equipped
|
||||
Object.keys(this.equippedItems).forEach(slot => {
|
||||
if (this.equippedItems[slot] === itemId) {
|
||||
this.equippedItems[slot] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.hasItem = function(itemId, quantity = 1) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
return item && item.quantity >= quantity;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemCount = function(itemId) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
|
||||
inventorySchema.methods.equipItem = function(itemId, slot) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
|
||||
if (!item) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
if (!item.equipable) {
|
||||
throw new Error('Item is not equipable');
|
||||
}
|
||||
|
||||
if (item.slot !== slot) {
|
||||
throw new Error('Item cannot be equipped in this slot');
|
||||
}
|
||||
|
||||
// Unequip current item in slot
|
||||
if (this.equippedItems[slot]) {
|
||||
const currentItem = this.items.find(item => item.id === this.equippedItems[slot]);
|
||||
if (currentItem) {
|
||||
currentItem.isEquipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Equip new item
|
||||
this.equippedItems[slot] = itemId;
|
||||
item.isEquipped = true;
|
||||
item.lastUsed = new Date();
|
||||
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.unequipItem = function(slot) {
|
||||
const itemId = this.equippedItems[slot];
|
||||
|
||||
if (!itemId) {
|
||||
throw new Error('No item equipped in this slot');
|
||||
}
|
||||
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
if (item) {
|
||||
item.isEquipped = false;
|
||||
}
|
||||
|
||||
this.equippedItems[slot] = null;
|
||||
this.updatedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
inventorySchema.methods.useConsumable = function(itemId) {
|
||||
const item = this.items.find(item => item.id === itemId);
|
||||
|
||||
if (!item) {
|
||||
throw new Error('Item not found in inventory');
|
||||
}
|
||||
|
||||
if (!item.consumable) {
|
||||
throw new Error('Item is not consumable');
|
||||
}
|
||||
|
||||
if (item.quantity <= 0) {
|
||||
throw new Error('No quantity left');
|
||||
}
|
||||
|
||||
// Apply effects
|
||||
const effects = { ...item.effect };
|
||||
|
||||
// Remove one from quantity
|
||||
item.quantity -= 1;
|
||||
item.lastUsed = new Date();
|
||||
|
||||
// Remove item if quantity is 0
|
||||
if (item.quantity === 0) {
|
||||
const itemIndex = this.items.findIndex(item => item.id === itemId);
|
||||
this.items.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
this.updatedAt = new Date();
|
||||
this.save();
|
||||
|
||||
return effects;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getInventorySummary = function() {
|
||||
const summary = {
|
||||
totalItems: this.items.length,
|
||||
usedSlots: this.items.length,
|
||||
maxSlots: this.maxSlots,
|
||||
itemsByType: {},
|
||||
equippedItems: this.equippedItems
|
||||
};
|
||||
|
||||
// Count items by type
|
||||
this.items.forEach(item => {
|
||||
summary.itemsByType[item.type] = (summary.itemsByType[item.type] || 0) + item.quantity;
|
||||
});
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemsByType = function(type) {
|
||||
return this.items.filter(item => item.type === type);
|
||||
};
|
||||
|
||||
inventorySchema.methods.getItemsByRarity = function(rarity) {
|
||||
return this.items.filter(item => item.rarity === rarity);
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Inventory', inventorySchema);
|
||||
@ -1,155 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const playerSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
// Authentication
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false // Don't include password in queries by default
|
||||
},
|
||||
|
||||
// Player stats (simplified for API server)
|
||||
stats: {
|
||||
level: { type: Number, default: 1 },
|
||||
experience: { type: Number, default: 0 },
|
||||
credits: { type: Number, default: 1000 },
|
||||
dungeonsCleared: { type: Number, default: 0 },
|
||||
playTime: { type: Number, default: 0 },
|
||||
lastLogin: { type: Date, default: Date.now }
|
||||
},
|
||||
|
||||
// Base attributes
|
||||
attributes: {
|
||||
health: { type: Number, default: 100 },
|
||||
maxHealth: { type: Number, default: 100 },
|
||||
energy: { type: Number, default: 100 },
|
||||
maxEnergy: { type: Number, default: 100 },
|
||||
attack: { type: Number, default: 10 },
|
||||
defense: { type: Number, default: 5 },
|
||||
speed: { type: Number, default: 10 },
|
||||
criticalChance: { type: Number, default: 0.05 },
|
||||
criticalDamage: { type: Number, default: 1.5 }
|
||||
},
|
||||
|
||||
// Player info
|
||||
info: {
|
||||
name: { type: String, default: 'Commander' },
|
||||
title: { type: String, default: 'Rookie Pilot' },
|
||||
guild: { type: String, default: null },
|
||||
rank: { type: String, default: 'Cadet' }
|
||||
},
|
||||
|
||||
// Current ship
|
||||
currentShip: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Ship'
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
autoSave: { type: Boolean, default: true },
|
||||
notifications: { type: Boolean, default: true },
|
||||
soundEffects: { type: Boolean, default: true },
|
||||
music: { type: Boolean, default: false },
|
||||
discordIntegration: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// Daily rewards
|
||||
dailyRewards: {
|
||||
lastClaim: { type: Date, default: null },
|
||||
consecutiveDays: { type: Number, default: 0 }
|
||||
},
|
||||
|
||||
// Server info
|
||||
currentServer: { type: String, default: null },
|
||||
|
||||
// Timestamps
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now }
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance (only for non-unique fields)
|
||||
playerSchema.index({ 'stats.level': 1 });
|
||||
playerSchema.index({ currentServer: 1 });
|
||||
|
||||
// Methods (simplified for API server)
|
||||
playerSchema.methods.addExperience = function(amount) {
|
||||
this.stats.experience += amount;
|
||||
return this.stats.experience;
|
||||
};
|
||||
|
||||
playerSchema.methods.addCredits = function(amount) {
|
||||
this.stats.credits += amount;
|
||||
return this.stats.credits;
|
||||
};
|
||||
|
||||
playerSchema.methods.canAfford = function(cost) {
|
||||
return this.stats.credits >= cost;
|
||||
};
|
||||
|
||||
playerSchema.methods.spendCredits = function(cost) {
|
||||
if (this.canAfford(cost)) {
|
||||
this.stats.credits -= cost;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
playerSchema.methods.updatePlayTime = function(sessionTime) {
|
||||
this.stats.playTime += sessionTime;
|
||||
this.stats.lastLogin = new Date();
|
||||
};
|
||||
|
||||
playerSchema.methods.claimDailyReward = function() {
|
||||
const today = new Date();
|
||||
const lastClaim = this.dailyRewards.lastClaim;
|
||||
|
||||
// Check if already claimed today
|
||||
if (lastClaim && lastClaim.toDateString() === today.toDateString()) {
|
||||
return { success: false, message: 'Daily reward already claimed today' };
|
||||
}
|
||||
|
||||
// Check consecutive days
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (lastClaim && lastClaim.toDateString() === yesterday.toDateString()) {
|
||||
this.dailyRewards.consecutiveDays += 1;
|
||||
} else {
|
||||
this.dailyRewards.consecutiveDays = 1;
|
||||
}
|
||||
|
||||
this.dailyRewards.lastClaim = today;
|
||||
|
||||
// Calculate reward based on consecutive days
|
||||
const baseReward = 100;
|
||||
const consecutiveBonus = (this.dailyRewards.consecutiveDays - 1) * 50;
|
||||
const totalReward = baseReward + consecutiveBonus;
|
||||
|
||||
this.addCredits(totalReward);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reward: totalReward,
|
||||
consecutiveDays: this.dailyRewards.consecutiveDays
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Player', playerSchema);
|
||||
@ -1,189 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const shipSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'Player'
|
||||
},
|
||||
|
||||
// Ship identification
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['Fighter', 'Cruiser', 'Battleship', 'Carrier', 'Explorer']
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
|
||||
// Ship stats
|
||||
stats: {
|
||||
health: { type: Number, required: true },
|
||||
maxHealth: { type: Number, required: true },
|
||||
attack: { type: Number, required: true },
|
||||
defense: { type: Number, required: true },
|
||||
speed: { type: Number, required: true },
|
||||
criticalChance: { type: Number, default: 0.05 },
|
||||
criticalDamage: { type: Number, default: 1.5 },
|
||||
hull: { type: Number, required: true }
|
||||
},
|
||||
|
||||
// Ship appearance
|
||||
texture: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// Ship progression
|
||||
experience: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
requiredExp: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
upgrades: [{
|
||||
type: String
|
||||
}],
|
||||
|
||||
// Ship status
|
||||
isEquipped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Shop information (if purchased)
|
||||
price: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
|
||||
default: 'common'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
acquiredAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance
|
||||
shipSchema.index({ userId: 1 });
|
||||
shipSchema.index({ id: 1 });
|
||||
shipSchema.index({ isEquipped: 1 });
|
||||
shipSchema.index({ isCurrent: 1 });
|
||||
|
||||
// Methods
|
||||
shipSchema.methods.addExperience = function(amount) {
|
||||
this.experience += amount;
|
||||
|
||||
// Level up logic
|
||||
while (this.experience >= this.requiredExp) {
|
||||
this.experience -= this.requiredExp;
|
||||
this.level += 1;
|
||||
this.requiredExp = this.level * 100;
|
||||
|
||||
// Increase stats on level up
|
||||
this.stats.maxHealth += 10;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
this.stats.attack += 2;
|
||||
this.stats.defense += 1;
|
||||
this.stats.speed += 1;
|
||||
}
|
||||
|
||||
return this.level;
|
||||
};
|
||||
|
||||
shipSchema.methods.takeDamage = function(damage) {
|
||||
const actualDamage = Math.max(0, damage - this.stats.defense);
|
||||
this.stats.health = Math.max(0, this.stats.health - actualDamage);
|
||||
|
||||
if (this.stats.health === 0) {
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
return actualDamage;
|
||||
};
|
||||
|
||||
shipSchema.methods.heal = function(amount) {
|
||||
const healAmount = Math.min(amount, this.stats.maxHealth - this.stats.health);
|
||||
this.stats.health += healAmount;
|
||||
this.isDestroyed = false;
|
||||
|
||||
return healAmount;
|
||||
};
|
||||
|
||||
shipSchema.methods.isAlive = function() {
|
||||
return this.stats.health > 0;
|
||||
};
|
||||
|
||||
shipSchema.methods.getStatSummary = function() {
|
||||
return {
|
||||
name: this.name,
|
||||
class: this.class,
|
||||
level: this.level,
|
||||
health: `${this.stats.health}/${this.stats.maxHealth}`,
|
||||
attack: this.stats.attack,
|
||||
defense: this.stats.defense,
|
||||
speed: this.stats.speed,
|
||||
criticalChance: `${(this.stats.criticalChance * 100).toFixed(1)}%`,
|
||||
criticalDamage: `${this.stats.criticalDamage}x`
|
||||
};
|
||||
};
|
||||
|
||||
shipSchema.methods.upgrade = function(upgradeType) {
|
||||
switch (upgradeType) {
|
||||
case 'health':
|
||||
this.stats.maxHealth += 20;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
break;
|
||||
case 'attack':
|
||||
this.stats.attack += 5;
|
||||
break;
|
||||
case 'defense':
|
||||
this.stats.defense += 3;
|
||||
break;
|
||||
case 'speed':
|
||||
this.stats.speed += 2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown upgrade type');
|
||||
}
|
||||
|
||||
if (!this.upgrades.includes(upgradeType)) {
|
||||
this.upgrades.push(upgradeType);
|
||||
}
|
||||
|
||||
this.lastUsed = new Date();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Ship', shipSchema);
|
||||
6068
Galaxy-Strike-Online-main/API/package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,214 +0,0 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Joi = require('joi');
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const Player = require('../models/Player');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Rate limiting for auth routes
|
||||
const authLimiter = new RateLimiterMemory({
|
||||
keyGenerator: (req) => req.ip,
|
||||
points: 5, // Number of requests
|
||||
duration: 900, // Per 15 minutes (900 seconds)
|
||||
blockDuration: 900, // Block for 15 minutes
|
||||
message: 'Too many authentication attempts, please try again later.'
|
||||
});
|
||||
|
||||
// Validation schemas
|
||||
const registerSchema = Joi.object({
|
||||
username: Joi.string().min(3).max(30).required(),
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required()
|
||||
});
|
||||
|
||||
const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required()
|
||||
});
|
||||
|
||||
// Register route
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const resLimiter = await authLimiter.consume(req.ip);
|
||||
if (!resLimiter.remainingPoints) {
|
||||
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
|
||||
}
|
||||
|
||||
const { error } = registerSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
|
||||
const { username, email, password } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await Player.findOne({
|
||||
$or: [{ email }, { username }]
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
error: 'User with this email or username already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new player
|
||||
const player = new Player({
|
||||
userId: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
lastLogin: new Date()
|
||||
});
|
||||
|
||||
await player.save();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: player.userId, email: player.email },
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
logger.info(`New user registered: ${email}`);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User registered successfully',
|
||||
token,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login route
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const resLimiter = await authLimiter.consume(req.ip);
|
||||
if (!resLimiter.remainingPoints) {
|
||||
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
|
||||
}
|
||||
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find user
|
||||
const player = await Player.findOne({ email }).select('+password');
|
||||
if (!player) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Check if password exists (for backward compatibility with existing users)
|
||||
if (!player.password) {
|
||||
logger.error('Player password field is missing for user:', email);
|
||||
return res.status(401).json({
|
||||
error: 'Account migration required. Please re-register your account.',
|
||||
requiresMigration: true
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await bcrypt.compare(password, player.password);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
player.stats.lastLogin = new Date();
|
||||
await player.save();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: player.userId, email: player.email },
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
logger.info(`User logged in: ${email}`);
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats,
|
||||
info: player.info
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify token route
|
||||
router.get('/verify', async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
|
||||
const player = await Player.findOne({ userId: decoded.userId });
|
||||
if (!player) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
valid: true,
|
||||
user: {
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
email: player.email,
|
||||
stats: player.stats,
|
||||
info: player.info
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Token verification error:', error);
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout route
|
||||
router.post('/logout', async (req, res) => {
|
||||
try {
|
||||
// In a real implementation, you might want to blacklist the token
|
||||
// For now, we'll just return success
|
||||
res.json({ message: 'Logout successful' });
|
||||
} catch (error) {
|
||||
logger.error('Logout error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -1,419 +0,0 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const GameServer = require('../models/GameServer');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register new game server (for GameServer instances to register themselves)
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner } = req.body;
|
||||
|
||||
logger.info(`[API SERVER] Game server registration request:`, {
|
||||
serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner
|
||||
});
|
||||
|
||||
// Check if server already exists
|
||||
const existingServer = await GameServer.findOne({ serverId });
|
||||
if (existingServer) {
|
||||
// Update existing server
|
||||
existingServer.name = name || existingServer.name;
|
||||
existingServer.type = type || existingServer.type;
|
||||
existingServer.region = region || existingServer.region;
|
||||
existingServer.maxPlayers = maxPlayers || existingServer.maxPlayers;
|
||||
existingServer.currentPlayers = currentPlayers !== undefined ? currentPlayers : existingServer.currentPlayers;
|
||||
existingServer.gameServerUrl = gameServerUrl || existingServer.gameServerUrl;
|
||||
existingServer.status = 'waiting';
|
||||
existingServer.lastActivity = new Date();
|
||||
|
||||
await existingServer.save();
|
||||
logger.info(`[API SERVER] Updated existing server: ${serverId} with ${existingServer.currentPlayers} players`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Server updated successfully',
|
||||
server: existingServer
|
||||
});
|
||||
}
|
||||
|
||||
// Create new server
|
||||
const newServer = new GameServer({
|
||||
serverId,
|
||||
name: name || `Game Server ${serverId}`,
|
||||
type: type || 'public',
|
||||
region: region || 'us-east',
|
||||
maxPlayers: maxPlayers || 10,
|
||||
currentPlayers: currentPlayers !== undefined ? currentPlayers : 0,
|
||||
owner: owner || {
|
||||
userId: 'system',
|
||||
username: 'System'
|
||||
},
|
||||
status: 'waiting',
|
||||
gameServerUrl,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
await newServer.save();
|
||||
logger.info(`[API SERVER] Registered new server: ${serverId}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Server registered successfully',
|
||||
server: newServer
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error registering server:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to register server'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update server status (for GameServer instances to update their status)
|
||||
router.post('/update-status/:serverId', async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
const { currentPlayers, status } = req.body;
|
||||
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
if (!server) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (currentPlayers !== undefined) {
|
||||
server.currentPlayers = currentPlayers;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
server.status = status;
|
||||
}
|
||||
|
||||
server.lastActivity = new Date();
|
||||
await server.save();
|
||||
|
||||
logger.info(`[API SERVER] Updated server ${serverId} status:`, {
|
||||
currentPlayers: server.currentPlayers,
|
||||
status: server.status
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server status updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error updating server status:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update server status'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unregister game server (for GameServer instances to unregister themselves)
|
||||
router.delete('/unregister/:serverId', async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
logger.info(`[API SERVER] Game server unregistration request:`, { serverId });
|
||||
|
||||
// Find and remove server
|
||||
const server = await GameServer.findOneAndDelete({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`[API SERVER] Unregistered server: ${serverId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server unregistered successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[API SERVER] Error unregistering server:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to unregister server'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get server list
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type, region } = req.query;
|
||||
|
||||
// Build filters
|
||||
const filters = {};
|
||||
if (type) filters.type = type;
|
||||
if (region) filters.region = region;
|
||||
|
||||
logger.info(`[API SERVER] Fetching servers for user ${req.userId} with filters:`, filters);
|
||||
|
||||
// Get available servers from database
|
||||
const servers = await GameServer.findAvailableServers(filters);
|
||||
|
||||
logger.info(`[API SERVER] Found ${servers.length} servers in database`);
|
||||
|
||||
// Format server list for client
|
||||
const serverList = servers.map(server => ({
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
region: server.region,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
status: server.status,
|
||||
ownerName: server.owner.username,
|
||||
createdAt: server.createdAt,
|
||||
lastActivity: server.lastActivity
|
||||
}));
|
||||
|
||||
logger.info(`[API SERVER] Returning ${serverList.length} servers to client`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
servers: serverList,
|
||||
totalServers: serverList.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting server list:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new server
|
||||
router.post('/create', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { name, type = 'public', maxPlayers = 10, region = 'us-east', settings = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Server name required' });
|
||||
}
|
||||
|
||||
// Generate unique server ID
|
||||
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Get user info from token (you might want to fetch full user data)
|
||||
const ownerUsername = req.body.username || 'Unknown'; // This should come from user data
|
||||
|
||||
// Create new server in database
|
||||
const newServer = new GameServer({
|
||||
serverId,
|
||||
name,
|
||||
type,
|
||||
region,
|
||||
maxPlayers,
|
||||
owner: {
|
||||
userId: req.userId,
|
||||
username: ownerUsername
|
||||
},
|
||||
settings,
|
||||
gameServerUrl: process.env.GAME_SERVER_URL || 'https://api.korvarix.com'
|
||||
});
|
||||
|
||||
await newServer.save();
|
||||
|
||||
logger.info(`Server created: ${serverId} by user ${req.userId}`);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Server created successfully',
|
||||
server: {
|
||||
id: newServer.serverId,
|
||||
name: newServer.name,
|
||||
type: newServer.type,
|
||||
region: newServer.region,
|
||||
currentPlayers: newServer.currentPlayers,
|
||||
maxPlayers: newServer.maxPlayers,
|
||||
status: newServer.status,
|
||||
ownerName: newServer.owner.username,
|
||||
createdAt: newServer.createdAt
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error creating server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Join server
|
||||
router.post('/:serverId/join', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
// Find server in database
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
// Check if server can be joined
|
||||
if (!server.canJoin()) {
|
||||
return res.status(400).json({ error: 'Server is full or offline' });
|
||||
}
|
||||
|
||||
// Add player to server
|
||||
const playerAdded = server.addPlayer();
|
||||
if (!playerAdded) {
|
||||
return res.status(400).json({ error: 'Server is full' });
|
||||
}
|
||||
|
||||
await server.save();
|
||||
|
||||
logger.info(`User ${req.userId} joined server ${serverId}`);
|
||||
|
||||
res.json({
|
||||
message: 'Joined server successfully',
|
||||
server: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
gameServerUrl: server.gameServerUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error joining server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Leave server
|
||||
router.post('/:serverId/leave', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
// Find server in database
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
// Remove player from server
|
||||
const playerRemoved = server.removePlayer();
|
||||
if (playerRemoved) {
|
||||
await server.save();
|
||||
logger.info(`User ${req.userId} left server ${serverId}`);
|
||||
}
|
||||
|
||||
// Update player's current server
|
||||
const Player = require('../models/Player');
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: req.userId },
|
||||
{ currentServer: null }
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Left server successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error leaving server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server details
|
||||
router.get('/:serverId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { serverId } = req.params;
|
||||
|
||||
const server = await GameServer.findOne({ serverId });
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
server: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
region: server.region,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers,
|
||||
status: server.status,
|
||||
ownerName: server.owner.username,
|
||||
settings: server.settings,
|
||||
createdAt: server.createdAt,
|
||||
lastActivity: server.lastActivity
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting server details:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user's current server
|
||||
router.get('/user/current', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const Player = require('../models/Player');
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player || !player.currentServer) {
|
||||
return res.json({ currentServer: null });
|
||||
}
|
||||
|
||||
const server = await GameServer.findOne({ serverId: player.currentServer });
|
||||
|
||||
if (!server) {
|
||||
// Clear invalid server reference
|
||||
await Player.findOneAndUpdate(
|
||||
{ userId: req.userId },
|
||||
{ currentServer: null }
|
||||
);
|
||||
return res.json({ currentServer: null });
|
||||
}
|
||||
|
||||
res.json({
|
||||
currentServer: {
|
||||
id: server.serverId,
|
||||
name: server.name,
|
||||
currentPlayers: server.currentPlayers,
|
||||
maxPlayers: server.maxPlayers
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting current server:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Create Test Server Script
|
||||
* Adds a test server to the database for testing the server browser
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GameServer = require('../models/GameServer');
|
||||
require('dotenv').config();
|
||||
|
||||
async function createTestServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log('Connected to database');
|
||||
|
||||
// Check if test server already exists
|
||||
const existingServer = await GameServer.findOne({ serverId: 'test_server_001' });
|
||||
if (existingServer) {
|
||||
console.log('Test server already exists, deleting it first...');
|
||||
await GameServer.deleteOne({ serverId: 'test_server_001' });
|
||||
}
|
||||
|
||||
// Create test server
|
||||
const testServer = new GameServer({
|
||||
serverId: 'test_server_001',
|
||||
name: 'Test Server - Galaxy Strike',
|
||||
type: 'public',
|
||||
region: 'us-east',
|
||||
maxPlayers: 10,
|
||||
currentPlayers: 2,
|
||||
owner: {
|
||||
userId: 'test_user_001',
|
||||
username: 'TestAdmin'
|
||||
},
|
||||
settings: {
|
||||
description: 'A test server for Galaxy Strike Online',
|
||||
tags: ['test', 'beginner', 'pve']
|
||||
},
|
||||
status: 'active',
|
||||
gameServerUrl: 'https://api.korvarix.com'
|
||||
});
|
||||
|
||||
await testServer.save();
|
||||
console.log('Test server created successfully!');
|
||||
console.log('Server details:', {
|
||||
id: testServer.serverId,
|
||||
name: testServer.name,
|
||||
type: testServer.type,
|
||||
region: testServer.region,
|
||||
currentPlayers: testServer.currentPlayers,
|
||||
maxPlayers: testServer.maxPlayers,
|
||||
status: testServer.status
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating test server:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (require.main === module) {
|
||||
createTestServer();
|
||||
}
|
||||
|
||||
module.exports = createTestServer;
|
||||
@ -1,50 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
require('dotenv').config();
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
logger.info('Starting database migration...');
|
||||
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
|
||||
|
||||
logger.info('Connected to database');
|
||||
|
||||
// Create indexes for performance
|
||||
const db = mongoose.connection.db;
|
||||
|
||||
// Player indexes
|
||||
await db.collection('players').createIndex({ userId: 1 }, { unique: true });
|
||||
await db.collection('players').createIndex({ email: 1 }, { unique: true });
|
||||
await db.collection('players').createIndex({ 'stats.level': 1 });
|
||||
await db.collection('players').createIndex({ currentServer: 1 });
|
||||
|
||||
// Ship indexes
|
||||
await db.collection('ships').createIndex({ userId: 1 });
|
||||
await db.collection('ships').createIndex({ id: 1 }, { unique: true });
|
||||
await db.collection('ships').createIndex({ isEquipped: 1 });
|
||||
await db.collection('ships').createIndex({ isCurrent: 1 });
|
||||
|
||||
// Inventory indexes
|
||||
await db.collection('inventories').createIndex({ userId: 1 }, { unique: true });
|
||||
await db.collection('inventories').createIndex({ 'items.id': 1 });
|
||||
await db.collection('inventories').createIndex({ 'items.type': 1 });
|
||||
|
||||
logger.info('Database migration completed successfully');
|
||||
|
||||
// Close connection
|
||||
await mongoose.connection.close();
|
||||
logger.info('Database connection closed');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
migrate();
|
||||
}
|
||||
|
||||
module.exports = migrate;
|
||||
@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Password Migration Script
|
||||
* Updates existing users to have password fields
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const Player = require('../models/Player');
|
||||
const logger = require('../utils/logger');
|
||||
require('dotenv').config();
|
||||
|
||||
async function migratePasswords() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
logger.info('Connected to database for password migration');
|
||||
|
||||
// Find all users without passwords
|
||||
const usersWithoutPasswords = await Player.find({
|
||||
password: { $exists: false }
|
||||
});
|
||||
|
||||
logger.info(`Found ${usersWithoutPasswords.length} users without passwords`);
|
||||
|
||||
if (usersWithoutPasswords.length === 0) {
|
||||
logger.info('No users need password migration');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update each user with a default password
|
||||
for (const user of usersWithoutPasswords) {
|
||||
// Generate a default password (you might want to use a different approach)
|
||||
const defaultPassword = 'tempPassword123!';
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(defaultPassword, salt);
|
||||
|
||||
await Player.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
'stats.lastLogin': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Migrated user: ${user.email} with default password`);
|
||||
}
|
||||
|
||||
logger.info('Password migration completed successfully');
|
||||
|
||||
// Output the default password for users to change
|
||||
console.log('\n=== MIGRATION COMPLETE ===');
|
||||
console.log(`Updated ${usersWithoutPasswords.length} users`);
|
||||
console.log('Default password for all migrated users: tempPassword123!');
|
||||
console.log('Users should change their password after first login\n');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Password migration error:', error);
|
||||
console.error('Migration failed:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
if (require.main === module) {
|
||||
migratePasswords();
|
||||
}
|
||||
|
||||
module.exports = migratePasswords;
|
||||
@ -1,196 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
require('dotenv').config();
|
||||
|
||||
async function seed() {
|
||||
try {
|
||||
logger.info('Starting database seeding...');
|
||||
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
|
||||
|
||||
logger.info('Connected to database');
|
||||
|
||||
// Clear existing data (optional - remove if you want to preserve data)
|
||||
logger.info('Clearing existing data...');
|
||||
await Player.deleteMany({});
|
||||
await Ship.deleteMany({});
|
||||
await Inventory.deleteMany({});
|
||||
|
||||
// Create a test user
|
||||
const testUser = new Player({
|
||||
userId: 'test_user_001',
|
||||
username: 'TestPlayer',
|
||||
email: 'test@example.com',
|
||||
password: '$2a$10$example_hashed_password_here',
|
||||
stats: {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
credits: 5000,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: new Date()
|
||||
},
|
||||
attributes: {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
},
|
||||
info: {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
},
|
||||
settings: {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
discordIntegration: false
|
||||
},
|
||||
dailyRewards: {
|
||||
lastClaim: null,
|
||||
consecutiveDays: 0
|
||||
}
|
||||
});
|
||||
|
||||
await testUser.save();
|
||||
logger.info('Created test user');
|
||||
|
||||
// Create starter ship for test user
|
||||
const starterShip = new Ship({
|
||||
userId: testUser.userId,
|
||||
id: 'starter_cruiser_001',
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
level: 1,
|
||||
stats: {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
attack: 15,
|
||||
defense: 12,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
hull: 100
|
||||
},
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
experience: 0,
|
||||
requiredExp: 100,
|
||||
upgrades: [],
|
||||
isEquipped: true,
|
||||
isCurrent: true,
|
||||
price: 5000,
|
||||
rarity: 'common',
|
||||
description: 'Reliable starter cruiser for new pilots',
|
||||
acquiredAt: new Date(),
|
||||
lastUsed: new Date()
|
||||
});
|
||||
|
||||
await starterShip.save();
|
||||
logger.info('Created starter ship');
|
||||
|
||||
// Update player with current ship
|
||||
testUser.currentShip = starterShip._id;
|
||||
await testUser.save();
|
||||
|
||||
// Create inventory for test user
|
||||
const inventory = new Inventory({
|
||||
userId: testUser.userId,
|
||||
maxSlots: 50,
|
||||
items: [
|
||||
{
|
||||
id: 'starter_blaster_common',
|
||||
name: 'Common Blaster',
|
||||
type: 'weapon',
|
||||
rarity: 'common',
|
||||
quantity: 1,
|
||||
stats: {
|
||||
attack: 5,
|
||||
criticalChance: 0.02,
|
||||
damage: 10,
|
||||
fireRate: 2,
|
||||
range: 5,
|
||||
energy: 5
|
||||
},
|
||||
description: 'A reliable basic blaster for new pilots',
|
||||
equipable: true,
|
||||
slot: 'weapon',
|
||||
isEquipped: false,
|
||||
stackable: false,
|
||||
acquiredAt: new Date()
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_common',
|
||||
name: 'Basic Armor',
|
||||
type: 'armor',
|
||||
rarity: 'common',
|
||||
quantity: 1,
|
||||
stats: {
|
||||
defense: 3,
|
||||
durability: 20,
|
||||
weight: 2,
|
||||
energyShield: 0
|
||||
},
|
||||
description: 'Light armor providing basic protection',
|
||||
equipable: true,
|
||||
slot: 'armor',
|
||||
isEquipped: false,
|
||||
stackable: false,
|
||||
acquiredAt: new Date()
|
||||
},
|
||||
{
|
||||
id: 'health_kit',
|
||||
name: 'Health Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
quantity: 3,
|
||||
stats: {},
|
||||
description: 'A medical kit that restores health',
|
||||
consumable: true,
|
||||
effect: {
|
||||
health: 50
|
||||
},
|
||||
stackable: true,
|
||||
acquiredAt: new Date()
|
||||
}
|
||||
],
|
||||
equippedItems: {
|
||||
weapon: null,
|
||||
armor: null,
|
||||
engine: null,
|
||||
shield: null,
|
||||
special: null
|
||||
}
|
||||
});
|
||||
|
||||
await inventory.save();
|
||||
logger.info('Created inventory with starter items');
|
||||
|
||||
logger.info('Database seeding completed successfully');
|
||||
|
||||
// Close connection
|
||||
await mongoose.connection.close();
|
||||
logger.info('Database connection closed');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Seeding failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
seed();
|
||||
}
|
||||
|
||||
module.exports = seed;
|
||||
@ -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 };
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
|
||||
};
|
||||
@ -1,220 +0,0 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const app = require('../server');
|
||||
const Player = require('../models/Player');
|
||||
|
||||
describe('API Tests', () => {
|
||||
let token;
|
||||
let testUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Connect to test database
|
||||
const mongoUri = process.env.MONGODB_TEST_URI || 'mongodb://localhost:27017/galaxystrikeonline_test';
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up and close connection
|
||||
await Player.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up before each test
|
||||
await Player.deleteMany({});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('POST /api/auth/register - should register new user', async () => {
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
expect(response.body.user).toHaveProperty('email', userData.email);
|
||||
});
|
||||
|
||||
test('POST /api/auth/login - should login existing user', async () => {
|
||||
// First register a user
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
// Then login
|
||||
const loginData = {
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
token = response.body.token;
|
||||
testUser = response.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/auth/verify - should verify token', async () => {
|
||||
// First login to get token
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const registerResponse = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
token = registerResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('valid', true);
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for game tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/game/player - should get player data', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/player')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('userId');
|
||||
expect(response.body).toHaveProperty('stats');
|
||||
expect(response.body).toHaveProperty('attributes');
|
||||
});
|
||||
|
||||
test('GET /api/game/ships - should get player ships', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/ships')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('ships');
|
||||
expect(Array.isArray(response.body.ships)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/game/inventory - should get player inventory', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/inventory')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('items');
|
||||
expect(response.body).toHaveProperty('summary');
|
||||
});
|
||||
|
||||
test('POST /api/game/daily-reward - should claim daily reward', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/game/daily-reward')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for server tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/servers - should get server list', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/servers')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('servers');
|
||||
expect(response.body).toHaveProperty('totalServers');
|
||||
expect(Array.isArray(response.body.servers)).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /api/servers/create - should create new server', async () => {
|
||||
const serverData = {
|
||||
name: 'Test Server',
|
||||
type: 'public',
|
||||
maxPlayers: 10,
|
||||
region: 'us-east'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/servers/create')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send(serverData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.server).toHaveProperty('name', serverData.name);
|
||||
expect(response.body.server).toHaveProperty('type', serverData.type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check', () => {
|
||||
test('GET /health - should return health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('status', 'OK');
|
||||
expect(response.body).toHaveProperty('timestamp');
|
||||
expect(response.body).toHaveProperty('uptime');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,27 +0,0 @@
|
||||
const winston = require('winston');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'galaxystrikeonline-server' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
]
|
||||
});
|
||||
|
||||
// Add console transport for development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
@ -1,120 +0,0 @@
|
||||
{
|
||||
"_readme": [
|
||||
"Galaxy Strike Online — Starbase World Layout",
|
||||
"Edit this file to customize your starbase. Changes take effect next time you visit the Starbases tab.",
|
||||
"",
|
||||
"GRID",
|
||||
" cols / rows : overall size of the world (min 8×8, max ~32×26 before performance drops)",
|
||||
"",
|
||||
"STYLE (global defaults — all optional hex strings)",
|
||||
" wallColor / wallColorLeft / wallColorRight / wallColorTop",
|
||||
" floorColorEven / floorColorOdd",
|
||||
" doorColor / doorFrameColor",
|
||||
"",
|
||||
"WALLS — each entry draws a run of wall tiles",
|
||||
" col, row : start position",
|
||||
" span : number of tiles (default 1)",
|
||||
" dir : 'h' horizontal | 'v' vertical",
|
||||
" color / colorLeft / colorRight / colorTop : per-segment color overrides",
|
||||
"",
|
||||
"DOORS — walkable openings; panel slides up when player is adjacent",
|
||||
" col, row : position",
|
||||
" color : panel color override",
|
||||
" frameColor : pillar/frame color override",
|
||||
"",
|
||||
"ROOMS — named regions; rendered as ghost labels + used for per-room wallpapers",
|
||||
" id : unique string (used by the unlock / wallpaper system)",
|
||||
" label : display text",
|
||||
" bounds : { col, row, cols, rows }",
|
||||
" unlock : item id required to unlock this room (omit = always open)",
|
||||
" Locked rooms are filled with sealed-wall tiles and shown as 'LOCKED'",
|
||||
"",
|
||||
"PLAYER START",
|
||||
" col, row : spawn tile (must be walkable floor)"
|
||||
],
|
||||
|
||||
"name": "Starbase Alpha-7",
|
||||
|
||||
"grid": { "cols": 26, "rows": 20 },
|
||||
|
||||
"style": {
|
||||
"wallColor": "#00d4ff",
|
||||
"wallColorLeft": "#0c1626",
|
||||
"wallColorRight": "#0a1220",
|
||||
"wallColorTop": "#1a2840",
|
||||
"floorColorEven": "#151c2e",
|
||||
"floorColorOdd": "#111827",
|
||||
"doorColor": "#00ffcc",
|
||||
"doorFrameColor": "#00d4ff"
|
||||
},
|
||||
|
||||
"walls": [
|
||||
{ "col": 0, "row": 0, "span": 26, "dir": "h", "_": "top wall" },
|
||||
{ "col": 0, "row": 19, "span": 26, "dir": "h", "_": "bottom wall" },
|
||||
{ "col": 0, "row": 0, "span": 20, "dir": "v", "_": "left wall" },
|
||||
{ "col": 25, "row": 0, "span": 20, "dir": "v", "_": "right wall" },
|
||||
|
||||
{ "col": 1, "row": 6, "span": 24, "dir": "h", "_": "main hall separator",
|
||||
"color": "#00d4ff", "colorLeft": "#0d1a2e", "colorRight": "#0a1525", "colorTop": "#182a42" },
|
||||
|
||||
{ "col": 7, "row": 7, "span": 13, "dir": "v", "_": "left inner wall" },
|
||||
{ "col": 17, "row": 7, "span": 2, "dir": "v", "_": "right stub top" },
|
||||
{ "col": 17, "row": 10, "span": 10, "dir": "v", "_": "right stub bottom",
|
||||
"color": "#4488ff", "colorLeft": "#0a1830", "colorRight": "#080e20", "colorTop": "#102040" },
|
||||
|
||||
{ "col": 7, "row": 13, "span": 10, "dir": "h", "_": "operations divider",
|
||||
"color": "#ff00ff", "colorLeft": "#1a0a20", "colorRight": "#120616", "colorTop": "#200a30" },
|
||||
|
||||
{ "col": 17, "row": 13, "span": 6, "dir": "h", "_": "vault corridor wall",
|
||||
"color": "#ffcc00", "colorLeft": "#1a1200", "colorRight": "#140e00", "colorTop": "#221800" }
|
||||
],
|
||||
|
||||
"doors": [
|
||||
{ "col": 13, "row": 6, "dir": "h", "_": "main hall → command centre" },
|
||||
{ "col": 17, "row": 6, "dir": "h", "color": "#4488ff", "frameColor": "#0066ff", "_": "main hall → right wing" },
|
||||
{ "col": 7, "row": 10, "dir": "v", "_": "left wing ↔ command centre" },
|
||||
{ "col": 17, "row": 9, "dir": "v", "color": "#ff88ff", "frameColor": "#cc44cc", "_": "command centre ↔ right wing" },
|
||||
{ "col": 7, "row": 15, "dir": "v", "_": "left wing → operations" },
|
||||
{ "col": 13, "row": 13, "dir": "h", "color": "#ff00ff", "frameColor": "#cc00cc", "_": "command centre → operations" },
|
||||
{ "col": 20, "row": 13, "dir": "h", "color": "#ffcc00", "frameColor": "#ddaa00", "_": "right wing → vault corridor" }
|
||||
],
|
||||
|
||||
"rooms": [
|
||||
{
|
||||
"id": "main_hall",
|
||||
"label": "Main Hall",
|
||||
"bounds": { "col": 1, "row": 1, "cols": 24, "rows": 5 }
|
||||
},
|
||||
{
|
||||
"id": "left_wing",
|
||||
"label": "Armory Wing",
|
||||
"bounds": { "col": 1, "row": 7, "cols": 6, "rows": 12 },
|
||||
"unlock": "room_armory"
|
||||
},
|
||||
{
|
||||
"id": "command_centre",
|
||||
"label": "Command Centre",
|
||||
"bounds": { "col": 8, "row": 7, "cols": 9, "rows": 6 }
|
||||
},
|
||||
{
|
||||
"id": "right_wing",
|
||||
"label": "Research Lab",
|
||||
"bounds": { "col": 18, "row": 7, "cols": 7, "rows": 6 },
|
||||
"unlock": "room_research_lab"
|
||||
},
|
||||
{
|
||||
"id": "operations",
|
||||
"label": "Operations Centre",
|
||||
"bounds": { "col": 8, "row": 14, "cols": 9, "rows": 5 },
|
||||
"unlock": "room_operations"
|
||||
},
|
||||
{
|
||||
"id": "commanders_vault",
|
||||
"label": "Commander's Vault",
|
||||
"bounds": { "col": 18, "row": 14, "cols": 7, "rows": 5 },
|
||||
"unlock": "room_vault"
|
||||
}
|
||||
],
|
||||
|
||||
"playerStart": { "col": 13, "row": 3 }
|
||||
}
|
||||
@ -1,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);
|
||||
});
|
||||
});
|
||||
@ -1,325 +0,0 @@
|
||||
/**
|
||||
* Save System Integration
|
||||
* Integrates SmartSaveManager with existing game systems
|
||||
*/
|
||||
|
||||
console.log('[SAVE INTEGRATION] Save system integration loading');
|
||||
|
||||
// Override the game's save method to use SmartSaveManager
|
||||
function integrateWithGameEngine() {
|
||||
if (window.game && window.game.save) {
|
||||
// Store original save method
|
||||
const originalSave = window.game.save;
|
||||
|
||||
// Override game save method
|
||||
window.game.save = async function() {
|
||||
// console.log('[SAVE INTEGRATION] Game save called');
|
||||
|
||||
if (window.smartSaveManager) {
|
||||
await window.smartSaveManager.save();
|
||||
} else {
|
||||
// Fallback to original save if SmartSaveManager not available
|
||||
return await originalSave.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game save method overridden');
|
||||
}
|
||||
}
|
||||
|
||||
// Override the game's load method to use SmartSaveManager
|
||||
function integrateLoadSystem() {
|
||||
if (window.game && window.game.load) {
|
||||
// Store original load method
|
||||
const originalLoad = window.game.load;
|
||||
|
||||
// Override load method
|
||||
window.game.load = async function(saveData = null) {
|
||||
console.log('[SAVE INTEGRATION] Game load called, using SmartSaveManager');
|
||||
|
||||
try {
|
||||
let dataToLoad = saveData;
|
||||
|
||||
// If no data provided, use SmartSaveManager
|
||||
if (!dataToLoad && window.smartSaveManager) {
|
||||
dataToLoad = await window.smartSaveManager.loadPlayerData();
|
||||
}
|
||||
|
||||
// Load the data
|
||||
if (dataToLoad) {
|
||||
if (this.loadPlayerData) {
|
||||
this.loadPlayerData(dataToLoad);
|
||||
} else {
|
||||
// Fallback to original load
|
||||
return await originalLoad.call(this, dataToLoad);
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game data loaded successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('[SAVE INTEGRATION] No save data found, starting fresh');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SAVE INTEGRATION] Load error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game load method overridden');
|
||||
}
|
||||
}
|
||||
|
||||
// Add server data loading method to game
|
||||
function addServerDataSupport() {
|
||||
if (window.game) {
|
||||
// Store pending server data for later application
|
||||
window.game.pendingServerData = null;
|
||||
|
||||
window.game.loadServerPlayerData = function(serverData) {
|
||||
console.log('[SAVE INTEGRATION] Loading server player data into game');
|
||||
console.log('[SAVE INTEGRATION] Server data received:', serverData);
|
||||
console.log('[SAVE INTEGRATION] Server data type:', typeof serverData);
|
||||
console.log('[SAVE INTEGRATION] Server data keys:', serverData ? Object.keys(serverData) : 'No data');
|
||||
console.log('[SAVE INTEGRATION] Game systems available:', this.systems ? Object.keys(this.systems) : 'No systems');
|
||||
|
||||
// Store server data for later if systems aren't ready
|
||||
if (!this.systems || Object.keys(this.systems).length === 0) {
|
||||
console.log('[SAVE INTEGRATION] Game systems not ready, storing data for later');
|
||||
this.pendingServerData = serverData;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Game systems ready, applying server data now');
|
||||
|
||||
try {
|
||||
// Apply player stats
|
||||
if (serverData.stats && this.systems && this.systems.player) {
|
||||
console.log('[SAVE INTEGRATION] Applying player stats:', serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player system methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(this.systems.player)));
|
||||
|
||||
// Check if load method exists
|
||||
if (typeof this.systems.player.load === 'function') {
|
||||
this.systems.player.load(serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player stats applied successfully');
|
||||
console.log('[SAVE INTEGRATION] Updated player stats:', this.systems.player.stats);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Player system has no load method, trying direct assignment');
|
||||
// Direct assignment as fallback
|
||||
if (this.systems.player.stats) {
|
||||
Object.assign(this.systems.player.stats, serverData.stats);
|
||||
console.log('[SAVE INTEGRATION] Player stats assigned directly:', this.systems.player.stats);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] No player system or stats in server data');
|
||||
console.log('[SAVE INTEGRATION] Has serverData.stats:', !!serverData?.stats);
|
||||
console.log('[SAVE INTEGRATION] Has systems.player:', !!(this.systems?.player));
|
||||
}
|
||||
|
||||
// Apply inventory
|
||||
if (serverData.inventory && this.systems && this.systems.inventory) {
|
||||
console.log('[SAVE INTEGRATION] Applying player inventory:', serverData.inventory);
|
||||
if (typeof this.systems.inventory.load === 'function') {
|
||||
this.systems.inventory.load(serverData.inventory);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Inventory system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ship data
|
||||
if (serverData.ship && this.systems && this.systems.ship) {
|
||||
console.log('[SAVE INTEGRATION] Applying player ship:', serverData.ship);
|
||||
if (typeof this.systems.ship.load === 'function') {
|
||||
this.systems.ship.load(serverData.ship);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Ship system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply base data
|
||||
if (serverData.base && this.systems && this.systems.base) {
|
||||
console.log('[SAVE INTEGRATION] Applying player base:', serverData.base);
|
||||
if (typeof this.systems.base.load === 'function') {
|
||||
this.systems.base.load(serverData.base);
|
||||
} else {
|
||||
console.warn('[SAVE INTEGRATION] Base system has no load method');
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification
|
||||
if (this.showNotification) {
|
||||
this.showNotification(`Welcome back! Level ${serverData.stats?.level || 1}`, 'success', 3000);
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Server player data application completed');
|
||||
|
||||
// Force UI update
|
||||
if (this.systems && this.systems.ui && this.systems.ui.updateUI) {
|
||||
this.systems.ui.updateUI();
|
||||
console.log('[SAVE INTEGRATION] Server player data application completed');
|
||||
}
|
||||
|
||||
// Apply pending server data if any exists
|
||||
if (this.pendingServerData) {
|
||||
console.log('[SAVE INTEGRATION] Applying pending server data');
|
||||
this.loadServerPlayerData(this.pendingServerData);
|
||||
this.pendingServerData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SAVE INTEGRATION] Error applying server player data:', error);
|
||||
if (this.showNotification) {
|
||||
this.showNotification('Failed to load server data!', 'error', 3000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Method to check and apply pending server data
|
||||
window.game.checkAndApplyPendingServerData = function() {
|
||||
if (this.pendingServerData && this.systems && Object.keys(this.systems).length > 0) {
|
||||
console.log('[SAVE INTEGRATION] Systems ready, applying pending server data');
|
||||
this.loadServerPlayerData(this.pendingServerData);
|
||||
this.pendingServerData = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback loadPlayerData method if GameEngine doesn't have it
|
||||
if (!window.game.loadPlayerData) {
|
||||
window.game.loadPlayerData = window.game.loadServerPlayerData;
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Server data support added to game');
|
||||
}
|
||||
}
|
||||
|
||||
// Add save mode switching to UI
|
||||
function addSaveModeUI() {
|
||||
// Add save mode indicator to UI
|
||||
const createSaveModeIndicator = () => {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.id = 'save-mode-indicator';
|
||||
indicator.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
`;
|
||||
document.body.appendChild(indicator);
|
||||
return indicator;
|
||||
};
|
||||
|
||||
const updateSaveModeIndicator = () => {
|
||||
const indicator = document.getElementById('save-mode-indicator') || createSaveModeIndicator();
|
||||
|
||||
if (window.smartSaveManager) {
|
||||
const info = window.smartSaveManager.getSaveInfo();
|
||||
indicator.textContent = `Save: ${info.saveLocation}`;
|
||||
indicator.style.display = 'block';
|
||||
|
||||
// Color code based on mode
|
||||
indicator.style.background = info.isMultiplayer ? 'rgba(0, 100, 200, 0.8)' : 'rgba(0, 150, 0, 0.8)';
|
||||
}
|
||||
};
|
||||
|
||||
// Update indicator when mode changes
|
||||
if (window.smartSaveManager) {
|
||||
const originalSetMultiplayerMode = window.smartSaveManager.setMultiplayerMode;
|
||||
window.smartSaveManager.setMultiplayerMode = function(...args) {
|
||||
originalSetMultiplayerMode.apply(this, args);
|
||||
updateSaveModeIndicator();
|
||||
};
|
||||
}
|
||||
|
||||
// Initial update
|
||||
setTimeout(updateSaveModeIndicator, 1000);
|
||||
}
|
||||
|
||||
// Debug function to check data flow
|
||||
function debugDataFlow() {
|
||||
console.log('[DEBUG] === DATA FLOW DEBUG ===');
|
||||
|
||||
// Check GameInitializer
|
||||
if (window.gameInitializer) {
|
||||
console.log('[DEBUG] GameInitializer exists:', !!window.gameInitializer);
|
||||
console.log('[DEBUG] GameInitializer serverPlayerData:', window.gameInitializer.serverPlayerData);
|
||||
console.log('[DEBUG] GameInitializer gameMode:', window.gameInitializer.gameMode);
|
||||
} else {
|
||||
console.log('[DEBUG] GameInitializer NOT found');
|
||||
}
|
||||
|
||||
// Check game systems
|
||||
if (window.game) {
|
||||
console.log('[DEBUG] Game exists:', !!window.game);
|
||||
console.log('[DEBUG] Game systems:', window.game.systems ? Object.keys(window.game.systems) : 'No systems');
|
||||
|
||||
if (window.game.systems && window.game.systems.player) {
|
||||
console.log('[DEBUG] Player system exists:', !!window.game.systems.player);
|
||||
console.log('[DEBUG] Player stats:', window.game.systems.player.stats);
|
||||
console.log('[DEBUG] Player credits:', window.game.systems.player.stats?.credits);
|
||||
}
|
||||
} else {
|
||||
console.log('[DEBUG] Game NOT found');
|
||||
}
|
||||
|
||||
// Check SmartSaveManager
|
||||
if (window.smartSaveManager) {
|
||||
console.log('[DEBUG] SmartSaveManager exists:', !!window.smartSaveManager);
|
||||
console.log('[DEBUG] SmartSaveManager mode:', window.smartSaveManager.isMultiplayer ? 'multiplayer' : 'singleplayer');
|
||||
} else {
|
||||
console.log('[DEBUG] SmartSaveManager NOT found');
|
||||
}
|
||||
|
||||
console.log('[DEBUG] === END DEBUG ===');
|
||||
}
|
||||
|
||||
// Debug function available for manual testing
|
||||
window.debugDataFlow = debugDataFlow;
|
||||
|
||||
// Enhanced debug function for connection testing
|
||||
window.debugConnectionState = function() {
|
||||
console.log('=== CONNECTION STATE DEBUG ===');
|
||||
console.log('GameInitializer exists:', !!window.gameInitializer);
|
||||
console.log('GameInitializer socket connected:', !!window.gameInitializer?.socket?.connected);
|
||||
console.log('GameInitializer gameMode:', window.gameInitializer?.gameMode);
|
||||
console.log('GameInitializer serverPlayerData:', !!window.gameInitializer?.serverPlayerData);
|
||||
console.log('SmartSaveManager exists:', !!window.smartSaveManager);
|
||||
console.log('SmartSaveManager mode:', window.smartSaveManager?.isMultiplayer ? 'multiplayer' : 'singleplayer');
|
||||
console.log('Game exists:', !!window.game);
|
||||
console.log('Game isRunning:', window.game?.isRunning);
|
||||
console.log('=== END CONNECTION DEBUG ===');
|
||||
};
|
||||
|
||||
// Initialize integration when DOM is ready
|
||||
function initializeIntegration() {
|
||||
console.log('[SAVE INTEGRATION] Initializing save system integration');
|
||||
|
||||
// Wait for game to be ready
|
||||
const checkGameReady = () => {
|
||||
if (window.game) {
|
||||
integrateWithGameEngine();
|
||||
integrateLoadSystem();
|
||||
addServerDataSupport();
|
||||
addSaveModeUI();
|
||||
console.log('[SAVE INTEGRATION] Integration complete');
|
||||
} else {
|
||||
setTimeout(checkGameReady, 500);
|
||||
}
|
||||
};
|
||||
|
||||
checkGameReady();
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeIntegration);
|
||||
} else {
|
||||
initializeIntegration();
|
||||
}
|
||||
|
||||
console.log('[SAVE INTEGRATION] Save system integration loaded');
|
||||
@ -1,228 +0,0 @@
|
||||
/**
|
||||
* Smart Save Manager
|
||||
* Intelligently handles save data for both singleplayer and multiplayer modes
|
||||
*/
|
||||
|
||||
class SmartSaveManager {
|
||||
constructor() {
|
||||
this.isMultiplayer = false;
|
||||
this.serverPlayerData = null;
|
||||
this.localSaveData = null;
|
||||
this.gameInitializer = null;
|
||||
|
||||
console.log('[SMART SAVE] SmartSaveManager initialized');
|
||||
}
|
||||
|
||||
setMultiplayerMode(isMultiplayer, gameInitializer = null) {
|
||||
const oldMode = this.isMultiplayer;
|
||||
this.isMultiplayer = isMultiplayer;
|
||||
this.gameInitializer = gameInitializer;
|
||||
|
||||
console.log(`[SMART SAVE] Mode change: ${oldMode ? 'multiplayer' : 'singleplayer'} -> ${isMultiplayer ? 'multiplayer' : 'singleplayer'}`);
|
||||
console.log(`[SMART SAVE] Set to ${isMultiplayer ? 'multiplayer' : 'singleplayer'} mode`);
|
||||
|
||||
if (isMultiplayer && gameInitializer) {
|
||||
// Load server data when switching to multiplayer
|
||||
this.loadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
// Load player data (intelligently chooses source)
|
||||
async loadPlayerData() {
|
||||
if (this.isMultiplayer) {
|
||||
return await this.loadServerData();
|
||||
} else {
|
||||
return await this.loadLocalData();
|
||||
}
|
||||
}
|
||||
|
||||
// Save player data (intelligently chooses destination)
|
||||
async savePlayerData(gameData) {
|
||||
if (this.isMultiplayer) {
|
||||
return await this.saveServerData(gameData);
|
||||
} else {
|
||||
return await this.saveLocalData(gameData);
|
||||
}
|
||||
}
|
||||
|
||||
// Load server data
|
||||
async loadServerData() {
|
||||
try {
|
||||
if (!this.gameInitializer || !this.gameInitializer.socket) {
|
||||
// Don't warn during initialization - this is expected before socket is ready
|
||||
// console.warn('[SMART SAVE] No multiplayer connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[SMART SAVE] Loading server player data');
|
||||
|
||||
// Request data from server
|
||||
this.gameInitializer.loadGameDataFromServer();
|
||||
|
||||
// Return cached server data if available
|
||||
return this.serverPlayerData;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error loading server data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save server data (DISABLED - client should not send data to server)
|
||||
async saveServerData(gameData) {
|
||||
console.warn('[SMART SAVE] Client save disabled - server is authoritative');
|
||||
return true; // Pretend it worked to avoid client errors
|
||||
}
|
||||
|
||||
// Load local data
|
||||
async loadLocalData() {
|
||||
try {
|
||||
console.log('[SMART SAVE] Loading local save data');
|
||||
|
||||
// Use existing local save system
|
||||
if (window.mainMenu && window.mainMenu.loadGame) {
|
||||
const saveData = await window.mainMenu.loadGame(1); // Load slot 1
|
||||
this.localSaveData = saveData;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
const saveKey = 'gso_save_slot_1';
|
||||
const saveData = localStorage.getItem(saveKey);
|
||||
|
||||
if (saveData) {
|
||||
const parsed = JSON.parse(saveData);
|
||||
this.localSaveData = parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error loading local data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save local data
|
||||
async saveLocalData(gameData) {
|
||||
try {
|
||||
// Don't save locally when in multiplayer mode
|
||||
if (this.isMultiplayer) {
|
||||
console.log('[SMART SAVE] Skipping local save - in multiplayer mode');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[SMART SAVE] Saving locally');
|
||||
|
||||
// Use existing local save system
|
||||
if (window.mainMenu && window.mainMenu.saveGame) {
|
||||
await window.mainMenu.saveGame(1, gameData); // Save to slot 1
|
||||
this.localSaveData = gameData;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
const saveKey = 'gso_save_slot_1';
|
||||
localStorage.setItem(saveKey, JSON.stringify(gameData));
|
||||
this.localSaveData = gameData;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SMART SAVE] Error saving local data:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply server data to game
|
||||
applyServerDataToGame(serverData) {
|
||||
console.log('[SMART SAVE] Applying server data to game');
|
||||
|
||||
this.serverPlayerData = serverData;
|
||||
|
||||
// Apply to game if game is running (try both methods)
|
||||
if (window.game) {
|
||||
console.log('[SMART SAVE] Game is available, checking for data loading methods');
|
||||
console.log('[SMART SAVE] - loadPlayerData:', !!window.game.loadPlayerData);
|
||||
console.log('[SMART SAVE] - loadServerPlayerData:', !!window.game.loadServerPlayerData);
|
||||
|
||||
if (window.game.loadServerPlayerData) {
|
||||
console.log('[SMART SAVE] Using loadServerPlayerData method');
|
||||
window.game.loadServerPlayerData(serverData);
|
||||
console.log('[SMART SAVE] Server data applied to game, forcing UI refresh');
|
||||
|
||||
// Force UI refresh after applying server data
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Forcing UI refresh after server data application');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] UI refresh not available after server data application - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
}
|
||||
} else if (window.game.loadPlayerData) {
|
||||
console.log('[SMART SAVE] Using loadPlayerData method');
|
||||
window.game.loadPlayerData(serverData);
|
||||
console.log('[SMART SAVE] Server data applied to game, forcing UI refresh');
|
||||
|
||||
// Force UI refresh after applying server data
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Forcing UI refresh after server data application');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] UI refresh not available after server data application - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
|
||||
// Try delayed UI refresh since UIManager might not be ready yet
|
||||
console.log('[SMART SAVE] Attempting delayed UI refresh...');
|
||||
setTimeout(() => {
|
||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||
console.log('[SMART SAVE] Delayed UI refresh successful');
|
||||
window.game.systems.ui.forceRefreshAllUI();
|
||||
} else {
|
||||
console.warn('[SMART SAVE] Delayed UI refresh also failed - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
|
||||
}
|
||||
}, 2000); // Wait 2 seconds for systems to initialize
|
||||
}
|
||||
} else {
|
||||
console.warn('[SMART SAVE] No data loading method available on game object');
|
||||
}
|
||||
} else {
|
||||
console.warn('[SMART SAVE] Game not available for data application');
|
||||
}
|
||||
|
||||
// Store for game engine
|
||||
if (window.gameInitializer) {
|
||||
window.gameInitializer.serverPlayerData = serverData;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current save source info
|
||||
getSaveInfo() {
|
||||
return {
|
||||
isMultiplayer: this.isMultiplayer,
|
||||
hasServerData: !!this.serverPlayerData,
|
||||
hasLocalData: !!this.localSaveData,
|
||||
saveLocation: this.isMultiplayer ? 'Server Database' : 'Local Storage'
|
||||
};
|
||||
}
|
||||
|
||||
// Sync data between local and server (for migration)
|
||||
async syncData(direction = 'toServer') {
|
||||
if (direction === 'toServer') {
|
||||
// Upload local data to server
|
||||
const localData = await this.loadLocalData();
|
||||
if (localData) {
|
||||
await this.saveServerData(localData);
|
||||
console.log('[SMART SAVE] Synced local data to server');
|
||||
}
|
||||
} else {
|
||||
// Download server data to local
|
||||
const serverData = await this.loadServerData();
|
||||
if (serverData) {
|
||||
await this.saveLocalData(serverData);
|
||||
console.log('[SMART SAVE] Synced server data to local');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.smartSaveManager = new SmartSaveManager();
|
||||
|
||||
console.log('[SMART SAVE] SmartSaveManager loaded and available globally');
|
||||
@ -1,179 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Debug Logger
|
||||
* Enhanced debugging that integrates with existing Logger system
|
||||
*/
|
||||
|
||||
class DebugLogger {
|
||||
constructor() {
|
||||
// Completely disable debug logging to prevent console flooding
|
||||
this.debugEnabled = false;
|
||||
|
||||
this.startTime = performance.now();
|
||||
this.stepTimers = new Map();
|
||||
this.debugLogs = []; // Store logs in memory
|
||||
this.maxLogs = 1000; // Limit memory usage
|
||||
|
||||
// Use the existing logger if available
|
||||
this.logger = window.logger || null;
|
||||
|
||||
// Log initialization
|
||||
if (this.debugEnabled) {
|
||||
this.log('=== DEBUG SESSION STARTED ===');
|
||||
}
|
||||
}
|
||||
|
||||
async log(message, data = null) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const stackTrace = new Error().stack;
|
||||
|
||||
// Build performance object
|
||||
const performanceData = {
|
||||
elapsed: `${(performance.now() - this.startTime).toFixed(2)}ms`,
|
||||
memory: performance.memory ? {
|
||||
used: `${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
|
||||
total: `${(performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`
|
||||
} : null
|
||||
};
|
||||
|
||||
// Create single formatted log message
|
||||
let logMessage = `[DEBUG] ${message}`;
|
||||
if (data) {
|
||||
logMessage += `\n${JSON.stringify(data, null, 2)}`;
|
||||
}
|
||||
if (performanceData) {
|
||||
logMessage += `\n[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory?.used || 'N/A'}/${performanceData.memory?.total || 'N/A'}`;
|
||||
}
|
||||
|
||||
// Add to memory logs
|
||||
const logEntry = {
|
||||
timestamp: timestamp,
|
||||
message: message,
|
||||
data: data ? JSON.stringify(data, null, 2) : '',
|
||||
stackTrace: stackTrace ? stackTrace.split('\n').slice(0, 3).join('\n') : '',
|
||||
performance: performanceData
|
||||
};
|
||||
this.debugLogs.push(logEntry);
|
||||
|
||||
// Limit memory usage
|
||||
if (this.debugLogs.length > this.maxLogs) {
|
||||
this.debugLogs.shift();
|
||||
}
|
||||
|
||||
// Skip console logging to prevent flooding
|
||||
// console.log(`[DEBUG] ${message}`, data || '');
|
||||
|
||||
// Skip performance logging to prevent flooding
|
||||
// if (performanceData.memory) {
|
||||
// console.log(`[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory.used}/${performanceData.memory.total}`);
|
||||
// }
|
||||
|
||||
// Use existing logger if available
|
||||
if (this.logger) {
|
||||
try {
|
||||
await this.logger.debug(logMessage);
|
||||
} catch (error) {
|
||||
console.error('[DEBUG LOGGER] Failed to log via existing logger:', error);
|
||||
}
|
||||
} else {
|
||||
// Fallback to electronAPI log
|
||||
if (window.electronAPI && window.electronAPI.log) {
|
||||
window.electronAPI.log('debug', logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async startStep(stepName) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
this.stepTimers.set(stepName, performance.now());
|
||||
await this.log(`STEP START: ${stepName}`, {
|
||||
type: 'step_start',
|
||||
step: stepName,
|
||||
elapsed: '0ms'
|
||||
});
|
||||
}
|
||||
|
||||
async endStep(stepName, data = null) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
const startTime = this.stepTimers.get(stepName);
|
||||
const duration = startTime ? (performance.now() - startTime).toFixed(2) : 'N/A';
|
||||
|
||||
this.stepTimers.delete(stepName);
|
||||
await this.log(`STEP END: ${stepName}`, {
|
||||
type: 'step_end',
|
||||
step: stepName,
|
||||
duration: `${duration}ms`,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async logStep(stepName, data = null) {
|
||||
// Skip logging if debug is disabled
|
||||
if (!this.debugEnabled) return;
|
||||
|
||||
await this.log(`STEP: ${stepName}`, {
|
||||
type: 'step',
|
||||
step: stepName,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return this.debugLogs;
|
||||
}
|
||||
|
||||
exportLogs() {
|
||||
const logText = this.debugLogs.map(entry =>
|
||||
`[${entry.timestamp}] ${entry.message}${entry.data ? '\n' + entry.data : ''}${entry.performance ? '\nPerf: ' + entry.performance.elapsed : ''}`
|
||||
).join('\n\n');
|
||||
|
||||
return logText;
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
this.debugLogs = [];
|
||||
this.log('=== LOGS CLEARED ===');
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
await this.log('=== DEBUG SESSION ENDING ===');
|
||||
await this.log('SESSION SUMMARY', {
|
||||
totalLogs: this.debugLogs.length,
|
||||
sessionDuration: `${(performance.now() - this.startTime).toFixed(2)}ms`
|
||||
});
|
||||
|
||||
// No need to finalize files - the existing Logger handles that
|
||||
console.log('[DEBUG LOGGER] Session ended cleanly');
|
||||
}
|
||||
|
||||
// Convenience methods for specific logging types
|
||||
async info(message, data = null) {
|
||||
await this.log(`[INFO] ${message}`, data);
|
||||
}
|
||||
|
||||
async error(message, data = null) {
|
||||
await this.log(`[ERROR] ${message}`, data);
|
||||
}
|
||||
|
||||
async warn(message, data = null) {
|
||||
await this.log(`[WARN] ${message}`, data);
|
||||
}
|
||||
|
||||
async errorEvent(error, context = 'Unknown') {
|
||||
await this.error(`Error in ${context}`, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global debug logger instance
|
||||
window.debugLogger = new DebugLogger();
|
||||
@ -1,905 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Economy System
|
||||
* Manages player currency, transactions, and shop functionality
|
||||
* Now uses server-side ItemSystem for all item data
|
||||
*/
|
||||
|
||||
class Economy {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Currency - don't override in multiplayer mode, will be set by server data
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
this.credits = 0; // Will be updated by server
|
||||
this.gems = 0; // Will be updated by server
|
||||
this.premiumCurrency = 0; // Will be updated by server
|
||||
} else {
|
||||
this.credits = 10000; // Starting credits for singleplayer
|
||||
this.gems = 50; // Starting premium currency
|
||||
this.premiumCurrency = 0; // Additional premium currency
|
||||
}
|
||||
|
||||
// Transaction history
|
||||
this.transactions = [];
|
||||
|
||||
// Shop categories
|
||||
this.shopCategories = {
|
||||
ships: 'Ships',
|
||||
weapons: 'Weapons',
|
||||
armors: 'Armors',
|
||||
cosmetics: 'Cosmetics',
|
||||
consumables: 'Consumables',
|
||||
materials: 'Materials'
|
||||
};
|
||||
|
||||
// Random shop system - now uses server ItemSystem
|
||||
this.randomShopItems = {}; // Current random items per category
|
||||
this.shopRefreshInterval = null; // Timer for 2-hour refresh
|
||||
this.shopHeartbeatInterval = null; // Timer for live countdown updates
|
||||
this.lastShopRefresh = null; // Timestamp of last refresh
|
||||
this.currentShopData = null; // Current shop data from server
|
||||
this.SHOP_REFRESH_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
|
||||
this.MAX_ITEMS_PER_CATEGORY = 8;
|
||||
this.categoryPurchaseLimits = {}; // Track purchases per category per refresh
|
||||
|
||||
// Shop items - now loaded from server ItemSystem
|
||||
this.shopItems = null; // Will be populated by ItemSystem in multiplayer
|
||||
|
||||
// Owned cosmetics
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
// Owned ships
|
||||
this.ownedShips = [];
|
||||
|
||||
console.log('[ECONOMY] Economy system initialized');
|
||||
|
||||
// Initialize global purchase function
|
||||
Economy.initGlobalPurchaseFunction();
|
||||
}
|
||||
|
||||
// Create global purchase function for shop buttons
|
||||
static initGlobalPurchaseFunction() {
|
||||
window.purchaseShopItem = function(itemId) {
|
||||
console.log('[GLOBAL] Purchase shop item called:', itemId);
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.purchaseItem(itemId, 1);
|
||||
} else {
|
||||
console.error('[GLOBAL] Economy system not available for purchase');
|
||||
}
|
||||
};
|
||||
|
||||
// Add test function for idle system
|
||||
window.testIdleRewards = function() {
|
||||
console.log('[GLOBAL] Testing idle rewards...');
|
||||
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('testIdleRewards', {});
|
||||
|
||||
// Listen for response
|
||||
window.game.socket.once('testIdleRewards', (data) => {
|
||||
console.log('[GLOBAL] Test idle rewards response:', data);
|
||||
});
|
||||
} else {
|
||||
console.error('[GLOBAL] No socket available for idle test');
|
||||
}
|
||||
};
|
||||
|
||||
// Add socket event monitor
|
||||
window.monitorSocketEvents = function() {
|
||||
if (window.game && window.game.socket) {
|
||||
console.log('[GLOBAL] Monitoring socket events...');
|
||||
|
||||
// Monitor all incoming events
|
||||
const originalOn = window.game.socket.on;
|
||||
window.game.socket.on = function(event, callback) {
|
||||
const wrappedCallback = function(data) {
|
||||
if (event === 'onlineIdleRewards' || event === 'economy_data') {
|
||||
console.log('[SOCKET MONITOR] Received event:', event, data);
|
||||
}
|
||||
return callback(data);
|
||||
};
|
||||
return originalOn.call(this, event, wrappedCallback);
|
||||
};
|
||||
|
||||
console.log('[GLOBAL] Socket event monitoring enabled');
|
||||
} else {
|
||||
console.error('[GLOBAL] No socket available for monitoring');
|
||||
}
|
||||
};
|
||||
|
||||
// Add function to give player energy for testing dungeons
|
||||
window.addEnergy = function(amount = 50) {
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
const player = window.game.systems.player;
|
||||
const oldEnergy = player.attributes.energy || 0;
|
||||
player.attributes.energy = Math.min(oldEnergy + amount, player.attributes.maxEnergy || 100);
|
||||
console.log('[GLOBAL] Added energy:', oldEnergy, '->', player.attributes.energy);
|
||||
|
||||
// Update UI
|
||||
if (player.updateUI) {
|
||||
player.updateUI();
|
||||
}
|
||||
|
||||
// Update dungeon UI if available
|
||||
if (window.game.systems.dungeonSystem && window.game.systems.dungeonSystem.updateUI) {
|
||||
window.game.systems.dungeonSystem.updateUI();
|
||||
}
|
||||
|
||||
return player.attributes.energy;
|
||||
} else {
|
||||
console.error('[GLOBAL] Player system not available');
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[GLOBAL] Global functions initialized - purchaseShopItem() and testIdleRewards() available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up socket listeners for economy data synchronization
|
||||
*/
|
||||
setupSocketListeners() {
|
||||
if (!this.game.socket) {
|
||||
console.warn('[ECONOMY] No socket available for economy sync');
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for economy data updates from server
|
||||
this.game.socket.on('economy_data', (data) => {
|
||||
console.log('[ECONOMY] Received economy data from server:', data);
|
||||
console.log('[ECONOMY] Current credits before update:', this.credits);
|
||||
console.log('[ECONOMY] Current gems before update:', this.gems);
|
||||
|
||||
this.credits = data.credits || 0;
|
||||
this.gems = data.gems || 0;
|
||||
|
||||
console.log('[ECONOMY] Updated credits:', this.credits);
|
||||
console.log('[ECONOMY] Updated gems:', this.gems);
|
||||
|
||||
// Update UI immediately
|
||||
if (this.game.ui) {
|
||||
this.game.ui.updatePlayerStats();
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Economy synced - Credits:', this.credits, 'Gems:', this.gems);
|
||||
});
|
||||
|
||||
// Note: onlineIdleRewards is handled by GameInitializer to avoid duplicate event handling
|
||||
|
||||
// Listen for play time updates from server
|
||||
this.game.socket.on('playTimeUpdated', (data) => {
|
||||
console.log('[ECONOMY] Received play time update from server:', data);
|
||||
|
||||
// Update player stats if available
|
||||
if (this.game.systems.player && this.game.systems.player.stats) {
|
||||
this.game.systems.player.stats.playTime = data.playTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request economy data from server
|
||||
*/
|
||||
requestEconomyData() {
|
||||
if (this.game.socket) {
|
||||
console.log('[ECONOMY] Requesting economy data from server');
|
||||
this.game.socket.emit('get_economy_data');
|
||||
} else {
|
||||
console.warn('[ECONOMY] Cannot request economy data - no socket available');
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('[ECONOMY] Initializing economy system');
|
||||
|
||||
// In multiplayer mode, wait for ItemSystem to be ready (handled by event listener)
|
||||
this.game.on('itemSystemReady', () => {
|
||||
console.log('[ECONOMY] ItemSystem is ready, updating shop UI');
|
||||
this.updateShopUI();
|
||||
});
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
console.log('[ECONOMY] Multiplayer mode - waiting for ItemSystem to be ready');
|
||||
// ItemSystem initialization removed - wait for event instead
|
||||
} else {
|
||||
console.log('[ECONOMY] Singleplayer mode - using local shop data');
|
||||
// Initialize random shop for singleplayer
|
||||
this.initializeRandomShop();
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Economy system initialized');
|
||||
}
|
||||
|
||||
// Shop functionality - now uses ItemSystem in multiplayer
|
||||
purchaseItem(itemId, quantity = 1) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// In multiplayer mode, send request to server
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
if (debugLogger) debugLogger.logStep('Sending purchase request to server', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
// Send purchase request to server
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('purchaseItem', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
// Show loading message
|
||||
this.game.showNotification('Processing purchase...', 'info', 2000);
|
||||
} else {
|
||||
this.game.showNotification('Not connected to server', 'error', 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Singleplayer mode - use local logic
|
||||
const item = this.findShopItem(itemId);
|
||||
|
||||
if (!item) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - item not found', {
|
||||
itemId: itemId,
|
||||
quantity: quantity
|
||||
});
|
||||
this.game.showNotification('Item not found in shop', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
const totalCost = item.price * quantity;
|
||||
const currency = item.currency;
|
||||
const oldCredits = this.credits;
|
||||
const oldGems = this.gems;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Item purchase attempted', {
|
||||
itemId: itemId,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
quantity: quantity,
|
||||
unitPrice: item.price,
|
||||
totalCost: totalCost,
|
||||
currency: currency,
|
||||
currentCredits: oldCredits,
|
||||
currentGems: oldGems
|
||||
});
|
||||
|
||||
// Check if player can afford
|
||||
if (currency === 'credits' && this.credits < totalCost) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient credits', {
|
||||
totalCost: totalCost,
|
||||
currentCredits: oldCredits,
|
||||
deficit: totalCost - oldCredits
|
||||
});
|
||||
this.game.showNotification('Not enough credits!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currency === 'gems' && this.gems < totalCost) {
|
||||
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient gems', {
|
||||
totalCost: totalCost,
|
||||
currentGems: oldGems,
|
||||
deficit: totalCost - oldGems
|
||||
});
|
||||
this.game.showNotification('Not enough gems!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already owns this cosmetic
|
||||
if (item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id)) {
|
||||
this.game.showNotification('You already own this cosmetic!', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process payment and give item based on type
|
||||
if (currency === 'credits') {
|
||||
this.credits -= totalCost;
|
||||
} else if (currency === 'gems') {
|
||||
this.gems -= totalCost;
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case 'ship':
|
||||
this.purchaseShip(item, quantity);
|
||||
break;
|
||||
case 'cosmetic':
|
||||
this.purchaseCosmetic(item, quantity);
|
||||
break;
|
||||
case 'consumable':
|
||||
this.purchaseConsumable(item, quantity);
|
||||
break;
|
||||
case 'material':
|
||||
this.purchaseMaterial(item, quantity);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ECONOMY] Unknown item type: ${item.type}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
this.game.showNotification(`Purchased ${item.name}!`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Item purchase completed successfully', {
|
||||
itemId: itemId,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
quantity: quantity,
|
||||
totalCost: totalCost,
|
||||
currency: currency,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: this.credits,
|
||||
oldGems: oldGems,
|
||||
newGems: this.gems
|
||||
});
|
||||
|
||||
// Update UI without calling updateShopUI to avoid circular updates
|
||||
return true;
|
||||
}
|
||||
|
||||
findShopItem(itemId) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[ECONOMY] Looking for shop item:', itemId);
|
||||
console.log('[ECONOMY] Multiplayer mode:', window.smartSaveManager?.isMultiplayer);
|
||||
console.log('[ECONOMY] ItemSystem available:', !!(this.game.systems.itemSystem));
|
||||
|
||||
// In multiplayer mode, use ItemSystem (required)
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
// Check if ItemSystem is ready before using it
|
||||
if (!this.game.systems.itemSystem || !this.game.systems.itemSystem.itemCatalog) {
|
||||
console.log('[ECONOMY] ItemSystem not ready yet, cannot find shop item');
|
||||
if (debugLogger) debugLogger.logStep('Shop item lookup failed - ItemSystem not ready', {
|
||||
itemId: itemId,
|
||||
multiplayer: true
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search in ItemSystem catalog
|
||||
const item = this.game.systems.itemSystem.itemCatalog.get(itemId);
|
||||
if (item) {
|
||||
console.log('[ECONOMY] Found item in ItemSystem:', item.name);
|
||||
return item;
|
||||
} else {
|
||||
console.log('[ECONOMY] Item not found in ItemSystem:', itemId);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Singleplayer mode - search in local random shop
|
||||
for (const categoryItems of Object.values(this.randomShopItems)) {
|
||||
const item = categoryItems.find(item => item.id === itemId);
|
||||
if (item) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Purchase methods
|
||||
purchaseShip(ship) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const player = this.game.systems.player;
|
||||
const oldShipName = player.ship.name;
|
||||
const oldShipClass = player.ship.class;
|
||||
const oldAttributes = { ...player.attributes };
|
||||
|
||||
// Update player ship
|
||||
player.ship = {
|
||||
name: ship.name,
|
||||
class: ship.id,
|
||||
texture: ship.texture,
|
||||
stats: ship.stats || {}
|
||||
};
|
||||
|
||||
// Update player attributes
|
||||
if (ship.stats) {
|
||||
player.attributes = { ...player.attributes, ...ship.stats };
|
||||
}
|
||||
|
||||
// Add to owned ships
|
||||
if (!player.ownedShips) {
|
||||
player.ownedShips = [];
|
||||
}
|
||||
if (!player.ownedShips.includes(ship.id)) {
|
||||
player.ownedShips.push(ship.id);
|
||||
}
|
||||
|
||||
// Add ship to BaseSystem ship gallery (singleplayer)
|
||||
if (this.game.systems.baseSystem) {
|
||||
const shipData = {
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.name.replace(/\s+/g, '_').toLowerCase(), // Generate class from name
|
||||
level: 1,
|
||||
stats: ship.stats || {},
|
||||
texture: ship.texture || `assets/textures/ships/${ship.id}.png`,
|
||||
isCurrent: false,
|
||||
rarity: ship.rarity || 'common'
|
||||
};
|
||||
|
||||
// Initialize ship gallery if needed
|
||||
if (!this.game.systems.baseSystem.purchasedShips) {
|
||||
this.game.systems.baseSystem.initializeShipGallery();
|
||||
}
|
||||
|
||||
// Add ship to gallery
|
||||
this.game.systems.baseSystem.purchasedShips.push(shipData);
|
||||
|
||||
// Update the ship gallery UI
|
||||
this.game.systems.baseSystem.updateShipGallery();
|
||||
|
||||
console.log('[ECONOMY] Ship added to BaseSystem gallery (singleplayer):', shipData.name);
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship purchase completed', {
|
||||
shipId: ship.id,
|
||||
shipName: ship.name,
|
||||
oldShipName: oldShipName,
|
||||
oldShipClass: oldShipClass,
|
||||
newShipName: ship.name,
|
||||
newShipClass: ship.id,
|
||||
oldAttributes: oldAttributes,
|
||||
newAttributes: player.attributes
|
||||
});
|
||||
}
|
||||
|
||||
purchaseCosmetic(cosmetic) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldOwnedCount = this.ownedCosmetics.length;
|
||||
|
||||
// Add to owned cosmetics
|
||||
this.ownedCosmetics.push(cosmetic.id);
|
||||
this.game.showNotification(`Cosmetic unlocked: ${cosmetic.name}`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Cosmetic purchase completed', {
|
||||
cosmeticId: cosmetic.id,
|
||||
cosmeticName: cosmetic.name,
|
||||
oldOwnedCount: oldOwnedCount,
|
||||
newOwnedCount: this.ownedCosmetics.length,
|
||||
totalOwnedCosmetics: this.ownedCosmetics.length
|
||||
});
|
||||
}
|
||||
|
||||
purchaseConsumable(consumable, quantity) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const inventory = this.game.systems.inventory;
|
||||
|
||||
// Create item object for inventory
|
||||
const item = {
|
||||
id: consumable.id,
|
||||
name: consumable.name,
|
||||
type: consumable.type,
|
||||
rarity: consumable.rarity,
|
||||
quantity: quantity,
|
||||
description: consumable.description,
|
||||
texture: consumable.texture,
|
||||
stats: consumable.stats || {},
|
||||
acquired: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
const oldInventorySize = inventory.items.length;
|
||||
inventory.addItem(item);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Consumable purchase completed', {
|
||||
itemId: consumable.id,
|
||||
itemName: consumable.name,
|
||||
quantity: quantity,
|
||||
oldInventorySize: oldInventorySize,
|
||||
newInventorySize: inventory.items.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ECONOMY] Error adding consumable to inventory:', error);
|
||||
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
purchaseMaterial(material, quantity) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const inventory = this.game.systems.inventory;
|
||||
|
||||
// Create item object for inventory
|
||||
const item = {
|
||||
id: material.id,
|
||||
name: material.name,
|
||||
type: material.type,
|
||||
rarity: material.rarity,
|
||||
quantity: quantity,
|
||||
description: material.description,
|
||||
texture: material.texture,
|
||||
stackable: material.stackable || true,
|
||||
acquired: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
const oldInventorySize = inventory.items.length;
|
||||
inventory.addItem(item);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Material purchase completed', {
|
||||
itemId: material.id,
|
||||
itemName: material.name,
|
||||
quantity: quantity,
|
||||
oldInventorySize: oldInventorySize,
|
||||
newInventorySize: inventory.items.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ECONOMY] Error adding material to inventory:', error);
|
||||
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Currency management
|
||||
addCredits(amount, source = 'unknown') {
|
||||
const oldCredits = this.credits;
|
||||
this.credits += amount;
|
||||
|
||||
// Add transaction
|
||||
this.addTransaction({
|
||||
type: 'credit',
|
||||
amount: amount,
|
||||
source: source,
|
||||
balance: this.credits,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`[ECONOMY] Added ${amount} credits from ${source}. New balance: ${this.credits}`);
|
||||
this.updateUI();
|
||||
|
||||
return this.credits - oldCredits;
|
||||
}
|
||||
|
||||
addGems(amount, source = 'unknown') {
|
||||
const oldGems = this.gems;
|
||||
this.gems += amount;
|
||||
|
||||
// Add transaction
|
||||
this.addTransaction({
|
||||
type: 'gem',
|
||||
amount: amount,
|
||||
source: source,
|
||||
balance: this.gems,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`[ECONOMY] Added ${amount} gems from ${source}. New balance: ${this.gems}`);
|
||||
this.updateUI();
|
||||
|
||||
return this.gems - oldGems;
|
||||
}
|
||||
|
||||
canAfford(cost, currency = 'credits') {
|
||||
if (currency === 'credits') {
|
||||
return this.credits >= cost;
|
||||
} else if (currency === 'gems') {
|
||||
return this.gems >= cost;
|
||||
} else if (currency === 'premium') {
|
||||
return this.premiumCurrency >= cost;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transaction management
|
||||
addTransaction(transaction) {
|
||||
this.transactions.push(transaction);
|
||||
this.transactionHistory.push(transaction);
|
||||
|
||||
// Keep only last 100 transactions in memory
|
||||
if (this.transactions.length > 100) {
|
||||
this.transactions = this.transactions.slice(-100);
|
||||
}
|
||||
}
|
||||
|
||||
// Manual sync with server data - call this to force update
|
||||
syncWithServerData(serverPlayerData) {
|
||||
console.log('[ECONOMY] Manual sync with server data:', {
|
||||
serverCredits: serverPlayerData?.stats?.credits,
|
||||
serverGems: serverPlayerData?.stats?.gems,
|
||||
currentCredits: this.credits,
|
||||
currentGems: this.gems
|
||||
});
|
||||
|
||||
if (serverPlayerData?.stats?.credits !== undefined) {
|
||||
this.credits = serverPlayerData.stats.credits;
|
||||
console.log('[ECONOMY] Updated credits from server:', this.credits);
|
||||
}
|
||||
|
||||
if (serverPlayerData?.stats?.gems !== undefined) {
|
||||
this.gems = serverPlayerData.stats.gems;
|
||||
console.log('[ECONOMY] Updated gems from server:', this.gems);
|
||||
}
|
||||
|
||||
// Update UI after sync
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
// Debug logging to track current values
|
||||
console.log('[ECONOMY] updateUI called - Current values:', {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
gameSystemsAvailable: !!(this.game && this.game.systems),
|
||||
uiSystemAvailable: !!(this.game && this.game.systems && this.game.systems.ui)
|
||||
});
|
||||
|
||||
// Update resource display
|
||||
if (this.game.systems.ui) {
|
||||
this.game.systems.ui.updateResourceDisplay();
|
||||
}
|
||||
|
||||
// Update shop UI if open
|
||||
this.updateShopUI();
|
||||
}
|
||||
|
||||
updateShopUI() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[ECONOMY] updateShopUI called');
|
||||
|
||||
if (this.game.multiplayerMode && this.game.itemSystem) {
|
||||
// Support both .catalog getter (new) and .shopItemsByCategory (legacy)
|
||||
const shopItems = this.game.itemSystem.catalog || this.game.itemSystem.shopItemsByCategory || {};
|
||||
const activeCategory = this.game.itemSystem.activeCategory || 'ships';
|
||||
const categoryItems = shopItems[activeCategory] || [];
|
||||
this.renderShopItems(categoryItems);
|
||||
} else {
|
||||
// Singleplayer mode - use local shop data
|
||||
const items = Object.values(this.randomShopItems).flat();
|
||||
|
||||
// Convert to categorized structure for consistency
|
||||
const categorizedItems = this.randomShopItems || {};
|
||||
this.renderShopItems(categorizedItems);
|
||||
}
|
||||
}
|
||||
|
||||
renderShopItems(items) {
|
||||
const shopItemsElement = document.getElementById('shopItems');
|
||||
if (!shopItemsElement) return;
|
||||
|
||||
const activeCategory = document.querySelector('.shop-cat-btn.active')?.dataset.category || 'ships';
|
||||
console.log('[ECONOMY] Active shop category:', activeCategory);
|
||||
|
||||
// Handle new shop data structure (items by category) or old structure (flat array)
|
||||
let categoryItems = [];
|
||||
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
// New structure: { ships: [...], weapons: [...], ... }
|
||||
categoryItems = items[activeCategory] || [];
|
||||
console.log('[ECONOMY] Using new shop structure - found', Object.keys(items).length, 'categories');
|
||||
} else if (Array.isArray(items)) {
|
||||
// Old structure: flat array of items
|
||||
const targetItemType = activeCategory.slice(0, -1); // Remove 's' from 'ships', 'weapons', etc.
|
||||
categoryItems = items.filter(item => item.type === targetItemType);
|
||||
console.log('[ECONOMY] Using old shop structure - filtered', items.length, 'total items');
|
||||
} else {
|
||||
console.warn('[ECONOMY] Invalid shop items structure:', typeof items);
|
||||
shopItemsElement.innerHTML = '<p>No items available</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ECONOMY] Filtered items for category', activeCategory, ':', categoryItems.length, 'items');
|
||||
console.log('[ECONOMY] Item types in category:', categoryItems.map(item => item.type));
|
||||
|
||||
if (categoryItems.length === 0) {
|
||||
shopItemsElement.innerHTML = '<p>No items available in this category</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
shopItemsElement.innerHTML = categoryItems.map(item => {
|
||||
const canAfford = this.canAfford(item.price, item.currency);
|
||||
const isOwned = item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id);
|
||||
|
||||
// Generate image URL - server will serve images
|
||||
const imageUrl = this.getItemImageUrl(item);
|
||||
const placeholderUrl = 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
return `
|
||||
<div class="shop-item ${canAfford ? '' : 'cant-afford'} ${isOwned ? 'owned' : ''}" data-item-id="${item.id}">
|
||||
<div class="shop-item-content">
|
||||
<div class="shop-item-image">
|
||||
<img src="${imageUrl}" alt="${item.name}"
|
||||
onerror="this.src='${placeholderUrl}'"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<div class="shop-item-info">
|
||||
<div class="shop-item-header">
|
||||
<h3 class="shop-item-name">${item.name}</h3>
|
||||
<span class="shop-item-rarity ${item.rarity}">${item.rarity}</span>
|
||||
</div>
|
||||
<div class="shop-item-body">
|
||||
<p class="shop-item-description">${item.description}</p>
|
||||
<div class="shop-item-price">
|
||||
${this.formatPrice(item)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-item-footer">
|
||||
<button class="shop-item-purchase-btn"
|
||||
data-item-id="${item.id}"
|
||||
onclick="purchaseShopItem('${item.id}')"
|
||||
${!canAfford || isOwned ? 'disabled' : ''}>
|
||||
${isOwned ? 'Owned' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add event listeners to purchase buttons
|
||||
shopItemsElement.querySelectorAll('.shop-item-purchase-btn').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const itemId = button.getAttribute('data-item-id');
|
||||
if (itemId && !button.disabled) {
|
||||
console.log('[ECONOMY] Purchase button clicked for item:', itemId);
|
||||
this.purchaseItem(itemId, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
formatPrice(item) {
|
||||
if (!item.price) return 'Free';
|
||||
|
||||
const currency = item.currency || 'credits';
|
||||
const price = this.game.formatNumber(item.price);
|
||||
|
||||
return `${price} ${currency}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image URL for an item from server
|
||||
*/
|
||||
getItemImageUrl(item) {
|
||||
if (!item) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
// For multiplayer, ALWAYS get from server
|
||||
if (window.smartSaveManager?.isMultiplayer && this.game.socket) {
|
||||
const serverUrl = this.getServerUrl();
|
||||
|
||||
// Map item types to proper server paths
|
||||
switch (item.type) {
|
||||
case 'ship':
|
||||
return `${serverUrl}/images/ships/${item.id}.png`;
|
||||
case 'weapon':
|
||||
return `${serverUrl}/images/weapons/${item.id}.png`;
|
||||
case 'armor':
|
||||
return `${serverUrl}/images/armors/${item.id}.png`;
|
||||
case 'material':
|
||||
return `${serverUrl}/images/items/materials/${item.id}.png`;
|
||||
case 'consumable':
|
||||
return `${serverUrl}/images/items/consumables/${item.id}.png`;
|
||||
case 'cosmetic':
|
||||
return `${serverUrl}/images/items/cosmetics/${item.id}.png`;
|
||||
default:
|
||||
return `${serverUrl}/images/ui/placeholder.png`;
|
||||
}
|
||||
}
|
||||
|
||||
// For singleplayer, use local texture path (if available)
|
||||
if (item.texture) {
|
||||
return item.texture;
|
||||
}
|
||||
|
||||
// Fallback to server placeholder
|
||||
return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server URL for image requests
|
||||
*/
|
||||
getServerUrl() {
|
||||
// Get server URL from socket connection
|
||||
if (this.game.socket && this.game.socket.io && this.game.socket.io.uri) {
|
||||
return this.game.socket.io.uri.replace('/socket.io', '');
|
||||
}
|
||||
// Fallback to environment variable or production server
|
||||
return process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||
}
|
||||
|
||||
// Save/Load functionality
|
||||
save() {
|
||||
return {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactions: this.transactions,
|
||||
ownedCosmetics: this.ownedCosmetics,
|
||||
shopData: {
|
||||
randomShopItems: this.randomShopItems,
|
||||
categoryPurchaseLimits: this.categoryPurchaseLimits,
|
||||
lastShopRefresh: this.lastShopRefresh
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.credits !== undefined) this.credits = data.credits;
|
||||
if (data.gems !== undefined) this.gems = data.gems;
|
||||
if (data.premiumCurrency !== undefined) this.premiumCurrency = data.premiumCurrency;
|
||||
if (data.transactions) this.transactions = data.transactions;
|
||||
if (data.ownedCosmetics) this.ownedCosmetics = data.ownedCosmetics;
|
||||
|
||||
// Load shop data
|
||||
if (data.shopData) {
|
||||
this.randomShopItems = data.shopData.randomShopItems || {};
|
||||
this.categoryPurchaseLimits = data.shopData.categoryPurchaseLimits || {};
|
||||
this.lastShopRefresh = data.shopData.lastShopRefresh || null;
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
// Reset functionality
|
||||
reset() {
|
||||
const oldState = {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length
|
||||
};
|
||||
|
||||
this.credits = 1000;
|
||||
this.gems = 10;
|
||||
this.premiumCurrency = 0;
|
||||
this.transactions = [];
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
// Reset daily rewards
|
||||
localStorage.removeItem('lastDailyReward');
|
||||
|
||||
this.updateUI();
|
||||
|
||||
return oldState;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const oldState = {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length
|
||||
};
|
||||
|
||||
this.credits = 0;
|
||||
this.gems = 0;
|
||||
this.premiumCurrency = 0;
|
||||
this.transactions = [];
|
||||
this.ownedCosmetics = [];
|
||||
|
||||
this.updateUI();
|
||||
|
||||
return oldState;
|
||||
}
|
||||
|
||||
// Initialize random shop for singleplayer (minimal implementation)
|
||||
initializeRandomShop() {
|
||||
console.log('[ECONOMY] Random shop not available in singleplayer mode');
|
||||
this.randomShopItems = {};
|
||||
}
|
||||
|
||||
// Get system statistics
|
||||
getStats() {
|
||||
return {
|
||||
credits: this.credits,
|
||||
gems: this.gems,
|
||||
premiumCurrency: this.premiumCurrency,
|
||||
transactionCount: this.transactions.length,
|
||||
ownedCosmeticsCount: this.ownedCosmetics.length,
|
||||
shopItemsCount: this.game.systems.itemSystem && this.game.systems.itemSystem.itemCatalog ?
|
||||
this.game.systems.itemSystem.getStats().totalItems : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Economy;
|
||||
} else {
|
||||
window.Economy = Economy;
|
||||
}
|
||||
@ -1,753 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Game Engine
|
||||
* Core game loop and state management
|
||||
*/
|
||||
|
||||
class GameEngine extends EventTarget {
|
||||
constructor() {
|
||||
// Must call super() first since we extend EventTarget
|
||||
super();
|
||||
|
||||
// Basic game state
|
||||
this.isRunning = false;
|
||||
this.isPaused = false;
|
||||
this.gameTime = 0;
|
||||
this.lastSaveTime = 0;
|
||||
this.autoSaveInterval = 5000; // 5 seconds
|
||||
this.gameLogicInterval = 1000; // 1 second for game updates
|
||||
|
||||
// Game systems
|
||||
this.systems = {};
|
||||
|
||||
// Save slot configuration
|
||||
this.saveSlotInfo = {
|
||||
slot: 1,
|
||||
useFileSystem: true
|
||||
};
|
||||
|
||||
// Game state
|
||||
this.state = {
|
||||
paused: false,
|
||||
currentTab: 'dashboard',
|
||||
notifications: []
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
this.eventListeners = new Map();
|
||||
|
||||
// Initialize immediately
|
||||
this.init();
|
||||
}
|
||||
|
||||
setMultiplayerMode(isMultiplayer, socket = null, serverData = null, currentUser = null) {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[GAME ENGINE] Setting multiplayer mode:', isMultiplayer);
|
||||
console.log('[GAME ENGINE] Previous mode was:', this.isMultiplayer);
|
||||
if (debugLogger) debugLogger.logStep('setMultiplayerMode', { isMultiplayer, previousMode: this.isMultiplayer });
|
||||
|
||||
// CRITICAL: Once set to multiplayer, never allow fallback to singleplayer
|
||||
if (this.isMultiplayer && !isMultiplayer) {
|
||||
console.warn('[GAME ENGINE] ATTEMPTED FALLBACK TO SINGLEPLAYER - BLOCKING!');
|
||||
console.log('[GAME ENGINE] Preserving multiplayer mode');
|
||||
return; // Don't allow fallback to singleplayer
|
||||
}
|
||||
|
||||
this.isMultiplayer = isMultiplayer;
|
||||
this.socket = socket;
|
||||
this.serverData = serverData;
|
||||
this.currentUser = currentUser;
|
||||
|
||||
// Store multiplayer settings for systems that need them
|
||||
this.multiplayerConfig = {
|
||||
isMultiplayer,
|
||||
socket,
|
||||
serverData,
|
||||
currentUser
|
||||
};
|
||||
|
||||
console.log('[GAME ENGINE] Multiplayer mode configured:', {
|
||||
isMultiplayer,
|
||||
hasSocket: !!socket,
|
||||
hasServerData: !!serverData,
|
||||
hasCurrentUser: !!currentUser
|
||||
});
|
||||
}
|
||||
|
||||
// Get random integer between min and max (inclusive)
|
||||
getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// Get random float between min and max
|
||||
getRandomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('[GAME ENGINE] Initializing game engine');
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (logger) await logger.info('Initializing game engine');
|
||||
if (debugLogger) await debugLogger.startStep('gameEngineInit');
|
||||
|
||||
try {
|
||||
// In multiplayer mode, use simplified initialization to avoid hanging
|
||||
if (this.isMultiplayer) {
|
||||
console.log('[GAME ENGINE] Using simplified multiplayer initialization');
|
||||
try {
|
||||
await this.initializeMultiplayerSystems();
|
||||
console.log('[GAME ENGINE] Multiplayer initialization complete - skipping event listeners');
|
||||
} catch (multiplayerError) {
|
||||
console.error('[GAME ENGINE] Multiplayer systems initialization failed:', multiplayerError);
|
||||
// Don't fall back to singleplayer - keep multiplayer mode but with minimal systems
|
||||
console.log('[GAME ENGINE] Continuing with minimal multiplayer systems');
|
||||
// Create essential systems only
|
||||
this.systems.player = new Player(this);
|
||||
this.systems.inventory = new Inventory(this);
|
||||
this.systems.economy = new Economy(this);
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
}
|
||||
} else {
|
||||
// Full initialization for singleplayer
|
||||
await this.initializeSystemsForLoad();
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Systems initialized, setting up event listeners');
|
||||
|
||||
// Set up event listeners (only in singleplayer to avoid conflicts)
|
||||
await this.setupEventListeners();
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.endStep('gameEngineInit', {
|
||||
systemsInitialized: Object.keys(this.systems).length,
|
||||
eventListeners: this.eventListeners.size
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game:', error);
|
||||
if (logger) await logger.errorEvent(error, 'Game Engine Initialization');
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Game Engine Initialization');
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified multiplayer-only system initialization to avoid hanging
|
||||
async initializeMultiplayerSystems() {
|
||||
console.log('[GAME ENGINE] Initializing multiplayer systems');
|
||||
|
||||
try {
|
||||
// Initialize texture manager first
|
||||
this.systems.textureManager = new TextureManager(this);
|
||||
|
||||
// Create essential systems immediately
|
||||
console.log('[GAME ENGINE] Creating essential systems');
|
||||
this.systems.player = new Player(this);
|
||||
this.systems.inventory = new Inventory(this);
|
||||
this.systems.economy = new Economy(this);
|
||||
this.systems.ui = new UIManager(this);
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
|
||||
console.log('[GAME ENGINE] Essential systems created successfully');
|
||||
|
||||
// Initialize ItemSystem ONCE and emit event when ready
|
||||
console.log('[GAME ENGINE] Initializing ItemSystem (single initialization)');
|
||||
this.systems.itemSystem.initialize().then(() => {
|
||||
console.log('[GAME ENGINE] ItemSystem fully initialized, emitting ready event');
|
||||
this.emit('itemSystemReady');
|
||||
}).catch(error => {
|
||||
console.error('[GAME ENGINE] ItemSystem initialization failed:', error);
|
||||
// Still emit event so Economy can fallback gracefully
|
||||
this.emit('itemSystemReady');
|
||||
});
|
||||
|
||||
// Initialize Economy (without ItemSystem - it will wait for the event)
|
||||
console.log('[GAME ENGINE] Initializing Economy system');
|
||||
this.systems.economy.initialize().catch(error => {
|
||||
console.error('[GAME ENGINE] Economy initialization failed:', error);
|
||||
});
|
||||
|
||||
// Create additional systems asynchronously to avoid blocking
|
||||
setTimeout(() => {
|
||||
console.log('[GAME ENGINE] Creating additional multiplayer systems asynchronously');
|
||||
|
||||
if (typeof SkillSystem !== 'undefined') {
|
||||
this.systems.skillSystem = new SkillSystem(this);
|
||||
}
|
||||
if (typeof DungeonSystem !== 'undefined') {
|
||||
this.systems.dungeonSystem = new DungeonSystem(this);
|
||||
// Initialize server-driven dungeon system
|
||||
this.systems.dungeonSystem.initialize().then(() => {
|
||||
console.log('[GAME ENGINE] DungeonSystem initialized with server data');
|
||||
}).catch(error => {
|
||||
console.error('[GAME ENGINE] Failed to initialize DungeonSystem:', error);
|
||||
});
|
||||
}
|
||||
if (typeof QuestSystem !== 'undefined') {
|
||||
this.systems.questSystem = new QuestSystem(this);
|
||||
console.log('[GAME ENGINE] QuestSystem created');
|
||||
}
|
||||
if (typeof CraftingSystem !== 'undefined') {
|
||||
this.systems.crafting = new CraftingSystem(this);
|
||||
}
|
||||
if (typeof BaseSystem !== 'undefined') {
|
||||
this.systems.base = new BaseSystem(this);
|
||||
}
|
||||
console.log('[GAME ENGINE] All multiplayer systems created asynchronously');
|
||||
}, 100); // Create after 100ms delay
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error in multiplayer systems initialization:', error);
|
||||
// Don't re-throw - allow game to continue with basic systems
|
||||
console.log('[GAME ENGINE] Continuing with available systems');
|
||||
}
|
||||
}
|
||||
|
||||
async initializeSystemsForLoad() {
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) await debugLogger.startStep('initializeSystemsForLoad');
|
||||
|
||||
if (logger) {
|
||||
await logger.timeAsync('Game Systems Initialization for Load', async () => {
|
||||
await logger.info('Initializing game systems for loading');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Initializing TextureManager');
|
||||
// Initialize texture manager first
|
||||
this.systems.textureManager = new TextureManager(this);
|
||||
if (logger) await logger.systemEvent('TextureManager', 'Initialized');
|
||||
if (debugLogger) await debugLogger.logStep('TextureManager initialized');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Player system (without initialization)');
|
||||
// Create systems but don't initialize with default data
|
||||
this.systems.player = new Player(this);
|
||||
if (logger) await logger.systemEvent('Player', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Player system created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Inventory system (without initialization)');
|
||||
this.systems.inventory = new Inventory(this);
|
||||
if (logger) await logger.systemEvent('Inventory', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Inventory system created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating Economy system (without initialization)');
|
||||
this.systems.economy = new Economy(this);
|
||||
if (logger) await logger.systemEvent('Economy', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('Economy system created');
|
||||
|
||||
// In multiplayer mode, skip singleplayer systems
|
||||
if (!this.isMultiplayer) {
|
||||
if (debugLogger) await debugLogger.logStep('Creating IdleSystem');
|
||||
this.systems.idleSystem = new IdleSystem(this);
|
||||
if (logger) await logger.systemEvent('IdleSystem', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('IdleSystem created');
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating ItemSystem');
|
||||
this.systems.itemSystem = new ItemSystem(this);
|
||||
if (logger) await logger.systemEvent('ItemSystem', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('ItemSystem created');
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Multiplayer mode - skipping singleplayer systems (IdleSystem, ItemSystem)');
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.logStep('Creating UIManager');
|
||||
if (typeof UIManager !== 'undefined') {
|
||||
console.log('[GAME ENGINE] UIManager class found, creating real UIManager');
|
||||
this.systems.ui = new UIManager(this);
|
||||
// Expose UIManager globally for button onclick handlers
|
||||
window.uiManager = this.systems.ui;
|
||||
window.game.systems.ui = this.systems.ui;
|
||||
if (logger) await logger.systemEvent('UIManager', 'Created');
|
||||
if (debugLogger) await debugLogger.logStep('UIManager created and exposed');
|
||||
} else {
|
||||
console.error('[GAME ENGINE] UIManager class not found - this should not happen!');
|
||||
if (debugLogger) await debugLogger.error('UIManager class not found - this should not happen!');
|
||||
}
|
||||
|
||||
if (debugLogger) await debugLogger.endStep('initializeSystemsForLoad', {
|
||||
systemsCreated: Object.keys(this.systems).length
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async startGame(continueGame = false) {
|
||||
const logger = window.logger;
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
console.log('[GAME ENGINE] startGame called with continueGame =', continueGame);
|
||||
if (logger) await logger.info('Starting game', { continueGame });
|
||||
if (debugLogger) await debugLogger.startStep('startGame', { continueGame });
|
||||
|
||||
try {
|
||||
if (continueGame) {
|
||||
console.log('[GAME ENGINE] Loading existing save data...');
|
||||
if (debugLogger) await debugLogger.logStep('Loading existing save data');
|
||||
await this.loadGame();
|
||||
console.log('[GAME ENGINE] Save data loaded');
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Creating new game...');
|
||||
if (debugLogger) await debugLogger.logStep('Creating new game');
|
||||
await this.newGame();
|
||||
console.log('[GAME ENGINE] New game created');
|
||||
}
|
||||
|
||||
// Start game loop
|
||||
this.start();
|
||||
console.log('[GAME ENGINE] Game loop started');
|
||||
if (debugLogger) await debugLogger.logStep('Game loop started');
|
||||
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
if (loadingStatus) {
|
||||
console.log('[GAME ENGINE] Hiding loading status text');
|
||||
if (debugLogger) await debugLogger.logStep('Hiding loading status text');
|
||||
loadingStatus.classList.add('hidden');
|
||||
}
|
||||
|
||||
const gameInterface = document.getElementById('gameInterface');
|
||||
if (gameInterface) {
|
||||
console.log('[GAME ENGINE] Showing game interface');
|
||||
if (debugLogger) await debugLogger.logStep('Showing game interface');
|
||||
gameInterface.classList.remove('hidden');
|
||||
} else {
|
||||
console.warn('[GAME ENGINE] gameInterface element not found');
|
||||
if (debugLogger) await debugLogger.warn('gameInterface element not found');
|
||||
}
|
||||
|
||||
if (logger) await logger.info('Game started successfully');
|
||||
if (debugLogger) await debugLogger.endStep('startGame', {
|
||||
continueGame,
|
||||
isRunning: this.isRunning,
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Failed to start game:', error);
|
||||
if (logger) await logger.errorEvent(error, 'Game Start');
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Game Start');
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (this.isRunning) {
|
||||
if (debugLogger) debugLogger.log('GameEngine.start() called but game is already running', {
|
||||
isRunning: this.isRunning,
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Starting game engine', {
|
||||
gameLogicInterval: this.gameLogicInterval
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
// Start game logic timer (completely independent of frame rate)
|
||||
console.log('[GAME ENGINE] Starting game logic timer with interval:', this.gameLogicInterval);
|
||||
this.gameLogicTimer = setInterval(() => {
|
||||
this.updateGameLogic();
|
||||
}, this.gameLogicInterval);
|
||||
|
||||
// Start auto-save
|
||||
this.startAutoSave();
|
||||
|
||||
console.log('[GAME ENGINE] Game engine started');
|
||||
if (debugLogger) debugLogger.logStep('Game engine started successfully', {
|
||||
gameLogicInterval: this.gameLogicInterval,
|
||||
autoSaveInterval: this.autoSaveInterval
|
||||
});
|
||||
}
|
||||
|
||||
updateGameLogic() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// Use fixed 1-second interval for all updates
|
||||
const fixedDelta = 1000; // 1 second in milliseconds
|
||||
|
||||
if (this.state.paused) {
|
||||
if (debugLogger) debugLogger.logStep('Game logic update called but game is paused', {
|
||||
gameTime: this.gameTime,
|
||||
fixedDelta: fixedDelta
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.gameTime += fixedDelta;
|
||||
|
||||
// Update player play time with fixed delta
|
||||
if (this.systems.player && this.systems.player.updatePlayTime) {
|
||||
this.systems.player.updatePlayTime(fixedDelta);
|
||||
}
|
||||
|
||||
// Update all systems with fixed delta
|
||||
for (const [name, system] of Object.entries(this.systems)) {
|
||||
if (system && typeof system.update === 'function') {
|
||||
try {
|
||||
system.update(fixedDelta);
|
||||
} catch (error) {
|
||||
console.error(`[GAME ENGINE] Error updating ${name} system:`, error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, `Update ${name} system`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI displays (money, gems, energy) after system updates
|
||||
if (this.systems && this.systems.ui) {
|
||||
try {
|
||||
this.systems.ui.updateUI();
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error updating UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit game updated event
|
||||
this.emit('gameUpdated', { gameTime: this.gameTime });
|
||||
}
|
||||
|
||||
startAutoSave() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
// Load saved interval or use default
|
||||
const savedInterval = localStorage.getItem('autoSaveInterval');
|
||||
this.autoSaveInterval = savedInterval ? parseInt(savedInterval) : 5;
|
||||
|
||||
console.log(`[GAME ENGINE] Starting auto-save with ${this.autoSaveInterval} minute interval`);
|
||||
|
||||
// Clear any existing timer
|
||||
this.stopAutoSave();
|
||||
|
||||
// Set up new timer
|
||||
this.autoSaveTimer = setInterval(async () => {
|
||||
console.log('[GAME ENGINE] Auto-save timer triggered - isRunning:', this.isRunning, 'paused:', this.state.paused);
|
||||
|
||||
if (this.isRunning && !this.state.paused) {
|
||||
console.log('[GAME ENGINE] Auto-saving game...');
|
||||
|
||||
try {
|
||||
// In multiplayer mode, save to server
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
console.log('[GAME ENGINE] Auto-saving to server...');
|
||||
if (this.socket) {
|
||||
this.socket.emit('saveGameData', {
|
||||
timestamp: Date.now(),
|
||||
gameTime: this.gameTime
|
||||
});
|
||||
} else {
|
||||
console.warn('[GAME ENGINE] No socket available for server save');
|
||||
}
|
||||
} else {
|
||||
// Singleplayer mode - local save (not implemented yet)
|
||||
console.log('[GAME ENGINE] Local auto-save not implemented');
|
||||
}
|
||||
|
||||
this.showNotification('Game auto-saved', 'info', 2000);
|
||||
console.log('[GAME ENGINE] Auto-save completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Auto-save failed:', error);
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Auto-save');
|
||||
}
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Auto-save skipped - game not running or paused');
|
||||
}
|
||||
}, this.autoSaveInterval * 60 * 1000); // Convert minutes to milliseconds
|
||||
}
|
||||
|
||||
stopAutoSave() {
|
||||
if (this.autoSaveTimer) {
|
||||
console.log('[GAME ENGINE] Stopping auto-save timer');
|
||||
clearInterval(this.autoSaveTimer);
|
||||
this.autoSaveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification system
|
||||
async showNotification(message, type = 'info', duration = 3000) {
|
||||
const logger = window.logger;
|
||||
if (logger) await logger.playerAction('Notification', { message, type, duration });
|
||||
|
||||
const notification = {
|
||||
id: Date.now(),
|
||||
message,
|
||||
type,
|
||||
duration,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.state.notifications.push(notification);
|
||||
|
||||
// Auto-remove notification after duration
|
||||
setTimeout(() => {
|
||||
this.removeNotification(notification.id);
|
||||
}, duration);
|
||||
|
||||
// Update UI
|
||||
if (this.systems.ui) {
|
||||
// UI updates handled by individual systems
|
||||
}
|
||||
}
|
||||
|
||||
removeNotification(id) {
|
||||
this.state.notifications = this.state.notifications.filter(notification => notification.id !== id);
|
||||
}
|
||||
|
||||
// Event system
|
||||
on(event, callback) {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
this.eventListeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
this.eventListeners.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`[GAME ENGINE] Error in event listener for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also dispatch as DOM event for UIManager
|
||||
this.dispatchEvent(new CustomEvent(event, { detail: data }));
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
formatNumber(num) {
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceStats() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
const stats = {
|
||||
gameTime: this.gameTime,
|
||||
isRunning: this.isRunning,
|
||||
lastUpdate: this.lastUpdate,
|
||||
memory: null
|
||||
};
|
||||
|
||||
// Add memory info if available
|
||||
if (window.performance && window.performance.memory) {
|
||||
stats.memory = {
|
||||
used: window.performance.memory.usedJSHeapSize,
|
||||
total: window.performance.memory.totalJSHeapSize,
|
||||
limit: window.performance.memory.jsHeapSizeLimit
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Load server player data (transforms server format to client format)
|
||||
async loadServerPlayerData(playerData) {
|
||||
console.log('[GAME ENGINE] Loading server player data with format transformation');
|
||||
console.log('[GAME ENGINE] Original server data structure:', playerData);
|
||||
|
||||
// Transform server data format to client format
|
||||
const transformedData = {
|
||||
...playerData,
|
||||
|
||||
// Transform quests from server format to client format
|
||||
quests: playerData.quests ? {
|
||||
mainQuests: playerData.quests.main || [],
|
||||
dailyQuests: playerData.quests.daily || [],
|
||||
weeklyQuests: playerData.quests.weekly || [],
|
||||
tutorialQuests: playerData.quests.tutorial || [],
|
||||
activeQuests: playerData.quests.active || [],
|
||||
completedQuests: playerData.quests.completed || []
|
||||
} : {
|
||||
mainQuests: [],
|
||||
dailyQuests: [],
|
||||
weeklyQuests: [],
|
||||
tutorialQuests: [],
|
||||
activeQuests: [],
|
||||
completedQuests: []
|
||||
}
|
||||
};
|
||||
|
||||
// DEBUG: Log quest data transformation
|
||||
console.log('[GAME ENGINE] Quest data transformation:', {
|
||||
serverQuests: playerData.quests,
|
||||
transformedQuests: transformedData.quests,
|
||||
mainQuestsCount: transformedData.quests.mainQuests.length,
|
||||
dailyQuestsCount: transformedData.quests.dailyQuests.length,
|
||||
weeklyQuestsCount: transformedData.quests.weeklyQuests.length,
|
||||
tutorialQuestsCount: transformedData.quests.tutorialQuests.length
|
||||
});
|
||||
|
||||
// Use crafting data from server or initialize empty
|
||||
transformedData.crafting = playerData.crafting || {
|
||||
skill: 1,
|
||||
experience: 0,
|
||||
knownRecipes: [],
|
||||
completedDungeons: [],
|
||||
currentInstance: null,
|
||||
dungeonProgress: {}
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
async loadPlayerData(playerData) {
|
||||
console.log('[GAME ENGINE] Loading server player data');
|
||||
console.log('[GAME ENGINE] Full playerData structure:', playerData);
|
||||
console.log('[GAME ENGINE] PlayerData keys:', Object.keys(playerData));
|
||||
|
||||
try {
|
||||
// Apply basic player stats
|
||||
if (playerData.stats && this.systems && this.systems.player) {
|
||||
console.log('[GAME ENGINE] Found player stats and player system, applying...');
|
||||
console.log('[GAME ENGINE] Server playerData.stats:', playerData.stats);
|
||||
console.log('[GAME ENGINE] Server playerData keys:', Object.keys(playerData));
|
||||
|
||||
// Check for playTime in different possible locations
|
||||
const possiblePlayTimeFields = [
|
||||
playerData.stats?.playTime,
|
||||
playerData.playTime,
|
||||
playerData.totalPlayTime,
|
||||
playerData.stats?.totalPlayTime
|
||||
];
|
||||
|
||||
console.log('[GAME ENGINE] Possible playTime fields found:', possiblePlayTimeFields);
|
||||
|
||||
// Preserve existing playTime if server doesn't provide it
|
||||
const existingPlayTime = this.systems.player.stats.playTime || 0;
|
||||
console.log('[GAME ENGINE] Preserving existing playTime:', existingPlayTime);
|
||||
|
||||
this.systems.player.load(playerData.stats);
|
||||
console.log('[GAME ENGINE] Applied player stats:', playerData.stats);
|
||||
|
||||
// Restore playTime if it was lost
|
||||
if (!this.systems.player.stats.playTime || this.systems.player.stats.playTime === 0) {
|
||||
this.systems.player.stats.playTime = existingPlayTime;
|
||||
console.log('[GAME ENGINE] Restored playTime to:', existingPlayTime);
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Final playTime after load:', this.systems.player.stats.playTime);
|
||||
|
||||
// Apply credits from server data to economy system
|
||||
if (playerData.stats.credits !== undefined && this.systems.economy) {
|
||||
this.systems.economy.credits = playerData.stats.credits;
|
||||
console.log('[GAME ENGINE] Applied credits from server:', playerData.stats.credits);
|
||||
}
|
||||
|
||||
// Apply gems from server data to economy system
|
||||
if (playerData.stats.gems !== undefined && this.systems.economy) {
|
||||
this.systems.economy.gems = playerData.stats.gems;
|
||||
console.log('[GAME ENGINE] Applied gems from server:', playerData.stats.gems);
|
||||
}
|
||||
|
||||
// Force manual sync to ensure economy is updated
|
||||
if (this.systems.economy && this.systems.economy.syncWithServerData) {
|
||||
console.log('[GAME ENGINE] Forcing manual economy sync');
|
||||
this.systems.economy.syncWithServerData(playerData);
|
||||
}
|
||||
|
||||
// Request fresh economy data from server to ensure sync
|
||||
if (this.systems.economy && this.systems.economy.requestEconomyData) {
|
||||
setTimeout(() => {
|
||||
this.systems.economy.requestEconomyData();
|
||||
}, 1000); // Delay to ensure socket is ready
|
||||
}
|
||||
|
||||
// Apply energy from server data to player attributes
|
||||
if (playerData.stats.currentEnergy !== undefined && this.systems.player.attributes) {
|
||||
this.systems.player.attributes.currentEnergy = playerData.stats.currentEnergy;
|
||||
console.log('[GAME ENGINE] Applied current energy from server:', playerData.stats.currentEnergy);
|
||||
}
|
||||
|
||||
if (playerData.stats.maxEnergy !== undefined && this.systems.player.attributes) {
|
||||
this.systems.player.attributes.maxEnergy = playerData.stats.maxEnergy;
|
||||
console.log('[GAME ENGINE] Applied max energy from server:', playerData.stats.maxEnergy);
|
||||
}
|
||||
|
||||
// Ensure player has minimum energy for dungeon access
|
||||
if (this.systems.player.attributes) {
|
||||
// Check if energy is missing or too low
|
||||
if (!this.systems.player.attributes.energy || this.systems.player.attributes.energy < 10) {
|
||||
const oldEnergy = this.systems.player.attributes.energy;
|
||||
this.systems.player.attributes.energy = 100;
|
||||
this.systems.player.attributes.maxEnergy = Math.max(this.systems.player.attributes.maxEnergy || 0, 100);
|
||||
console.log('[GAME ENGINE] Set minimum energy for dungeon access:', oldEnergy, '->', this.systems.player.attributes.energy);
|
||||
}
|
||||
|
||||
// Also ensure currentEnergy is set if it exists
|
||||
if (this.systems.player.attributes.currentEnergy !== undefined) {
|
||||
if (this.systems.player.attributes.currentEnergy < 10) {
|
||||
this.systems.player.attributes.currentEnergy = 100;
|
||||
console.log('[GAME ENGINE] Set minimum currentEnergy for dungeon access');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Final player stats after application:', this.systems.player.stats);
|
||||
} else {
|
||||
console.log('[GAME ENGINE] Missing player stats or player system');
|
||||
console.log('[GAME ENGINE] - playerData.stats:', !!playerData.stats);
|
||||
console.log('[GAME ENGINE] - this.systems:', !!this.systems);
|
||||
console.log('[GAME ENGINE] - this.systems.player:', !!this.systems?.player);
|
||||
}
|
||||
|
||||
// Apply inventory
|
||||
if (playerData.inventory && this.systems && this.systems.inventory) {
|
||||
this.systems.inventory.load(playerData.inventory);
|
||||
console.log('[GAME ENGINE] Applied inventory');
|
||||
}
|
||||
|
||||
// REMOVED: QuestSystem should be server-driven only
|
||||
// Quest data will be handled by server-side systems only
|
||||
|
||||
// Show notification
|
||||
if (this.showNotification) {
|
||||
this.showNotification(`Welcome back! Level ${playerData.stats?.level || 1}`, 'success', 3000);
|
||||
}
|
||||
|
||||
console.log('[GAME ENGINE] Server player data loaded successfully');
|
||||
|
||||
// Trigger UI update to refresh all tabs with new data
|
||||
if (this.systems && this.systems.ui) {
|
||||
this.systems.ui.updateUI();
|
||||
console.log('[GAME ENGINE] Triggered UI update after server data load');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GAME ENGINE] Error loading server player data:', error);
|
||||
if (this.showNotification) {
|
||||
this.showNotification('Failed to load server data!', 'error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global game instance
|
||||
let game = null;
|
||||
|
||||
// Export GameEngine to global scope
|
||||
if (typeof window !== 'undefined') {
|
||||
window.GameEngine = GameEngine;
|
||||
}
|
||||
@ -1,304 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Logging System
|
||||
* Provides file-based logging with rotation and formatting
|
||||
* Renderer process version that uses IPC to communicate with main process
|
||||
*/
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.logLevel = 'INFO';
|
||||
this.isRenderer = typeof window !== 'undefined' && typeof window.electronAPI !== 'undefined';
|
||||
this.timers = new Map();
|
||||
|
||||
this.levels = {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(appDataPath) {
|
||||
if (this.isRenderer) {
|
||||
// In renderer process, just log that we're ready
|
||||
console.log('Logger initialized in renderer process');
|
||||
return;
|
||||
}
|
||||
|
||||
// Main process initialization (original code)
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
// Set up log directory in app storage location
|
||||
this.logDir = path.join(appDataPath, 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
await fs.mkdir(this.logDir, { recursive: true });
|
||||
|
||||
// Set current log file with full timestamp (YYYY-MM-DD-HH-MM-SS)
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
|
||||
|
||||
// Test write to ensure permissions
|
||||
await this.writeToFile('=== Galaxy Strike Online Log Session Started ===\n');
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log(`Logger initialized: ${this.logDir}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize logger:', error);
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
async writeToFile(message) {
|
||||
if (this.isRenderer) {
|
||||
// In renderer process, send to main process via IPC
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isInitialized || !this.currentLogFile) return;
|
||||
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Check if file needs rotation
|
||||
await this.rotateLogIfNeeded();
|
||||
|
||||
// Append message to current log file
|
||||
await fs.appendFile(this.currentLogFile, message, 'utf8');
|
||||
} catch (error) {
|
||||
console.error('Failed to write to log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async rotateLogIfNeeded() {
|
||||
if (this.isRenderer) return;
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(this.currentLogFile);
|
||||
|
||||
if (stats.size >= this.maxFileSize) {
|
||||
// Rotate log file with full timestamp
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
const rotatedFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
|
||||
|
||||
await fs.rename(this.currentLogFile, rotatedFile);
|
||||
|
||||
// Clean up old log files
|
||||
await this.cleanupOldLogs();
|
||||
|
||||
// Create new current log file with new timestamp
|
||||
const newTimestamp = new Date().toISOString()
|
||||
.replace(/T/, '-')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${newTimestamp}.log`);
|
||||
|
||||
await this.writeToFile('=== Log Rotated ===\n');
|
||||
}
|
||||
} catch (error) {
|
||||
// File might not exist yet, which is fine
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error('Failed to rotate log:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupOldLogs() {
|
||||
if (this.isRenderer) return;
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.logDir);
|
||||
const logFiles = files
|
||||
.filter(file => file.startsWith('galaxy-strike-') && file.endsWith('.log'))
|
||||
.map(file => ({
|
||||
name: file,
|
||||
path: path.join(this.logDir, file)
|
||||
}));
|
||||
|
||||
if (logFiles.length > this.maxLogFiles) {
|
||||
// Get file stats and sort by modification time
|
||||
const filesWithStats = await Promise.all(
|
||||
logFiles.map(async file => {
|
||||
const stats = await fs.stat(file.path);
|
||||
return { ...file, mtime: stats.mtime };
|
||||
})
|
||||
);
|
||||
|
||||
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
// Delete oldest files
|
||||
const filesToDelete = filesWithStats.slice(this.maxLogFiles);
|
||||
for (const file of filesToDelete) {
|
||||
await fs.unlink(file.path);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(level, message, data = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const dataStr = data ? ` | Data: ${JSON.stringify(data)}` : '';
|
||||
return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
|
||||
}
|
||||
|
||||
shouldLog(level) {
|
||||
const currentLevel = this.levels[this.logLevel] || 2;
|
||||
const messageLevel = this.levels[level] || 2;
|
||||
return messageLevel <= currentLevel;
|
||||
}
|
||||
|
||||
async log(level, message, data = null) {
|
||||
if (!this.shouldLog(level)) return;
|
||||
|
||||
if (this.isRenderer && window.electronAPI) {
|
||||
// Send to main process via IPC - ensure data is serializable
|
||||
try {
|
||||
const serializableData = data ? this.makeSerializable(data) : null;
|
||||
window.electronAPI.log(level, message, serializableData);
|
||||
} catch (error) {
|
||||
console.error('Failed to send log to main process:', error);
|
||||
// Fallback to console
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
} else {
|
||||
// Main process logging
|
||||
const formattedMessage = this.formatMessage(level, message, data);
|
||||
await this.writeToFile(formattedMessage);
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
}
|
||||
|
||||
makeSerializable(obj) {
|
||||
try {
|
||||
// Convert to JSON and back to ensure it's serializable
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
// If not serializable, convert to string representation
|
||||
return {
|
||||
type: 'non-serializable',
|
||||
toString: obj.toString ? obj.toString() : String(obj),
|
||||
constructor: obj.constructor ? obj.constructor.name : 'Unknown'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async error(message, data = null) {
|
||||
await this.log('error', message, data);
|
||||
console.error(`[ERROR] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async warn(message, data = null) {
|
||||
await this.log('warn', message, data);
|
||||
console.warn(`[WARN] ${message}`, data || '');
|
||||
}
|
||||
|
||||
async info(message, data = null) {
|
||||
await this.log('info', message, data);
|
||||
}
|
||||
|
||||
async debug(message, data = null) {
|
||||
await this.log('debug', message, data);
|
||||
}
|
||||
|
||||
async gameEvent(eventType, details) {
|
||||
const message = `Game Event: ${eventType}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
// Timing methods
|
||||
startTimer(name) {
|
||||
this.timers.set(name, performance.now());
|
||||
}
|
||||
|
||||
endTimer(name) {
|
||||
const startTime = this.timers.get(name);
|
||||
if (startTime) {
|
||||
const duration = performance.now() - startTime;
|
||||
this.timers.delete(name);
|
||||
return duration;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async timeAsync(name, asyncFunction) {
|
||||
this.startTimer(name);
|
||||
try {
|
||||
const result = await asyncFunction();
|
||||
const duration = this.endTimer(name);
|
||||
await this.info(`${name} completed`, { duration: `${duration.toFixed(2)}ms` });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = this.endTimer(name);
|
||||
await this.error(`${name} failed`, { duration: `${duration.toFixed(2)}ms`, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async playerAction(action, details) {
|
||||
const message = `Player Action: ${action}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
async systemEvent(system, event, details) {
|
||||
const message = `System Event: ${system} - ${event}`;
|
||||
await this.info(message, details);
|
||||
}
|
||||
|
||||
async errorEvent(error, context = null) {
|
||||
const message = `Error Event: ${error.message || error}`;
|
||||
const data = {
|
||||
stack: error.stack,
|
||||
context: context
|
||||
};
|
||||
await this.error(message, data);
|
||||
}
|
||||
|
||||
setLogLevel(level) {
|
||||
if (this.levels.hasOwnProperty(level)) {
|
||||
this.logLevel = level;
|
||||
this.info(`Log level changed to: ${level}`);
|
||||
}
|
||||
}
|
||||
|
||||
getLogInfo() {
|
||||
return {
|
||||
isInitialized: this.isInitialized,
|
||||
isRenderer: this.isRenderer,
|
||||
logDirectory: this.logDir,
|
||||
currentLogFile: this.currentLogFile,
|
||||
logLevel: this.logLevel
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
const logger = new Logger();
|
||||
|
||||
// For Node.js environments
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = logger;
|
||||
}
|
||||
|
||||
// For browser environments
|
||||
if (typeof window !== 'undefined') {
|
||||
window.logger = logger;
|
||||
}
|
||||
@ -1,965 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Player System
|
||||
* Manages player stats, levels, and progression
|
||||
*/
|
||||
|
||||
class Player {
|
||||
constructor(gameEngine) {
|
||||
const debugLogger = window.debugLogger;
|
||||
if (debugLogger) debugLogger.log('Player constructor called');
|
||||
|
||||
this.game = gameEngine;
|
||||
|
||||
// Player stats
|
||||
this.stats = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
totalXP: 0, // Total accumulated XP across all levels
|
||||
experienceToNext: window.XPProgression ? window.XPProgression.calculateXPToNextLevel(1, 0) : 100,
|
||||
skillPoints: 0,
|
||||
totalKills: 0,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: Date.now()
|
||||
};
|
||||
|
||||
// Base attributes
|
||||
this.attributes = {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Player info
|
||||
this.info = {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
};
|
||||
|
||||
// Ship info
|
||||
this.ship = {
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
level: 1,
|
||||
upgrades: []
|
||||
};
|
||||
|
||||
// Settings
|
||||
this.settings = {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
discordIntegration: false
|
||||
};
|
||||
|
||||
if (debugLogger) debugLogger.log('Player constructor completed', {
|
||||
initialLevel: this.stats.level,
|
||||
initialHealth: this.attributes.health,
|
||||
shipName: this.ship.name,
|
||||
shipClass: this.ship.class
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Player system initializing');
|
||||
if (debugLogger) await debugLogger.startStep('playerInitialize');
|
||||
|
||||
try {
|
||||
if (debugLogger) await debugLogger.logStep('Player initialization started');
|
||||
// Player initialization is handled by GameEngine
|
||||
// This method is kept for compatibility but doesn't load game data
|
||||
console.log('[PLAYER] Player system initialization completed');
|
||||
if (debugLogger) await debugLogger.endStep('playerInitialize');
|
||||
} catch (error) {
|
||||
console.error('[PLAYER] Error during initialization:', error);
|
||||
if (debugLogger) await debugLogger.errorEvent(error, 'Player Initialize');
|
||||
}
|
||||
}
|
||||
|
||||
setupNewPlayer() {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Setting up new player');
|
||||
if (debugLogger) debugLogger.logStep('Setting up new player', {
|
||||
currentLevel: this.stats.level,
|
||||
currentTitle: this.info.title
|
||||
});
|
||||
|
||||
this.game.showNotification('Welcome to Galaxy Strike Online, Commander!', 'success', 5000);
|
||||
this.game.showNotification('Complete quests and explore dungeons to progress!', 'info', 4000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('New player setup completed', {
|
||||
notificationsShown: 2
|
||||
});
|
||||
}
|
||||
|
||||
// Experience and leveling
|
||||
addExperience(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldExperience = this.stats.experience;
|
||||
const oldLevel = this.stats.level;
|
||||
const oldTotalXP = this.stats.totalXP;
|
||||
|
||||
// Add to total accumulated XP
|
||||
this.stats.totalXP += amount;
|
||||
|
||||
// Calculate new level based on total XP
|
||||
if (window.XPProgression) {
|
||||
const levelInfo = window.XPProgression.getLevelFromXP(this.stats.totalXP);
|
||||
this.stats.level = levelInfo.level;
|
||||
this.stats.experience = levelInfo.xpIntoLevel;
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
} else {
|
||||
// Fallback to old system
|
||||
this.stats.experience += amount;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Experience added', {
|
||||
amount: amount,
|
||||
oldExperience: oldExperience,
|
||||
newExperience: this.stats.experience,
|
||||
oldTotalXP: oldTotalXP,
|
||||
newTotalXP: this.stats.totalXP,
|
||||
experienceToNext: this.stats.experienceToNext,
|
||||
currentLevel: oldLevel,
|
||||
newLevel: this.stats.level
|
||||
});
|
||||
|
||||
// Check for level up
|
||||
const levelsGained = this.stats.level - oldLevel;
|
||||
if (levelsGained > 0) {
|
||||
for (let i = 0; i < levelsGained; i++) {
|
||||
this.levelUp();
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogger && levelsGained > 0) {
|
||||
debugLogger.logStep('Level up(s) occurred', {
|
||||
levelsGained: levelsGained,
|
||||
newLevel: this.stats.level,
|
||||
remainingExperience: this.stats.experience,
|
||||
totalXP: this.stats.totalXP
|
||||
});
|
||||
}
|
||||
|
||||
this.game.showNotification(`+${this.game.formatNumber(amount)} XP`, 'success', 2000);
|
||||
}
|
||||
|
||||
levelUp() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldLevel = this.stats.level;
|
||||
const oldSkillPoints = this.stats.skillPoints;
|
||||
const oldMaxHealth = this.attributes.maxHealth;
|
||||
const oldMaxEnergy = this.attributes.maxEnergy;
|
||||
const oldAttack = this.attributes.attack;
|
||||
const oldDefense = this.attributes.defense;
|
||||
const oldShipMaxHealth = this.ship.maxHealth;
|
||||
const oldTitle = this.info.title;
|
||||
|
||||
console.log(`[PLAYER] Level up! New level: ${this.stats.level}`);
|
||||
if (debugLogger) debugLogger.logStep('Level up initiated', {
|
||||
oldLevel: oldLevel,
|
||||
newLevel: this.stats.level,
|
||||
skillPointsGained: 2,
|
||||
totalSkillPoints: this.stats.skillPoints + 2,
|
||||
currentXP: this.stats.experience,
|
||||
totalXP: this.stats.totalXP
|
||||
});
|
||||
|
||||
// Update quest progress for level objectives
|
||||
if (this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.updateLevelProgress(this.stats.level);
|
||||
if (debugLogger) debugLogger.logStep('Quest progress updated for new level');
|
||||
}
|
||||
|
||||
// Update experience requirement for next level
|
||||
if (window.XPProgression) {
|
||||
const levelInfo = window.XPProgression.getLevelFromXP(this.stats.totalXP);
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
} else {
|
||||
// Fallback to old system
|
||||
this.stats.experienceToNext = Math.floor(this.stats.experienceToNext * 1.5);
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Experience requirement updated', {
|
||||
newRequirement: this.stats.experienceToNext,
|
||||
usingNewSystem: !!window.XPProgression
|
||||
});
|
||||
|
||||
// Improve base stats
|
||||
this.attributes.maxHealth += 10;
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
|
||||
// Update UI to show new level
|
||||
if (this.game && this.game.systems && this.game.systems.ui) {
|
||||
this.game.systems.ui.updateUI();
|
||||
if (debugLogger) debugLogger.logStep('UI updated for new level');
|
||||
}
|
||||
|
||||
this.game.showNotification(`Level Up! You are now level ${this.stats.level}!`, 'success', 3000);
|
||||
this.attributes.maxEnergy += 5;
|
||||
this.attributes.energy = this.attributes.maxEnergy;
|
||||
this.attributes.attack += 2;
|
||||
this.attributes.defense += 1;
|
||||
|
||||
// Update ship health
|
||||
this.ship.maxHealth += 15;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
|
||||
// Update title based on level
|
||||
this.updateTitle();
|
||||
|
||||
this.game.showNotification(`Level Up! You are now level ${this.stats.level}!`, 'success', 5000);
|
||||
this.game.showNotification(`+2 Skill Points available`, 'info', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Level up completed', {
|
||||
levelChange: `${oldLevel} → ${this.stats.level}`,
|
||||
healthChange: `${oldMaxHealth} → ${this.attributes.maxHealth}`,
|
||||
energyChange: `${oldMaxEnergy} → ${this.attributes.maxEnergy}`,
|
||||
attackChange: `${oldAttack} → ${this.attributes.attack}`,
|
||||
defenseChange: `${oldDefense} → ${this.attributes.defense}`,
|
||||
shipHealthChange: `${oldShipMaxHealth} → ${this.ship.maxHealth}`,
|
||||
titleChange: `${oldTitle} → ${this.info.title}`,
|
||||
skillPointsChange: `${oldSkillPoints} → ${this.stats.skillPoints + 2}`
|
||||
});
|
||||
|
||||
// Add skill points after logging the changes
|
||||
this.stats.skillPoints += 2;
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldTitle = this.info.title;
|
||||
|
||||
const titles = {
|
||||
1: 'Rookie Pilot',
|
||||
5: 'Space Cadet',
|
||||
10: 'Star Explorer',
|
||||
15: 'Galaxy Ranger',
|
||||
20: 'Space Captain',
|
||||
25: 'Star Commander',
|
||||
30: 'Galaxy Admiral',
|
||||
40: 'Space Legend',
|
||||
50: 'Cosmic Master'
|
||||
};
|
||||
|
||||
for (const [level, title] of Object.entries(titles)) {
|
||||
if (this.stats.level >= parseInt(level)) {
|
||||
this.info.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogger && oldTitle !== this.info.title) {
|
||||
debugLogger.logStep('Player title updated', {
|
||||
level: this.stats.level,
|
||||
oldTitle: oldTitle,
|
||||
newTitle: this.info.title
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Combat stats
|
||||
takeDamage(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldHealth = this.attributes.health;
|
||||
const actualDamage = Math.max(1, amount - this.attributes.defense);
|
||||
this.attributes.health = Math.max(0, this.attributes.health - actualDamage);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player took damage', {
|
||||
damageAmount: amount,
|
||||
playerDefense: this.attributes.defense,
|
||||
actualDamage: actualDamage,
|
||||
oldHealth: oldHealth,
|
||||
newHealth: this.attributes.health,
|
||||
healthRemaining: this.attributes.health > 0
|
||||
});
|
||||
|
||||
if (this.attributes.health === 0) {
|
||||
this.onDeath();
|
||||
}
|
||||
return actualDamage;
|
||||
}
|
||||
|
||||
heal(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldHealth = this.attributes.health;
|
||||
const healAmount = Math.min(amount, this.attributes.maxHealth - this.attributes.health);
|
||||
this.attributes.health += healAmount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player healed', {
|
||||
healAmount: amount,
|
||||
actualHealAmount: healAmount,
|
||||
oldHealth: oldHealth,
|
||||
newHealth: this.attributes.health,
|
||||
maxHealth: this.attributes.maxHealth,
|
||||
healthPercent: Math.round((this.attributes.health / this.attributes.maxHealth) * 100)
|
||||
});
|
||||
|
||||
return healAmount;
|
||||
}
|
||||
|
||||
useEnergy(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldEnergy = this.attributes.energy;
|
||||
|
||||
if (this.attributes.energy < amount) {
|
||||
if (debugLogger) debugLogger.logStep('Energy use failed - insufficient energy', {
|
||||
requestedAmount: amount,
|
||||
currentEnergy: oldEnergy,
|
||||
deficit: amount - oldEnergy
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.attributes.energy -= amount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Energy used', {
|
||||
amountUsed: amount,
|
||||
oldEnergy: oldEnergy,
|
||||
newEnergy: this.attributes.energy,
|
||||
maxEnergy: this.attributes.maxEnergy,
|
||||
energyPercent: Math.round((this.attributes.energy / this.attributes.maxEnergy) * 100)
|
||||
});
|
||||
|
||||
// Update UI to show energy change
|
||||
this.updateUI();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
restoreEnergy(amount) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldEnergy = this.attributes.energy;
|
||||
const restoreAmount = Math.min(amount, this.attributes.maxEnergy - this.attributes.energy);
|
||||
this.attributes.energy += restoreAmount;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Energy restored', {
|
||||
restoreAmount: amount,
|
||||
actualRestoreAmount: restoreAmount,
|
||||
oldEnergy: oldEnergy,
|
||||
newEnergy: this.attributes.energy,
|
||||
maxEnergy: this.attributes.maxEnergy,
|
||||
energyPercent: Math.round((this.attributes.energy / this.attributes.maxEnergy) * 100)
|
||||
});
|
||||
|
||||
// Update UI to show energy change
|
||||
this.updateUI();
|
||||
|
||||
return restoreAmount;
|
||||
}
|
||||
|
||||
// Energy regeneration
|
||||
regenerateEnergy(deltaTime) {
|
||||
const regenerationRate = this.getMaxEnergy() * 0.1; // 10% of max energy per second
|
||||
const energyToRegen = (deltaTime / 1000) * regenerationRate;
|
||||
|
||||
if (this.attributes.energy < this.getMaxEnergy()) {
|
||||
this.attributes.energy = Math.min(this.attributes.energy + energyToRegen, this.getMaxEnergy());
|
||||
|
||||
const debugLogger = window.debugLogger;
|
||||
if (debugLogger) debugLogger.logStep('Energy regenerated', {
|
||||
deltaTime: deltaTime,
|
||||
energyRegenerated: energyToRegen,
|
||||
currentEnergy: this.attributes.energy,
|
||||
maxEnergy: this.getMaxEnergy()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Combat calculations
|
||||
calculateDamage(enemyDifficulty = 'normal') {
|
||||
const debugLogger = window.debugLogger;
|
||||
const baseDamage = this.ship.attack || this.attributes.attack;
|
||||
|
||||
// Adjust critical chance based on enemy difficulty
|
||||
let criticalChance = this.ship.criticalChance || this.attributes.criticalChance;
|
||||
const difficultyMultipliers = {
|
||||
'tutorial': 1.5, // Higher chance against easy enemies
|
||||
'easy': 1.2,
|
||||
'normal': 1.0,
|
||||
'medium': 0.9,
|
||||
'hard': 0.7, // Lower chance against hard enemies
|
||||
'extreme': 0.5 // Much lower chance against extreme enemies
|
||||
};
|
||||
|
||||
const originalCriticalChance = criticalChance;
|
||||
criticalChance *= (difficultyMultipliers[enemyDifficulty] || 1.0);
|
||||
|
||||
const criticalRoll = Math.random();
|
||||
|
||||
let damage = baseDamage;
|
||||
let isCritical = false;
|
||||
|
||||
if (criticalRoll < criticalChance) {
|
||||
damage *= (this.ship.criticalDamage || this.attributes.criticalDamage);
|
||||
isCritical = true;
|
||||
}
|
||||
|
||||
// Add some randomness
|
||||
const randomMultiplier = this.game.getRandomFloat(0.9, 1.1);
|
||||
damage *= randomMultiplier;
|
||||
|
||||
const finalDamage = Math.floor(damage);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Damage calculation completed', {
|
||||
enemyDifficulty: enemyDifficulty,
|
||||
baseDamage: baseDamage,
|
||||
originalCriticalChance: originalCriticalChance,
|
||||
adjustedCriticalChance: criticalChance,
|
||||
criticalRoll: criticalRoll,
|
||||
criticalDamageMultiplier: this.ship.criticalDamage || this.attributes.criticalDamage,
|
||||
randomMultiplier: randomMultiplier,
|
||||
isCritical: isCritical,
|
||||
finalDamage: finalDamage
|
||||
});
|
||||
|
||||
return {
|
||||
damage: finalDamage,
|
||||
isCritical
|
||||
};
|
||||
}
|
||||
|
||||
onDeath() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
console.log('[PLAYER] Player death occurred');
|
||||
if (debugLogger) debugLogger.logStep('Player death triggered', {
|
||||
currentLevel: this.stats.level,
|
||||
oldCredits: oldCredits,
|
||||
totalKills: this.stats.totalKills,
|
||||
dungeonsCleared: this.stats.dungeonsCleared
|
||||
});
|
||||
|
||||
this.game.showNotification('Your ship was destroyed! Respawning...', 'error', 3000);
|
||||
|
||||
// Reset health and energy
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
this.attributes.energy = this.attributes.maxEnergy;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
|
||||
// Apply death penalty
|
||||
const lostCredits = Math.floor(this.game.systems.economy.credits * 0.1);
|
||||
this.game.systems.economy.removeCredits(lostCredits);
|
||||
|
||||
const newCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
this.game.showNotification(`Death penalty: -${this.game.formatNumber(lostCredits)} credits`, 'warning', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player death completed', {
|
||||
healthRestored: this.attributes.health,
|
||||
energyRestored: this.attributes.energy,
|
||||
shipHealthRestored: this.ship.health,
|
||||
creditsLost: lostCredits,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: newCredits,
|
||||
penaltyPercentage: 10
|
||||
});
|
||||
}
|
||||
|
||||
resetToLevel1() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldStats = { ...this.stats };
|
||||
const oldAttributes = { ...this.attributes };
|
||||
const oldInfo = { ...this.info };
|
||||
const oldShip = { ...this.ship };
|
||||
|
||||
console.log('[PLAYER] Resetting player to level 1');
|
||||
if (debugLogger) debugLogger.logStep('Player reset to level 1 initiated', {
|
||||
oldLevel: oldStats.level,
|
||||
oldExperience: oldStats.experience,
|
||||
oldKills: oldStats.totalKills
|
||||
});
|
||||
|
||||
// Reset stats to initial values
|
||||
this.stats = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
totalXP: 0, // Total accumulated XP across all levels
|
||||
experienceToNext: window.XPProgression ? window.XPProgression.calculateXPToNextLevel(1, 0) : 100,
|
||||
skillPoints: 0,
|
||||
totalKills: 0,
|
||||
dungeonsCleared: 0,
|
||||
playTime: 0,
|
||||
lastLogin: Date.now(),
|
||||
tutorialDungeonCompleted: false
|
||||
};
|
||||
|
||||
// Reset attributes to base values
|
||||
this.attributes = {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Reset info
|
||||
this.info = {
|
||||
name: 'Commander',
|
||||
title: 'Rookie Pilot',
|
||||
guild: null,
|
||||
rank: 'Cadet'
|
||||
};
|
||||
|
||||
// Reset ship
|
||||
this.ship = {
|
||||
name: 'Starter Cruiser',
|
||||
class: 'Cruiser',
|
||||
health: 1000,
|
||||
maxHealth: 1000,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5,
|
||||
level: 1,
|
||||
upgrades: []
|
||||
};
|
||||
|
||||
// console.log('=== DEBUG: Character Reset ===');
|
||||
// console.log('Player health reset to:', this.attributes.health, '/', this.attributes.maxHealth);
|
||||
// console.log('Ship health reset to:', this.ship.health, '/', this.ship.maxHealth);
|
||||
|
||||
// Reset skills
|
||||
this.skills = {};
|
||||
|
||||
// Reset settings to defaults
|
||||
this.settings = {
|
||||
autoSave: true,
|
||||
notifications: true,
|
||||
soundEffects: true,
|
||||
music: false,
|
||||
theme: 'dark'
|
||||
};
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player reset to level 1 completed', {
|
||||
newLevel: this.stats.level,
|
||||
newHealth: this.attributes.health,
|
||||
newShipHealth: this.ship.health,
|
||||
skillsCleared: true,
|
||||
settingsReset: true
|
||||
});
|
||||
}
|
||||
|
||||
// Ship management
|
||||
upgradeShip(upgradeType) {
|
||||
const debugLogger = window.debugLogger;
|
||||
const upgradeCosts = {
|
||||
health: 100,
|
||||
attack: 150,
|
||||
defense: 120,
|
||||
speed: 80,
|
||||
critical: 200
|
||||
};
|
||||
|
||||
const cost = upgradeCosts[upgradeType];
|
||||
const oldCredits = this.game.systems.economy?.credits || 0;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade attempted', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
currentCredits: oldCredits,
|
||||
canAfford: oldCredits >= cost
|
||||
});
|
||||
|
||||
if (!cost || !this.game.systems.economy || this.game.systems.economy.credits < cost) {
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade failed - insufficient funds or invalid type', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
currentCredits: oldCredits,
|
||||
deficit: cost - oldCredits,
|
||||
economySystemAvailable: !!this.game.systems.economy
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldShipStats = { ...this.ship };
|
||||
const oldPlayerStats = { ...this.attributes };
|
||||
|
||||
if (this.game.systems.economy) {
|
||||
this.game.systems.economy.removeCredits(cost);
|
||||
} else {
|
||||
if (debugLogger) debugLogger.log('Economy system not available during ship upgrade');
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (upgradeType) {
|
||||
case 'health':
|
||||
this.ship.maxHealth += 20;
|
||||
this.ship.health = this.ship.maxHealth;
|
||||
this.attributes.maxHealth += 10;
|
||||
this.attributes.health = this.attributes.maxHealth;
|
||||
break;
|
||||
case 'attack':
|
||||
this.ship.attack += 3;
|
||||
break;
|
||||
case 'defense':
|
||||
this.ship.defense += 2;
|
||||
break;
|
||||
case 'speed':
|
||||
this.ship.speed += 2;
|
||||
break;
|
||||
case 'critical':
|
||||
this.ship.criticalChance = Math.min(0.5, this.ship.criticalChance + 0.02);
|
||||
this.ship.criticalDamage += 0.1;
|
||||
break;
|
||||
}
|
||||
|
||||
this.ship.upgrades.push(upgradeType);
|
||||
this.game.showNotification(`Ship upgraded: ${upgradeType}!`, 'success', 3000);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Ship upgrade completed', {
|
||||
upgradeType: upgradeType,
|
||||
cost: cost,
|
||||
oldCredits: oldCredits,
|
||||
newCredits: this.game.systems.economy?.credits || 0,
|
||||
shipChanges: {
|
||||
oldMaxHealth: oldShipStats.maxHealth,
|
||||
newMaxHealth: this.ship.maxHealth,
|
||||
oldAttack: oldShipStats.attack,
|
||||
newAttack: this.ship.attack,
|
||||
oldDefense: oldShipStats.defense,
|
||||
newDefense: this.ship.defense,
|
||||
oldSpeed: oldShipStats.speed,
|
||||
newSpeed: this.ship.speed,
|
||||
oldCriticalChance: oldShipStats.criticalChance,
|
||||
newCriticalChance: this.ship.criticalChance,
|
||||
oldCriticalDamage: oldShipStats.criticalDamage,
|
||||
newCriticalDamage: this.ship.criticalDamage
|
||||
},
|
||||
playerChanges: {
|
||||
oldMaxHealth: oldPlayerStats.maxHealth,
|
||||
newMaxHealth: this.attributes.maxHealth,
|
||||
oldHealth: oldPlayerStats.health,
|
||||
newHealth: this.attributes.health
|
||||
},
|
||||
totalUpgrades: this.ship.upgrades.length
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Statistics tracking
|
||||
incrementKills() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldKills = this.stats.totalKills;
|
||||
this.stats.totalKills++;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Kill count incremented', {
|
||||
oldKills: oldKills,
|
||||
newKills: this.stats.totalKills,
|
||||
currentLevel: this.stats.level
|
||||
});
|
||||
|
||||
// Update quest progress for combat objectives
|
||||
if (this.game && this.game.systems && this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.onEnemyDefeated();
|
||||
if (debugLogger) debugLogger.logStep('Quest system notified of enemy defeat');
|
||||
}
|
||||
}
|
||||
|
||||
incrementDungeonsCleared() {
|
||||
const debugLogger = window.debugLogger;
|
||||
const oldDungeons = this.stats.dungeonsCleared;
|
||||
this.stats.dungeonsCleared++;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Dungeons cleared incremented', {
|
||||
oldDungeons: oldDungeons,
|
||||
newDungeons: this.stats.dungeonsCleared,
|
||||
currentLevel: this.stats.level
|
||||
});
|
||||
}
|
||||
|
||||
updatePlayTime(deltaTime) {
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
console.log('[PLAYER] updatePlayTime called with deltaTime:', deltaTime, 'ms');
|
||||
console.log('[PLAYER] Game state check:', {
|
||||
hasGame: !!this.game,
|
||||
isRunning: this.game?.isRunning,
|
||||
isPaused: this.game?.state?.paused,
|
||||
isHidden: document.hidden
|
||||
});
|
||||
*/
|
||||
|
||||
// Only update playtime when game is actively running and not paused
|
||||
if (!this.game || !this.game.isRunning || this.game.state.paused) {
|
||||
// console.log('[PLAYER] Skipping playtime update - game not running or paused');
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check if tab is visible (don't count time when tab is in background)
|
||||
if (document.hidden) {
|
||||
// console.log('[PLAYER] Skipping playtime update - tab hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
console.log('[PLAYER] Before update - playTime:', this.stats.playTime, 'ms');
|
||||
*/
|
||||
|
||||
// Use real computer time delta
|
||||
this.stats.playTime += deltaTime;
|
||||
|
||||
// DISABLED: Reduce console spam for quest debugging
|
||||
/*
|
||||
console.log('[PLAYER] After update - playTime:', this.stats.playTime, 'ms');
|
||||
console.log('[PLAYER] PlayTime in seconds:', this.stats.playTime / 1000, 'seconds');
|
||||
console.log('[PLAYER] PlayTime in minutes:', this.stats.playTime / 60000, 'minutes');
|
||||
*/
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player UI update started', {
|
||||
currentLevel: this.stats.level,
|
||||
currentHealth: this.attributes.health,
|
||||
currentEnergy: this.attributes.energy,
|
||||
totalKills: this.stats.totalKills
|
||||
});
|
||||
|
||||
// Update player info
|
||||
const playerNameElement = document.getElementById('playerName');
|
||||
const playerTitleElement = document.getElementById('playerTitle');
|
||||
const playerLevelElement = document.getElementById('playerLevel');
|
||||
|
||||
if (playerNameElement) {
|
||||
playerNameElement.textContent = this.info.name;
|
||||
}
|
||||
|
||||
if (playerTitleElement) {
|
||||
playerTitleElement.textContent = ` - ${this.info.title}`;
|
||||
}
|
||||
|
||||
if (playerLevelElement) {
|
||||
playerLevelElement.textContent = `Lv. ${this.stats.level}`;
|
||||
}
|
||||
|
||||
// Update health and energy only if in multiplayer mode or game is actively running
|
||||
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
|
||||
|
||||
if (shouldUpdateUI && this.game && this.game.systems && this.game.systems.ui) {
|
||||
this.game.systems.ui.updateResourceDisplay();
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const totalKillsElement = document.getElementById('totalKills');
|
||||
const dungeonsClearedElement = document.getElementById('dungeonsCleared');
|
||||
const playTimeElement = document.getElementById('playTime');
|
||||
|
||||
if (totalKillsElement) {
|
||||
totalKillsElement.textContent = this.game.formatNumber(this.stats.totalKills);
|
||||
}
|
||||
|
||||
if (dungeonsClearedElement) {
|
||||
dungeonsClearedElement.textContent = this.game.formatNumber(this.stats.dungeonsCleared);
|
||||
}
|
||||
|
||||
if (playTimeElement) {
|
||||
playTimeElement.textContent = this.game.formatTime(this.stats.playTime / 1000);
|
||||
}
|
||||
|
||||
// Update ship info
|
||||
const flagshipNameElement = document.getElementById('flagshipName');
|
||||
const shipHealthElement = document.getElementById('shipHealth');
|
||||
|
||||
if (flagshipNameElement) {
|
||||
flagshipNameElement.textContent = this.ship.name;
|
||||
}
|
||||
|
||||
if (shipHealthElement) {
|
||||
const healthPercent = Math.round((this.ship.health / this.ship.maxHealth) * 100);
|
||||
shipHealthElement.textContent = `${healthPercent}%`;
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player UI update completed', {
|
||||
elementsUpdated: {
|
||||
playerName: !!playerNameElement,
|
||||
playerTitle: !!playerTitleElement,
|
||||
playerLevel: !!playerLevelElement,
|
||||
totalKills: !!totalKillsElement,
|
||||
dungeonsCleared: !!dungeonsClearedElement,
|
||||
playTime: !!playTimeElement,
|
||||
flagshipName: !!flagshipNameElement,
|
||||
shipHealth: !!shipHealthElement
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save/Load
|
||||
save() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
const saveData = {
|
||||
stats: this.stats,
|
||||
attributes: this.attributes,
|
||||
info: this.info,
|
||||
ship: this.ship,
|
||||
settings: this.settings
|
||||
};
|
||||
|
||||
// if (debugLogger) debugLogger.logStep('Player save data prepared', {
|
||||
// level: this.stats.level,
|
||||
// experience: this.stats.experience,
|
||||
// totalKills: this.stats.totalKills,
|
||||
// dungeonsCleared: this.stats.dungeonsCleared,
|
||||
// playTime: this.stats.playTime,
|
||||
// shipName: this.ship.name,
|
||||
// shipLevel: this.ship.level,
|
||||
// upgradesCount: this.ship.upgrades.length,
|
||||
// dataSize: JSON.stringify(saveData).length
|
||||
// });
|
||||
|
||||
return saveData;
|
||||
}
|
||||
|
||||
load(data) {
|
||||
const debugLogger = window.debugLogger;
|
||||
console.log('[PLAYER] Loading player data:', data);
|
||||
console.log('[PLAYER] Current level before load:', this.stats.level);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player load initiated', {
|
||||
hasData: !!data,
|
||||
dataKeys: data ? Object.keys(data) : [],
|
||||
currentLevel: this.stats.level,
|
||||
currentExperience: this.stats.experience
|
||||
});
|
||||
|
||||
try {
|
||||
if (data.stats) {
|
||||
console.log('[PLAYER] Loading stats:', data.stats);
|
||||
console.log('[PLAYER] Current playTime before load:', this.stats.playTime);
|
||||
console.log('[PLAYER] Server playTime:', data.stats.playTime);
|
||||
|
||||
const oldStats = { ...this.stats };
|
||||
|
||||
// Preserve playTime if server doesn't provide it or provides 0
|
||||
const existingPlayTime = this.stats.playTime || 0;
|
||||
const serverPlayTime = data.stats.playTime || 0;
|
||||
|
||||
// Use server playTime if it's greater than existing, otherwise preserve existing
|
||||
const preservedPlayTime = serverPlayTime > existingPlayTime ? serverPlayTime : existingPlayTime;
|
||||
|
||||
console.log('[PLAYER] Preserving playTime:', preservedPlayTime, '(existing:', existingPlayTime, ', server:', serverPlayTime, ')');
|
||||
|
||||
// Merge stats but preserve playTime
|
||||
this.stats = {
|
||||
...this.stats,
|
||||
...data.stats,
|
||||
playTime: preservedPlayTime // Force preserve playTime
|
||||
};
|
||||
|
||||
console.log('[PLAYER] Level after stats load:', this.stats.level);
|
||||
console.log('[PLAYER] PlayTime after stats load:', this.stats.playTime);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player stats loaded', {
|
||||
oldLevel: oldStats.level,
|
||||
newLevel: this.stats.level,
|
||||
oldExperience: oldStats.experience,
|
||||
newExperience: this.stats.experience,
|
||||
oldKills: oldStats.totalKills,
|
||||
newKills: this.stats.totalKills
|
||||
});
|
||||
}
|
||||
|
||||
if (data.attributes) {
|
||||
console.log('[PLAYER] Loading attributes:', data.attributes);
|
||||
const oldAttributes = { ...this.attributes };
|
||||
this.attributes = { ...this.attributes, ...data.attributes };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player attributes loaded', {
|
||||
oldHealth: oldAttributes.health,
|
||||
newHealth: this.attributes.health,
|
||||
oldMaxHealth: oldAttributes.maxHealth,
|
||||
newMaxHealth: this.attributes.maxHealth,
|
||||
oldAttack: oldAttributes.attack,
|
||||
newAttack: this.attributes.attack,
|
||||
oldDefense: oldAttributes.defense,
|
||||
newDefense: this.attributes.defense
|
||||
});
|
||||
}
|
||||
|
||||
if (data.info) {
|
||||
console.log('[PLAYER] Loading info:', data.info);
|
||||
const oldInfo = { ...this.info };
|
||||
this.info = { ...this.info, ...data.info };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player info loaded', {
|
||||
oldName: oldInfo.name,
|
||||
newName: this.info.name,
|
||||
oldTitle: oldInfo.title,
|
||||
newTitle: this.info.title,
|
||||
oldGuild: oldInfo.guild,
|
||||
newGuild: this.info.guild
|
||||
});
|
||||
}
|
||||
|
||||
if (data.ship) {
|
||||
console.log('[PLAYER] Loading ship:', data.ship);
|
||||
const oldShip = { ...this.ship };
|
||||
this.ship = { ...this.ship, ...data.ship };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player ship loaded', {
|
||||
oldShipName: oldShip.name,
|
||||
newShipName: this.ship.name,
|
||||
oldShipLevel: oldShip.level,
|
||||
newShipLevel: this.ship.level,
|
||||
oldUpgrades: oldShip.upgrades.length,
|
||||
newUpgrades: this.ship.upgrades.length
|
||||
});
|
||||
}
|
||||
|
||||
if (data.settings) {
|
||||
console.log('[PLAYER] Loading settings:', data.settings);
|
||||
const oldSettings = { ...this.settings };
|
||||
this.settings = { ...this.settings, ...data.settings };
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player settings loaded', {
|
||||
oldAutoSave: oldSettings.autoSave,
|
||||
newAutoSave: this.settings.autoSave,
|
||||
oldNotifications: oldSettings.notifications,
|
||||
newNotifications: this.settings.notifications
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[PLAYER] Final level after load:', this.stats.level);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Player load completed successfully', {
|
||||
finalLevel: this.stats.level,
|
||||
finalExperience: this.stats.experience,
|
||||
finalHealth: this.attributes.health,
|
||||
finalShipHealth: this.ship.health,
|
||||
totalDataSections: ['stats', 'attributes', 'info', 'ship', 'settings'].filter(key => data[key]).length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PLAYER] Error loading player data:', error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, 'Player Load');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Texture Manager
|
||||
* Handles texture loading and missing texture fallbacks
|
||||
*/
|
||||
|
||||
class TextureManager {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
this.textures = new Map();
|
||||
this.missingTextureUrl = 'assets/textures/missing-texture.png';
|
||||
|
||||
// Initialize missing texture
|
||||
this.loadMissingTexture();
|
||||
}
|
||||
|
||||
async loadMissingTexture() {
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = this.missingTextureUrl;
|
||||
await img.decode();
|
||||
this.textures.set('missing', img);
|
||||
} catch (error) {
|
||||
console.warn('Could not load missing texture, creating fallback');
|
||||
this.createFallbackTexture();
|
||||
}
|
||||
}
|
||||
|
||||
createFallbackTexture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Create a pink and black checkerboard pattern
|
||||
const squareSize = 8;
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ? '#FF00FF' : '#000000';
|
||||
ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Add "GSO Missing" text
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('GSO', 32, 28);
|
||||
ctx.fillText('Missing', 32, 36);
|
||||
|
||||
const img = new Image();
|
||||
img.src = canvas.toDataURL();
|
||||
this.textures.set('missing', img);
|
||||
}
|
||||
|
||||
async loadTexture(textureId, textureUrl) {
|
||||
// Check if already loaded
|
||||
if (this.textures.has(textureId)) {
|
||||
return this.textures.get(textureId);
|
||||
}
|
||||
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = textureUrl;
|
||||
await img.decode();
|
||||
this.textures.set(textureId, img);
|
||||
return img;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load texture ${textureId} from ${textureUrl}, using missing texture`);
|
||||
return this.getMissingTexture();
|
||||
}
|
||||
}
|
||||
|
||||
getTexture(textureId) {
|
||||
return this.textures.get(textureId) || this.getMissingTexture();
|
||||
}
|
||||
|
||||
getMissingTexture() {
|
||||
return this.textures.get('missing') || this.createFallbackTexture();
|
||||
}
|
||||
|
||||
// Icon fallback for FontAwesome icons
|
||||
getIcon(iconClass) {
|
||||
// Check if this is a valid FontAwesome icon
|
||||
const validIconPrefixes = ['fas', 'far', 'fab', 'fal'];
|
||||
const iconParts = iconClass.split(' ');
|
||||
const hasValidPrefix = iconParts.some(part => validIconPrefixes.includes(part));
|
||||
|
||||
if (hasValidPrefix) {
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
// Return missing icon fallback - use missing texture
|
||||
return 'missing-texture';
|
||||
}
|
||||
|
||||
// Get item icon as HTML element
|
||||
getItemIconElement(iconClass, size = '32px') {
|
||||
const icon = this.getIcon(iconClass);
|
||||
|
||||
if (icon === 'missing-texture') {
|
||||
return `<img src="assets/textures/missing-texture.png" style="width: ${size}; height: ${size}; object-fit: contain;" alt="Missing texture">`;
|
||||
}
|
||||
|
||||
return `<i class="fas ${icon}" style="font-size: ${size};"></i>`;
|
||||
}
|
||||
|
||||
// Preload common textures
|
||||
async preloadTextures() {
|
||||
const commonTextures = [
|
||||
'ship_fighter',
|
||||
'ship_cruiser',
|
||||
'room_command_center',
|
||||
'room_power_core',
|
||||
'item_weapon',
|
||||
'item_shield'
|
||||
];
|
||||
|
||||
const loadPromises = commonTextures.map(textureId => {
|
||||
const url = `assets/textures/${textureId}.png`;
|
||||
return this.loadTexture(textureId, url);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(loadPromises);
|
||||
console.log('Common textures preloaded');
|
||||
} catch (error) {
|
||||
console.warn('Some textures failed to preload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up unused textures
|
||||
cleanup() {
|
||||
// Keep only essential textures in memory
|
||||
const essentialTextures = ['missing'];
|
||||
for (const [textureId, texture] of this.textures) {
|
||||
if (!essentialTextures.includes(textureId)) {
|
||||
this.textures.delete(textureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Game Data
|
||||
* UI constants and configuration only.
|
||||
* All game content (items, skills, recipes, dungeons, enemies) is loaded from the server.
|
||||
*/
|
||||
|
||||
// Game configuration
|
||||
const GAME_CONFIG = {
|
||||
version: '1.0.0',
|
||||
name: 'Galaxy Strike Online',
|
||||
maxLevel: 100,
|
||||
saveInterval: 30000, // 30 seconds
|
||||
notificationDuration: 3000,
|
||||
maxNotifications: 5
|
||||
};
|
||||
|
||||
// Player defaults (used only for initial UI state before server data arrives)
|
||||
const PLAYER_DEFAULTS = {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
skillPoints: 0,
|
||||
credits: 1000,
|
||||
gems: 10,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
energy: 100,
|
||||
maxEnergy: 100,
|
||||
attack: 10,
|
||||
defense: 5,
|
||||
speed: 10,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
};
|
||||
|
||||
// Experience requirements (client-side display only; server is authoritative)
|
||||
const EXPERIENCE_TABLE = [];
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
|
||||
}
|
||||
|
||||
// Item rarity display properties (colours/labels only - drop rates are server-side)
|
||||
const ITEM_RARITIES = {
|
||||
common: { name: 'Common', color: '#888888', multiplier: 1.0 },
|
||||
uncommon: { name: 'Uncommon', color: '#00ff00', multiplier: 1.2 },
|
||||
rare: { name: 'Rare', color: '#0088ff', multiplier: 1.5 },
|
||||
epic: { name: 'Epic', color: '#8833ff', multiplier: 2.0 },
|
||||
legendary: { name: 'Legendary', color: '#ff8800', multiplier: 3.0 }
|
||||
};
|
||||
|
||||
// Game messages and notifications
|
||||
const GAME_MESSAGES = {
|
||||
welcome: 'Welcome to Galaxy Strike Online, Commander!',
|
||||
levelUp: 'Level Up! You are now level {level}!',
|
||||
questCompleted: 'Quest completed: {questName}!',
|
||||
dungeonCompleted: 'Dungeon completed! Time: {time}',
|
||||
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
|
||||
insufficientCredits: 'Not enough credits!',
|
||||
insufficientGems: 'Not enough gems!',
|
||||
insufficientEnergy: 'Not enough energy!',
|
||||
inventoryFull: 'Inventory is full!',
|
||||
skillPointsAvailable: 'You have {points} skill points available!',
|
||||
dailyReward: 'Daily reward claimed! Day {day}',
|
||||
offlineRewards: 'Welcome back! You were offline for {time}'
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
const GameUtils = {
|
||||
getRandomItem(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
},
|
||||
|
||||
getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
|
||||
getRandomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
},
|
||||
|
||||
checkChance(chance) {
|
||||
return Math.random() < chance;
|
||||
},
|
||||
|
||||
formatNumber(num) {
|
||||
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
||||
return Math.floor(num).toString();
|
||||
},
|
||||
|
||||
formatTime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ${hours % 24}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||
return `${seconds}s`;
|
||||
},
|
||||
|
||||
getExperienceForLevel(level) {
|
||||
return EXPERIENCE_TABLE[level] || 0;
|
||||
},
|
||||
|
||||
deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
generateId() {
|
||||
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
GAME_CONFIG,
|
||||
PLAYER_DEFAULTS,
|
||||
EXPERIENCE_TABLE,
|
||||
ITEM_RARITIES,
|
||||
GAME_MESSAGES,
|
||||
GameUtils
|
||||
};
|
||||
}
|
||||
@ -1,718 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Main Entry Point
|
||||
* Initializes and starts the game
|
||||
*/
|
||||
|
||||
console.log('[MAIN] main.js script loaded');
|
||||
|
||||
// Wait for DOM to be loaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.domContentLoaded', {
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent,
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('DOM elements found', {
|
||||
loadingIndicator: !!loadingIndicator,
|
||||
loadingStatus: !!loadingStatus
|
||||
});
|
||||
|
||||
try {
|
||||
// Start debug logging
|
||||
if (window.debugLogger) {
|
||||
window.debugLogger.startStep('domLoad');
|
||||
window.debugLogger.logStep('DOM loaded, starting initialization');
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
if (loadingIndicator) loadingIndicator.classList.remove('hidden');
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Initializing application...';
|
||||
loadingStatus.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading indicator shown');
|
||||
|
||||
// Initialize title bar controls immediately (don't wait for DOMContentLoaded)
|
||||
console.log('[MAIN] Initializing title bar controls immediately');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Initializing title bar controls');
|
||||
initializeTitleBar();
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (debugLogger) debugLogger.startStep('main.secondDOMContentLoaded', {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
window.debugLogger.startStep('domLoad');
|
||||
window.debugLogger.logStep('DOM loaded, starting initialization');
|
||||
|
||||
// Auto-start local server for singleplayer mode (DISABLED to prevent auto-start after multiplayer disconnect)
|
||||
console.log('[MAIN] Skipping local server auto-start to prevent conflicts with multiplayer mode');
|
||||
/*
|
||||
if (window.localServerManager) {
|
||||
try {
|
||||
const serverResult = await window.localServerManager.autoStartIfSingleplayer();
|
||||
if (serverResult.success) {
|
||||
console.log('[MAIN] Local server started successfully:', serverResult);
|
||||
if (debugLogger) debugLogger.logStep('Local server auto-started', {
|
||||
port: serverResult.port,
|
||||
url: serverResult.url
|
||||
});
|
||||
} else {
|
||||
console.log('[MAIN] Local server not started:', serverResult.reason || serverResult.error);
|
||||
if (debugLogger) debugLogger.logStep('Local server not started', {
|
||||
reason: serverResult.reason || serverResult.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MAIN] Error starting local server:', error);
|
||||
if (debugLogger) debugLogger.errorEvent(error, 'Local server startup');
|
||||
}
|
||||
} else {
|
||||
console.warn('[MAIN] LocalServerManager not available');
|
||||
}
|
||||
*/
|
||||
|
||||
// Title bar is already initialized, just log it
|
||||
console.log('[MAIN] DOM loaded - title bar should already be working');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('DOM loaded - title bar should be working');
|
||||
|
||||
// Show main menu instead of directly loading game
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Ready';
|
||||
loadingStatus.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading status updated to Ready');
|
||||
|
||||
// Hide loading screen and show main menu
|
||||
const loadingScreen = document.getElementById('loadingScreen');
|
||||
const mainMenu = document.getElementById('mainMenu');
|
||||
|
||||
if (loadingScreen) loadingScreen.classList.add('hidden');
|
||||
if (mainMenu) mainMenu.classList.remove('hidden');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Loading screen hidden, main menu shown', {
|
||||
loadingScreenFound: !!loadingScreen,
|
||||
mainMenuFound: !!mainMenu,
|
||||
liveMainMenuReady: !!window.liveMainMenu
|
||||
});
|
||||
|
||||
// The LiveMainMenu will initialize itself and handle authentication
|
||||
console.log('[MAIN] Main menu displayed - LiveMainMenu will handle authentication and server browsing');
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.secondDOMContentLoaded', {
|
||||
success: true,
|
||||
mainMenuDisplayed: !!mainMenu
|
||||
});
|
||||
});
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.domContentLoaded', {
|
||||
success: true,
|
||||
titleBarInitialized: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game:', error);
|
||||
|
||||
if (debugLogger) debugLogger.errorEvent('main.domContentLoaded', error, {
|
||||
phase: 'initialization',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (window.debugLogger) {
|
||||
window.debugLogger.log('CRITICAL ERROR: Initialization failed', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (loadingIndicator) loadingIndicator.classList.add('error');
|
||||
if (loadingStatus) {
|
||||
loadingStatus.textContent = 'Failed to load application';
|
||||
loadingStatus.classList.add('error');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Error state displayed');
|
||||
|
||||
const logger = window.logger;
|
||||
if (logger) {
|
||||
await logger.errorEvent(error, 'Main.js Initialization');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.domContentLoaded', {
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize title bar controls
|
||||
function initializeTitleBar() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.initializeTitleBar', {
|
||||
timestamp: new Date().toISOString(),
|
||||
electronAPIAvailable: !!window.electronAPI
|
||||
});
|
||||
|
||||
console.log('[TITLE BAR] Starting title bar initialization');
|
||||
|
||||
// Wait for both electronAPI and DOM elements to be available
|
||||
const checkReady = () => {
|
||||
const hasElectronAPI = !!window.electronAPI;
|
||||
const hasMinimizeBtn = !!document.getElementById('minimizeBtn');
|
||||
const hasCloseBtn = !!document.getElementById('closeBtn');
|
||||
const hasFullscreenBtn = !!document.getElementById('fullscreenBtn');
|
||||
|
||||
const readyState = {
|
||||
hasElectronAPI,
|
||||
hasMinimizeBtn,
|
||||
hasCloseBtn,
|
||||
hasFullscreenBtn
|
||||
};
|
||||
|
||||
console.log(`[TITLE BAR] electronAPI: ${hasElectronAPI}, minimizeBtn: ${hasMinimizeBtn}, closeBtn: ${hasCloseBtn}, fullscreenBtn: ${hasFullscreenBtn}`);
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Title bar readiness check', readyState);
|
||||
|
||||
if (hasElectronAPI && hasMinimizeBtn && hasCloseBtn && hasFullscreenBtn) {
|
||||
console.log('[TITLE BAR] All elements ready, setting up events');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('All title bar elements ready, setting up events');
|
||||
setupTitleBarEvents();
|
||||
|
||||
// Hide the "Initializing application..." text since title bar is now working
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
if (loadingStatus && loadingStatus.textContent === 'Initializing application...') {
|
||||
console.log('[TITLE BAR] Hiding initializing text');
|
||||
loadingStatus.classList.add('hidden');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Hiding initializing text');
|
||||
}
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.initializeTitleBar', {
|
||||
success: true,
|
||||
allElementsReady: true
|
||||
});
|
||||
} else {
|
||||
if (debugLogger) debugLogger.logStep('Not all elements ready, retrying in 50ms', {
|
||||
missingElements: Object.keys(readyState).filter(key => !readyState[key])
|
||||
});
|
||||
setTimeout(checkReady, 50);
|
||||
}
|
||||
};
|
||||
|
||||
checkReady();
|
||||
}
|
||||
|
||||
function setupTitleBarEvents() {
|
||||
const debugLogger = window.debugLogger;
|
||||
|
||||
if (debugLogger) debugLogger.startStep('main.setupTitleBarEvents');
|
||||
|
||||
const minimizeBtn = document.getElementById('minimizeBtn');
|
||||
const closeBtn = document.getElementById('closeBtn');
|
||||
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
||||
|
||||
console.log('[TITLE BAR] Setting up event listeners');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Title bar buttons found', {
|
||||
minimizeBtn: !!minimizeBtn,
|
||||
closeBtn: !!closeBtn,
|
||||
fullscreenBtn: !!fullscreenBtn
|
||||
});
|
||||
|
||||
let eventsSetup = 0;
|
||||
|
||||
if (minimizeBtn) {
|
||||
console.log('[TITLE BAR] Adding minimize button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up minimize button listener');
|
||||
minimizeBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Minimize button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar minimize button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.minimize) {
|
||||
window.electronAPI.minimize();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Window minimized via electronAPI');
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when minimize clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for minimize');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Minimize button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Minimize button not found');
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
console.log('[TITLE BAR] Adding close button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up close button listener');
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Close button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar close button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
// ... rest of the code remains the same ...
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.closeWindow) {
|
||||
window.electronAPI.closeWindow();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Window closed via electronAPI');
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when close clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for close');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Close button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Close button not found');
|
||||
}
|
||||
|
||||
if (fullscreenBtn) {
|
||||
console.log('[TITLE BAR] Adding fullscreen button listener');
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Setting up fullscreen button listener');
|
||||
fullscreenBtn.addEventListener('click', (e) => {
|
||||
console.log('[TITLE BAR] Fullscreen button clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('Title bar fullscreen button clicked');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.electronAPI && window.electronAPI.toggleFullscreen) {
|
||||
window.electronAPI.toggleFullscreen();
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen toggled via electronAPI');
|
||||
|
||||
// Toggle fullscreen class on body
|
||||
document.body.classList.toggle('fullscreen');
|
||||
document.body.classList.toggle('fullscreen');
|
||||
// Update icon
|
||||
const icon = fullscreenBtn.querySelector('i');
|
||||
if (document.body.classList.contains('fullscreen')) {
|
||||
icon.className = 'fas fa-compress';
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen mode activated, icon changed to compress');
|
||||
} else {
|
||||
icon.className = 'fas fa-expand';
|
||||
|
||||
if (debugLogger) debugLogger.logStep('Fullscreen mode deactivated, icon changed to expand');
|
||||
}
|
||||
} else {
|
||||
console.error('[TITLE BAR] electronAPI not available when fullscreen clicked');
|
||||
|
||||
if (debugLogger) debugLogger.log('electronAPI not available for fullscreen');
|
||||
}
|
||||
});
|
||||
eventsSetup++;
|
||||
} else {
|
||||
console.error('[TITLE BAR] Fullscreen button not found during setup');
|
||||
|
||||
if (debugLogger) debugLogger.log('Fullscreen button not found');
|
||||
}
|
||||
|
||||
console.log('[TITLE BAR] Event listeners setup complete');
|
||||
|
||||
if (debugLogger) debugLogger.endStep('main.setupTitleBarEvents', {
|
||||
eventsSetup: eventsSetup,
|
||||
totalExpectedEvents: 3
|
||||
});
|
||||
}
|
||||
|
||||
// Global utility functions for onclick handlers
|
||||
window.game = null;
|
||||
|
||||
// Error handling
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('Game error:', event.error);
|
||||
if (window.game) {
|
||||
window.game.showNotification('An error occurred. Please refresh the page.', 'error', 5000);
|
||||
}
|
||||
});
|
||||
|
||||
// Shutdown handler for debug logging
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
if (window.debugLogger) {
|
||||
try {
|
||||
await window.debugLogger.shutdown();
|
||||
} catch (error) {
|
||||
console.error('[MAIN] Failed to shutdown debug logger:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Performance monitoring
|
||||
if (window.performance && window.performance.memory) {
|
||||
setInterval(() => {
|
||||
if (window.game && window.game.isRunning) {
|
||||
const stats = window.game.getPerformanceStats();
|
||||
if (stats.memory && stats.memory.used / stats.memory.limit > 0.8) {
|
||||
console.warn('High memory usage detected:', stats.memory);
|
||||
}
|
||||
}
|
||||
}, 30000); // Check every 30 seconds
|
||||
}
|
||||
|
||||
// Global console functions
|
||||
function toggleConsole() {
|
||||
console.log('[DEBUG] toggleConsole called');
|
||||
const consoleWindow = document.getElementById('consoleWindow');
|
||||
const consoleInput = document.getElementById('consoleInput');
|
||||
|
||||
console.log('[DEBUG] consoleWindow element:', consoleWindow);
|
||||
console.log('[DEBUG] consoleInput element:', consoleInput);
|
||||
|
||||
if (!consoleWindow) {
|
||||
console.error('[DEBUG] consoleWindow element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (consoleWindow.style.display === 'flex') {
|
||||
consoleWindow.style.display = 'none';
|
||||
console.log('[DEBUG] Console hidden');
|
||||
} else {
|
||||
consoleWindow.style.display = 'flex';
|
||||
console.log('[DEBUG] Console shown');
|
||||
if (consoleInput) {
|
||||
consoleInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleConsoleInput(event) {
|
||||
if (event.key === 'Enter') {
|
||||
const input = event.target;
|
||||
const command = input.value.trim();
|
||||
|
||||
if (command) {
|
||||
executeConsoleCommand(command);
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeConsoleCommand(command) {
|
||||
const output = document.getElementById('consoleOutput');
|
||||
const commandLine = document.createElement('div');
|
||||
commandLine.className = 'console-line';
|
||||
commandLine.textContent = `> ${command}`;
|
||||
output.appendChild(commandLine);
|
||||
|
||||
// Log command to file and browser console
|
||||
console.log(`[CONSOLE] Command: ${command}`);
|
||||
if (window.logger) {
|
||||
window.logger.playerAction('Console Command', { command: command });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = processCommand(command);
|
||||
const resultLine = document.createElement('div');
|
||||
resultLine.className = `console-line ${result.type || 'success'}`;
|
||||
// Convert line breaks to HTML for proper rendering
|
||||
resultLine.innerHTML = result.message.replace(/\n/g, '<br>');
|
||||
output.appendChild(resultLine);
|
||||
|
||||
// Log result to file and browser console
|
||||
const consoleMethod = result.type === 'error' ? console.error :
|
||||
result.type === 'info' ? console.info : console.log;
|
||||
consoleMethod(`[CONSOLE] Result (${result.type}): ${result.message.replace(/\n/g, ' ')}`);
|
||||
|
||||
if (window.logger) {
|
||||
window.logger.playerAction('Console Result', {
|
||||
command: command,
|
||||
result: result.type,
|
||||
message: result.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorLine = document.createElement('div');
|
||||
errorLine.className = 'console-line console-error';
|
||||
errorLine.textContent = `Error: ${error.message}`;
|
||||
output.appendChild(errorLine);
|
||||
|
||||
// Log error to file and browser console
|
||||
console.error(`[CONSOLE] Error: ${error.message}`);
|
||||
if (window.logger) {
|
||||
window.logger.errorEvent(error, 'Console Command', { command: command });
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function processCommand(command) {
|
||||
const parts = command.split(' ');
|
||||
const cmd = parts[0].toLowerCase();
|
||||
const args = parts.slice(1);
|
||||
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Available commands:\nhelp - Show this help message\nclear - Clear console output\ncoins <amount> - Add coins to player (e.g., "coins 1000")\ngems <amount> - Add gems to player (e.g., "gems 100")\nresearch <amount> - Add research points (e.g., "research 500")\ncraftingxp <amount> - Add crafting experience (e.g., "craftingxp 200")\ngiveitem <item_id> <quantity> - Add item to inventory (e.g., "giveitem iron_ore 10")\nhealth <amount> - Set current ship health (e.g., "health 150")\nlevel <level> - Set current ship level (e.g., "level 5")\nunlock <ship_id> - Unlock a ship (e.g., "unlock heavy_fighter")\nstats - Show current player stats\nships - List all ships\ncurrent - Show current ship info`
|
||||
};
|
||||
|
||||
case 'clear':
|
||||
const output = document.getElementById('consoleOutput');
|
||||
output.innerHTML = '';
|
||||
return { type: 'success', message: 'Console cleared' };
|
||||
|
||||
case 'coins':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: coins <amount>' };
|
||||
}
|
||||
const amount = parseInt(args[0]);
|
||||
if (isNaN(amount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.addCredits(amount, 'console');
|
||||
window.game.systems.economy.updateUI();
|
||||
return { type: 'success', message: `Added ${amount} credits` };
|
||||
}
|
||||
return { type: 'error', message: 'Economy system not available' };
|
||||
|
||||
case 'gems':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: gems <amount>' };
|
||||
}
|
||||
const gemAmount = parseInt(args[0]);
|
||||
if (isNaN(gemAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.economy) {
|
||||
window.game.systems.economy.addGems(gemAmount, 'console');
|
||||
window.game.systems.economy.updateUI();
|
||||
return { type: 'success', message: `Added ${gemAmount} gems` };
|
||||
}
|
||||
return { type: 'error', message: 'Economy system not available' };
|
||||
|
||||
case 'research':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: research <amount>' };
|
||||
}
|
||||
const researchAmount = parseInt(args[0]);
|
||||
if (isNaN(researchAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
const currentSkillPoints = window.game.systems.player.stats.skillPoints || 0;
|
||||
window.game.systems.player.stats.skillPoints = currentSkillPoints + researchAmount;
|
||||
window.game.systems.player.updateUI();
|
||||
|
||||
// Also update skill system UI
|
||||
if (window.game.systems.skillSystem) {
|
||||
window.game.systems.skillSystem.updateUI();
|
||||
}
|
||||
|
||||
return { type: 'success', message: `Added ${researchAmount} skill points (Total: ${window.game.systems.player.stats.skillPoints})` };
|
||||
}
|
||||
return { type: 'error', message: 'Player system not available' };
|
||||
|
||||
case 'craftingxp':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: craftingxp <amount>' };
|
||||
}
|
||||
const craftingXpAmount = parseInt(args[0]);
|
||||
if (isNaN(craftingXpAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.skillSystem) {
|
||||
window.game.systems.skillSystem.awardCraftingExperience(craftingXpAmount);
|
||||
const currentLevel = window.game.systems.skillSystem.getSkillLevel('crafting');
|
||||
const currentExp = window.game.systems.skillSystem.getSkillExperience('crafting');
|
||||
return { type: 'success', message: `Added ${craftingXpAmount} crafting experience (Level: ${currentLevel}, XP: ${currentExp})` };
|
||||
}
|
||||
return { type: 'error', message: 'Skill system not available' };
|
||||
|
||||
case 'giveitem':
|
||||
if (args.length < 2) {
|
||||
return { type: 'error', message: 'Usage: giveitem <item_id> <quantity>' };
|
||||
}
|
||||
const itemId = args[0];
|
||||
const quantity = parseInt(args[1]);
|
||||
if (isNaN(quantity)) {
|
||||
return { type: 'error', message: 'Invalid quantity' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.inventory) {
|
||||
window.game.systems.inventory.addItem(itemId, quantity);
|
||||
window.game.systems.ui.updateInventory();
|
||||
return { type: 'success', message: `Added ${quantity}x ${itemId} to inventory` };
|
||||
}
|
||||
return { type: 'error', message: 'Inventory system not available' };
|
||||
|
||||
case 'health':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: health <amount>' };
|
||||
}
|
||||
const healthAmount = parseInt(args[0]);
|
||||
if (isNaN(healthAmount)) {
|
||||
return { type: 'error', message: 'Invalid amount' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
if (currentShip) {
|
||||
currentShip.health = Math.min(healthAmount, currentShip.maxHealth);
|
||||
window.game.systems.ship.updateCurrentShipDisplay();
|
||||
return { type: 'success', message: `Set ship health to ${currentShip.health}/${currentShip.maxHealth}` };
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'level':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: level <level>' };
|
||||
}
|
||||
const levelAmount = parseInt(args[0]);
|
||||
if (isNaN(levelAmount)) {
|
||||
return { type: 'error', message: 'Invalid level' };
|
||||
}
|
||||
if (window.game && window.game.systems && window.game.systems.player) {
|
||||
window.game.systems.player.stats.level = levelAmount;
|
||||
window.game.systems.player.updateTitle();
|
||||
window.game.systems.player.updateUI();
|
||||
return { type: 'success', message: `Set player level to ${levelAmount}` };
|
||||
}
|
||||
return { type: 'error', message: 'Player system not available' };
|
||||
|
||||
case 'unlock':
|
||||
if (args.length === 0) {
|
||||
return { type: 'error', message: 'Usage: unlock <ship_id>' };
|
||||
}
|
||||
const shipId = args[0];
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const ship = window.game.systems.ship.ships.find(s => s.id === shipId);
|
||||
if (ship) {
|
||||
ship.status = 'inactive';
|
||||
window.game.systems.ship.renderShips();
|
||||
return { type: 'success', message: `Unlocked ship: ${ship.name}` };
|
||||
} else {
|
||||
return { type: 'error', message: `Ship not found: ${shipId}` };
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'stats':
|
||||
if (window.game && window.game.systems && window.game.systems.player && window.game.systems.economy) {
|
||||
const player = window.game.systems.player;
|
||||
const economy = window.game.systems.economy;
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Player Stats:\nLevel: ${player.stats.level}\nExperience: ${player.stats.experience}/${player.stats.experienceToNext}\nCredits: ${economy.credits}\nGems: ${economy.gems}\nTotal Kills: ${player.stats.totalKills}\nDungeons Cleared: ${player.stats.dungeonsCleared}\nTitle: ${player.info.title}`
|
||||
};
|
||||
}
|
||||
return { type: 'error', message: 'Player or Economy system not available' };
|
||||
|
||||
case 'ships':
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const ships = window.game.systems.ship.ships;
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
const shipList = ships.map(ship =>
|
||||
`- ${ship.name} (${ship.id}) - Level ${ship.level} - ${ship.status} - ${ship.rarity}${currentShip.id === ship.id ? ' [CURRENT]' : ''}`
|
||||
).join('\n');
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Available Ships:\n${shipList}`
|
||||
};
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
case 'current':
|
||||
if (window.game && window.game.systems && window.game.systems.ship) {
|
||||
const currentShip = window.game.systems.ship.currentShip;
|
||||
if (currentShip) {
|
||||
return {
|
||||
type: 'info',
|
||||
message: `Current Ship:
|
||||
- Name: ${currentShip.name}
|
||||
- Class: ${currentShip.class}
|
||||
- Level: ${currentShip.level}
|
||||
- Health: ${currentShip.health}/${currentShip.maxHealth}
|
||||
- Attack: ${currentShip.attack}
|
||||
- Defense: ${currentShip.defense}
|
||||
- Speed: ${currentShip.speed}
|
||||
- Rarity: ${currentShip.rarity}`
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: 'error', message: 'Ship system not available' };
|
||||
|
||||
default:
|
||||
return { type: 'error', message: `Unknown command: ${cmd}. Type 'help' for available commands.` };
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut listener
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[DEBUG] DOMContentLoaded event fired for keyboard shortcuts');
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Log all key combinations for debugging
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
||||
console.log('[DEBUG] Key pressed:', {
|
||||
key: event.key,
|
||||
ctrlKey: event.ctrlKey,
|
||||
altKey: event.altKey,
|
||||
shiftKey: event.shiftKey,
|
||||
code: event.code
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Alt+Shift+C to toggle console
|
||||
if (event.ctrlKey && event.altKey && event.shiftKey && event.key === 'C') {
|
||||
console.log('[DEBUG] Ctrl+Alt+Shift+C detected!');
|
||||
event.preventDefault();
|
||||
|
||||
// Check if toggleConsole function exists
|
||||
if (typeof toggleConsole === 'function') {
|
||||
console.log('[DEBUG] toggleConsole function exists, calling it');
|
||||
toggleConsole();
|
||||
} else {
|
||||
console.error('[DEBUG] toggleConsole function not found!');
|
||||
}
|
||||
}
|
||||
|
||||
// Escape to close console
|
||||
if (event.key === 'Escape') {
|
||||
const consoleWindow = document.getElementById('consoleWindow');
|
||||
if (consoleWindow && consoleWindow.style.display === 'flex') {
|
||||
consoleWindow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize console output with welcome message
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const output = document.getElementById('consoleOutput');
|
||||
if (output) {
|
||||
const welcomeLine = document.createElement('div');
|
||||
welcomeLine.className = 'console-line console-info';
|
||||
welcomeLine.textContent = 'Developer Console ready. Type "help" for available commands.';
|
||||
output.appendChild(welcomeLine);
|
||||
}
|
||||
});
|
||||
@ -1,403 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Crafting System
|
||||
* Recipe definitions are loaded from the server; this file handles
|
||||
* local crafting logic, requirement checking, and UI rendering.
|
||||
*/
|
||||
|
||||
class CraftingSystem extends BaseSystem {
|
||||
constructor(gameEngine) {
|
||||
super(gameEngine);
|
||||
|
||||
this.recipes = new Map(); // recipeId -> recipe object
|
||||
this.currentCategory = 'weapons';
|
||||
this.selectedRecipe = null;
|
||||
|
||||
this._loaded = false;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Initialisation — request recipes from the server
|
||||
// ------------------------------------------------------------------ //
|
||||
async initialize() {
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
console.log('[CRAFTING SYSTEM] Requesting recipes from server');
|
||||
|
||||
if (!window.game?.socket) {
|
||||
console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected');
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recipes = await this._fetchRecipesFromServer();
|
||||
this._applyServerRecipes(recipes);
|
||||
this._loaded = true;
|
||||
console.log(`[CRAFTING SYSTEM] Loaded ${this.recipes.size} recipes from server`);
|
||||
} catch (err) {
|
||||
console.error('[CRAFTING SYSTEM] Failed to load recipes from server:', err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
_fetchRecipesFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = window.game.socket;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
socket.off('recipes_data', handler);
|
||||
reject(new Error('Recipe data request timed out'));
|
||||
}, 10000);
|
||||
|
||||
const handler = (data) => {
|
||||
clearTimeout(timeout);
|
||||
socket.off('recipes_data', handler);
|
||||
if (data && (Array.isArray(data) || typeof data === 'object')) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error('Invalid recipe data from server'));
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('recipes_data', handler);
|
||||
socket.emit('get_recipes');
|
||||
});
|
||||
}
|
||||
|
||||
_applyServerRecipes(serverRecipes) {
|
||||
this.recipes.clear();
|
||||
|
||||
// Server may return array or object keyed by id
|
||||
const asList = Array.isArray(serverRecipes)
|
||||
? serverRecipes
|
||||
: Object.values(serverRecipes);
|
||||
|
||||
for (const recipe of asList) {
|
||||
if (!recipe.id) continue;
|
||||
|
||||
// Normalise materials: server uses { itemId: qty } objects, client expects array
|
||||
let materials = recipe.materials;
|
||||
if (materials && !Array.isArray(materials)) {
|
||||
materials = Object.entries(materials).map(([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
// Normalise results similarly
|
||||
let results = recipe.results;
|
||||
if (results && !Array.isArray(results)) {
|
||||
results = Object.entries(results)
|
||||
.filter(([k]) => k !== 'experience')
|
||||
.map(([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
this.recipes.set(recipe.id, {
|
||||
...recipe,
|
||||
materials: materials || [],
|
||||
results: results || [],
|
||||
category: recipe.type || recipe.category || 'items',
|
||||
unlocked: false // will be resolved by checkRecipeUnlocks()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Runtime
|
||||
// ------------------------------------------------------------------ //
|
||||
addRecipe(id, recipe) {
|
||||
recipe.id = id;
|
||||
recipe.unlocked = false;
|
||||
this.recipes.set(id, recipe);
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.checkRecipeUnlocks();
|
||||
}
|
||||
|
||||
checkRecipeUnlocks() {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return;
|
||||
|
||||
for (const [id, recipe] of this.recipes) {
|
||||
if (recipe.unlocked) continue;
|
||||
|
||||
let canUnlock = true;
|
||||
if (recipe.requirements) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skillName) < requiredLevel) {
|
||||
canUnlock = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canUnlock) {
|
||||
recipe.unlocked = true;
|
||||
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRecipesByCategory(category) {
|
||||
return Array.from(this.recipes.values())
|
||||
.filter(r => r.category === category || r.type === category);
|
||||
}
|
||||
|
||||
canCraftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!recipe) return false;
|
||||
|
||||
if (recipe.requirements && skillSystem) {
|
||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.materials && inventory) {
|
||||
for (const mat of recipe.materials) {
|
||||
if (!inventory.hasItem(mat.id, mat.quantity)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMissingMaterials(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!recipe?.materials) return [];
|
||||
|
||||
const missing = [];
|
||||
for (const mat of recipe.materials) {
|
||||
let current = 0;
|
||||
if (inventory?.getItemCount) {
|
||||
try { current = inventory.getItemCount(mat.id) || 0; } catch (_) {}
|
||||
}
|
||||
const required = mat.quantity || 0;
|
||||
if (current < required) {
|
||||
missing.push({ id: mat.id, required, current, missing: required - current });
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
async craftRecipe(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe || !this.canCraftRecipe(recipeId)) return false;
|
||||
|
||||
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
|
||||
|
||||
if (recipe.materials) {
|
||||
for (const mat of recipe.materials) {
|
||||
this.game.systems.inventory.removeItem(mat.id, mat.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.experience && this.game.systems.skillSystem) {
|
||||
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000));
|
||||
|
||||
if (recipe.results) {
|
||||
for (const result of recipe.results) {
|
||||
this.game.systems.inventory.addItem(result.id, result.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.game.systems.questSystem) {
|
||||
this.game.systems.questSystem.onItemCrafted?.();
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING] Done: ${recipe.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
selectRecipe(recipeId) {
|
||||
this.selectedRecipe = this.recipes.get(recipeId);
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
|
||||
getSelectedRecipe() { return this.selectedRecipe; }
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// UI
|
||||
// ------------------------------------------------------------------ //
|
||||
updateUI() {
|
||||
this.updateRecipeList();
|
||||
this.updateCraftingDetails();
|
||||
this.updateCraftingInfo();
|
||||
}
|
||||
|
||||
updateRecipeList() {
|
||||
const listEl = document.getElementById('recipeList');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!this._loaded) {
|
||||
listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading recipes from server...</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const recipes = this.getRecipesByCategory(this.currentCategory);
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (recipes.length === 0) {
|
||||
listEl.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'recipe-item';
|
||||
el.dataset.recipeId = recipe.id;
|
||||
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
const missingMats = this.getMissingMaterials(recipe.id);
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
let skillsMet = true;
|
||||
|
||||
if (recipe.requirements && skillSystem) {
|
||||
for (const [skill, level] of Object.entries(recipe.requirements)) {
|
||||
if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillsMet) el.classList.add('locked');
|
||||
else if (!canCraft) el.classList.add('missing-materials');
|
||||
else el.classList.add('can-craft');
|
||||
|
||||
const reqText = recipe.requirements
|
||||
? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).join(', ')
|
||||
: 'None';
|
||||
|
||||
const matsHtml = recipe.materials.map(mat => {
|
||||
const mis = missingMats.find(m => m.id === mat.id);
|
||||
const cur = mis ? mis.current : (this.game.systems.inventory?.getItemCount(mat.id) || 0);
|
||||
const cls = mis ? 'material-item missing' : 'material-item';
|
||||
return `<div class="${cls}">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-quantity">${cur}/${mat.quantity}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="recipe-header">
|
||||
<h4>${recipe.name}</h4>
|
||||
<span class="recipe-level">Level ${reqText}</span>
|
||||
</div>
|
||||
<div class="recipe-description">${recipe.description || ''}</div>
|
||||
<div class="recipe-materials">${matsHtml}</div>
|
||||
${missingMats.length > 0 ? `
|
||||
<div class="missing-materials-text">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
|
||||
</div>` : ''}
|
||||
<div class="recipe-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${(recipe.craftingTime || 0) / 1000}s</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
this.selectRecipe(recipe.id);
|
||||
this.updateCraftingDetails();
|
||||
});
|
||||
|
||||
listEl.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
updateCraftingDetails() {
|
||||
const detailsEl = document.getElementById('craftingDetails');
|
||||
if (!detailsEl) return;
|
||||
|
||||
if (!this.selectedRecipe) {
|
||||
detailsEl.innerHTML = `
|
||||
<div class="selected-recipe">
|
||||
<h3>Select a Recipe</h3>
|
||||
<p>Choose a recipe from the list to see details and craft items.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const recipe = this.selectedRecipe;
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
|
||||
detailsEl.innerHTML = `
|
||||
<div class="selected-recipe">
|
||||
<h3>${recipe.name}</h3>
|
||||
<p class="recipe-description">${recipe.description || ''}</p>
|
||||
<div class="recipe-requirements">
|
||||
<h4>Requirements:</h4>
|
||||
${recipe.requirements
|
||||
? Object.entries(recipe.requirements).map(([s, l]) =>
|
||||
`<div class="requirement-item">
|
||||
<span class="skill-name">${s}</span>
|
||||
<span class="skill-level">Level ${l}</span>
|
||||
</div>`).join('')
|
||||
: '<p>No special requirements</p>'}
|
||||
</div>
|
||||
<div class="recipe-materials-needed">
|
||||
<h4>Materials Needed:</h4>
|
||||
${recipe.materials.map(mat =>
|
||||
`<div class="material-needed">
|
||||
<span class="material-name">${mat.id}</span>
|
||||
<span class="material-needed">x${mat.quantity}</span>
|
||||
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="recipe-results">
|
||||
<h4>Results:</h4>
|
||||
${recipe.results.map(r =>
|
||||
`<div class="result-item">
|
||||
<span class="result-name">${r.id}</span>
|
||||
<span class="result-quantity">x${r.quantity}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="recipe-info">
|
||||
<div class="experience-reward">
|
||||
<i class="fas fa-star"></i>
|
||||
<span>${recipe.experience || 0} XP</span>
|
||||
</div>
|
||||
<div class="crafting-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>${(recipe.craftingTime || 0) / 1000} seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
|
||||
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
|
||||
${canCraft ? 'Craft Item' : 'Cannot Craft'}
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
updateCraftingInfo() {
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
if (!skillSystem) return;
|
||||
|
||||
const craftingLevel = skillSystem.getSkillLevel('crafting');
|
||||
const craftingExp = skillSystem.getSkillExperience('crafting');
|
||||
const expNeeded = skillSystem.getExperienceNeeded('crafting');
|
||||
|
||||
const levelEl = document.getElementById('craftingLevel');
|
||||
const expEl = document.getElementById('craftingExp');
|
||||
if (levelEl) levelEl.textContent = craftingLevel;
|
||||
if (expEl) expEl.textContent = `${craftingExp}/${expNeeded}`;
|
||||
}
|
||||
|
||||
switchCategory(category) {
|
||||
this.currentCategory = category;
|
||||
this.selectedRecipe = null;
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in GameEngine
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CraftingSystem;
|
||||
}
|
||||
@ -1,832 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Dungeon System
|
||||
* Server-driven dungeon management client
|
||||
*/
|
||||
|
||||
// Create global function for dungeon start that's more reliable
|
||||
window.startDungeon = function(dungeonId) {
|
||||
console.log('[DUNGEON SYSTEM] startDungeon called with:', dungeonId);
|
||||
console.log('[DUNGEON SYSTEM] Game available:', !!window.game);
|
||||
console.log('[DUNGEON SYSTEM] Game systems available:', !!(window.game && window.game.systems));
|
||||
console.log('[DUNGEON SYSTEM] Dungeon system available:', !!(window.game && window.game.systems && window.game.systems.dungeonSystem));
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.startDungeon(dungeonId);
|
||||
}
|
||||
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for dungeon start');
|
||||
};
|
||||
|
||||
// Create global function for process encounter that's more reliable
|
||||
window.processEncounter = function() {
|
||||
console.log('[DUNGEON SYSTEM] processEncounter called');
|
||||
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.processEncounter();
|
||||
}
|
||||
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for process encounter');
|
||||
};
|
||||
|
||||
// Create global function for dungeon toggle that's more reliable
|
||||
window.toggleDungeonSection = function(sectionId) {
|
||||
// Try to use the dungeon system if available
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
return window.game.systems.dungeonSystem.toggleDungeonSection(sectionId);
|
||||
}
|
||||
|
||||
// Fallback: Direct DOM manipulation
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (!section || !indicator) {
|
||||
console.warn('[DUNGEON SYSTEM] Section or indicator not found:', sectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCollapsed = section.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Expand
|
||||
section.classList.remove('collapsed');
|
||||
indicator.classList.remove('fa-chevron-right');
|
||||
indicator.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
// Collapse
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
|
||||
// Save the state in the dungeon system if available
|
||||
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
|
||||
window.game.systems.dungeonSystem.collapseStates.set(sectionId, !isCollapsed);
|
||||
}
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
|
||||
};
|
||||
|
||||
class DungeonSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Current dungeon state (runtime only)
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.dungeonProgress = 0;
|
||||
this.isExploring = false;
|
||||
|
||||
// Debouncing to prevent multiple rapid clicks
|
||||
this.lastProcessTime = 0;
|
||||
this.processCooldown = 1000; // 1 second cooldown
|
||||
|
||||
// Prevent duplicate event processing
|
||||
this.lastEncounterData = null;
|
||||
this.lastNextRoomData = null;
|
||||
|
||||
// Store collapse states to preserve them during regeneration
|
||||
this.collapseStates = new Map();
|
||||
|
||||
// Track last generation to prevent unnecessary regenerations
|
||||
this.lastGenerationTime = 0;
|
||||
this.generationThrottle = 500; // 500ms throttle
|
||||
|
||||
// Server dungeons data
|
||||
this.serverDungeons = null;
|
||||
this.roomTypes = {};
|
||||
this.enemyTemplates = {};
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Client DungeonSystem initialized - server-driven mode');
|
||||
|
||||
// Set up socket event listeners
|
||||
this.setupSocketListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up Socket.IO event listeners for dungeon data
|
||||
*/
|
||||
setupSocketListeners() {
|
||||
if (!this.game.socket) {
|
||||
console.warn('[DUNGEON SYSTEM] No socket available for event listeners');
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for dungeon data response
|
||||
this.game.socket.on('dungeons_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received dungeons data:', data);
|
||||
this.serverDungeons = data.dungeons || data;
|
||||
console.log('[DUNGEON SYSTEM] Loaded grouped dungeons from server:', Object.keys(this.serverDungeons));
|
||||
// Update UI when data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for room types response
|
||||
this.game.socket.on('room_types_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received room types data:', data);
|
||||
this.roomTypes = data;
|
||||
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.roomTypes).length} room types from server`);
|
||||
// Update UI when room data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for enemy templates response
|
||||
this.game.socket.on('enemy_templates_data', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Received enemy templates data:', data);
|
||||
this.enemyTemplates = data;
|
||||
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.enemyTemplates).length} enemy templates from server`);
|
||||
// Update UI when enemy data is loaded
|
||||
this.forceGenerateDungeonList();
|
||||
});
|
||||
|
||||
// Listen for dungeon start response
|
||||
this.game.socket.on('dungeon_started', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon started:', data);
|
||||
|
||||
// Handle error responses
|
||||
if (data.success === false) {
|
||||
console.error('[DUNGEON SYSTEM] Failed to start dungeon:', data.error);
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(data.error, 'error', 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing dungeon state first
|
||||
if (this.currentDungeon) {
|
||||
console.warn('[DUNGEON SYSTEM] Clearing existing dungeon state before starting new one');
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
}
|
||||
|
||||
this.currentDungeon = data.instance;
|
||||
this.isExploring = true;
|
||||
this.dungeonProgress = 0;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] About to update UI - State:', {
|
||||
currentDungeon: !!this.currentDungeon,
|
||||
isExploring: this.isExploring,
|
||||
dungeonProgress: this.dungeonProgress,
|
||||
gameUIManager: !!this.game.systems.ui,
|
||||
instanceId: this.currentDungeon?.id
|
||||
});
|
||||
|
||||
// Update UI to show dungeon exploration
|
||||
this.updateUI();
|
||||
|
||||
// Show notification to player
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(`Entered ${data.instance.dungeonId} dungeon!`, 'success', 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for encounter response
|
||||
this.game.socket.on('encounter_data', (data) => {
|
||||
// Skip duplicate events
|
||||
if (this.lastEncounterData &&
|
||||
this.lastEncounterData.encounterIndex === data.encounterIndex &&
|
||||
this.lastEncounterData.encounter?.name === data.encounter?.name) {
|
||||
console.log('[DUNGEON SYSTEM] Skipping duplicate encounter data');
|
||||
return;
|
||||
}
|
||||
this.lastEncounterData = data;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Encounter received:', data);
|
||||
console.log('[DUNGEON SYSTEM] Current state before update:', {
|
||||
currentDungeonId: this.currentDungeon?.id,
|
||||
currentProgress: this.dungeonProgress,
|
||||
newEncounterIndex: data.encounterIndex,
|
||||
encounterType: data.encounter?.type,
|
||||
encounterName: data.encounter?.name
|
||||
});
|
||||
|
||||
this.currentRoom = data.encounter;
|
||||
this.dungeonProgress = data.encounterIndex; // Use server data, not local increment
|
||||
|
||||
// Update UI to show the new encounter
|
||||
this.updateUI();
|
||||
});
|
||||
|
||||
// Listen for encounter completion (auto-combat)
|
||||
this.game.socket.on('encounter_completed', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Encounter completed:', data);
|
||||
if (data.success) {
|
||||
// Check if dungeon is complete
|
||||
if (data.isComplete) {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completed!');
|
||||
|
||||
// Clear all dungeon state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.dungeonProgress = 0;
|
||||
this.isExploring = false;
|
||||
this.lastEncounterData = null;
|
||||
this.lastNextRoomData = null;
|
||||
|
||||
// Show completion notification
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification('Dungeon completed! 🎉', 'success', 5000);
|
||||
}
|
||||
|
||||
// Force UI to show dungeon list
|
||||
setTimeout(() => {
|
||||
this.updateUI();
|
||||
}, 1000);
|
||||
} else {
|
||||
this.currentRoom = data.nextEncounter;
|
||||
this.dungeonProgress = data.encounterIndex;
|
||||
|
||||
// Show rewards notification
|
||||
if (data.rewards && (data.rewards.credits > 0 || data.rewards.experience > 0)) {
|
||||
const rewardText = [];
|
||||
if (data.rewards.credits > 0) rewardText.push(`${data.rewards.credits} credits`);
|
||||
if (data.rewards.experience > 0) rewardText.push(`${data.rewards.experience} exp`);
|
||||
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification(`Combat complete! Gained: ${rewardText.join(', ')}`, 'success', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI to show the new state
|
||||
this.updateUI();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Error completing encounter:', data.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for next room response
|
||||
this.game.socket.on('next_room_data', (data) => {
|
||||
// Skip duplicate events
|
||||
if (this.lastNextRoomData &&
|
||||
this.lastNextRoomData.encounterIndex === data.encounterIndex &&
|
||||
this.lastNextRoomData.encounter?.name === data.encounter?.name) {
|
||||
console.log('[DUNGEON SYSTEM] Skipping duplicate next room data');
|
||||
return;
|
||||
}
|
||||
this.lastNextRoomData = data;
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Next room received:', data);
|
||||
console.log('[DUNGEON SYSTEM] Current state before update:', {
|
||||
currentDungeonId: this.currentDungeon?.id,
|
||||
currentProgress: this.dungeonProgress,
|
||||
newEncounterIndex: data.encounterIndex,
|
||||
encounterType: data.encounter?.type,
|
||||
encounterName: data.encounter?.name,
|
||||
isComplete: data.isComplete
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
this.currentRoom = data.encounter;
|
||||
this.dungeonProgress = data.encounterIndex;
|
||||
|
||||
// Update UI to show the new room
|
||||
this.updateUI();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Error moving to next room:', data.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for dungeon completion response
|
||||
this.game.socket.on('dungeon_completed', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completed:', data);
|
||||
// Reset dungeon state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
});
|
||||
|
||||
// Listen for dungeon status response
|
||||
this.game.socket.on('dungeon_status', (data) => {
|
||||
console.log('[DUNGEON SYSTEM] Dungeon status received:', data);
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Socket event listeners set up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dungeon data from server using Socket.IO packets
|
||||
*/
|
||||
async loadServerData() {
|
||||
try {
|
||||
console.log('[DUNGEON SYSTEM] Loading dungeon data from server via packets...');
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request dungeons from server
|
||||
this.game.socket.emit('get_dungeons');
|
||||
|
||||
// Request room types from server
|
||||
this.game.socket.emit('get_room_types');
|
||||
|
||||
// Request enemy templates from server
|
||||
this.game.socket.emit('get_enemy_templates');
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Server data requests sent via packets');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error loading server data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available dungeons
|
||||
*/
|
||||
getAllDungeons() {
|
||||
return this.serverDungeons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dungeons by difficulty
|
||||
*/
|
||||
getDungeonsByDifficulty(difficulty) {
|
||||
return this.serverDungeons.filter(dungeon => dungeon.difficulty === difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific dungeon by ID
|
||||
*/
|
||||
getDungeon(dungeonId) {
|
||||
return this.serverDungeons.find(dungeon => dungeon.id === dungeonId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get room type by ID
|
||||
*/
|
||||
getRoomType(roomTypeId) {
|
||||
return this.roomTypes[roomTypeId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enemy template by ID
|
||||
*/
|
||||
getEnemyTemplate(enemyId) {
|
||||
return this.enemyTemplates[enemyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start exploring a dungeon using Socket.IO packets
|
||||
*/
|
||||
async startDungeon(dungeonId) {
|
||||
try {
|
||||
console.log(`[DUNGEON SYSTEM] Starting dungeon: ${dungeonId}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to start dungeon
|
||||
this.game.socket.emit('start_dungeon', {
|
||||
dungeonId: dungeonId,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon start packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error starting dungeon:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process encounter in current dungeon room
|
||||
*/
|
||||
async processEncounter() {
|
||||
// Debounce to prevent multiple rapid clicks
|
||||
const now = Date.now();
|
||||
if (now - this.lastProcessTime < this.processCooldown) {
|
||||
console.log('[DUNGEON SYSTEM] Process throttled, please wait...');
|
||||
return null;
|
||||
}
|
||||
this.lastProcessTime = now;
|
||||
|
||||
try {
|
||||
// Safety check - make sure we have an active dungeon
|
||||
if (!this.currentDungeon) {
|
||||
console.error('[DUNGEON SYSTEM] No active dungeon to process encounter for');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Processing encounter for dungeon: ${this.currentDungeon.id}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to process encounter
|
||||
this.game.socket.emit('process_encounter', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Encounter process packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error processing encounter:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete current dungeon using Socket.IO packets
|
||||
*/
|
||||
async completeDungeon() {
|
||||
if (!this.currentDungeon || !this.isExploring) {
|
||||
console.warn('[DUNGEON SYSTEM] No active dungeon to complete');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[DUNGEON SYSTEM] Completing dungeon: ${this.currentDungeon.id}`);
|
||||
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to complete dungeon
|
||||
this.game.socket.emit('complete_dungeon', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon completion packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error completing dungeon:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player's current dungeon status using Socket.IO packets
|
||||
*/
|
||||
async getDungeonStatus() {
|
||||
try {
|
||||
if (!this.game.socket) {
|
||||
console.error('[DUNGEON SYSTEM] No socket connection available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send packet to get dungeon status
|
||||
this.game.socket.emit('get_dungeon_status', {
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
|
||||
console.log('[DUNGEON SYSTEM] Dungeon status request packet sent');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DUNGEON SYSTEM] Error getting dungeon status:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force generate dungeon list (bypasses throttle)
|
||||
*/
|
||||
forceGenerateDungeonList() {
|
||||
this.lastGenerationTime = 0; // Reset throttle
|
||||
this.generateDungeonList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dungeon list UI using server data
|
||||
*/
|
||||
generateDungeonList() {
|
||||
const now = Date.now();
|
||||
|
||||
// Throttle generation to prevent excessive calls
|
||||
if (now - this.lastGenerationTime < this.generationThrottle) {
|
||||
return; // Silently skip instead of logging
|
||||
}
|
||||
|
||||
this.lastGenerationTime = now;
|
||||
// console.log('[DUNGEON SYSTEM] Generating dungeon list UI');
|
||||
|
||||
const dungeonListElement = document.getElementById('dungeonList');
|
||||
if (!dungeonListElement) {
|
||||
console.error('[DUNGEON SYSTEM] Dungeon list element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
dungeonListElement.innerHTML = '';
|
||||
|
||||
if (!this.serverDungeons || Object.keys(this.serverDungeons).length === 0) {
|
||||
dungeonListElement.innerHTML = '<p>Loading dungeons from server...</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate HTML for each difficulty category
|
||||
let html = '';
|
||||
|
||||
Object.entries(this.serverDungeons).forEach(([difficulty, dungeons]) => {
|
||||
if (!dungeons || dungeons.length === 0) return;
|
||||
|
||||
const difficultyClass = difficulty === 'tutorial' ? 'tutorial' : difficulty;
|
||||
const difficultyTitle = difficulty === 'tutorial' ? 'Tutorial Dungeons' :
|
||||
difficulty.charAt(0).toUpperCase() + difficulty.slice(1) + ' Dungeons';
|
||||
const difficultyIcon = this.getDifficultyIcon(difficulty);
|
||||
const sectionId = `dungeon-section-${difficulty}`;
|
||||
|
||||
// Add collapsible difficulty header
|
||||
html += `
|
||||
<div class="dungeon-section">
|
||||
<div class="difficulty-header ${difficultyClass} collapsible" onclick="toggleDungeonSection('${sectionId}')">
|
||||
<div class="header-content">
|
||||
<i class="${difficultyIcon}"></i>
|
||||
<span>${difficultyTitle}</span>
|
||||
<span class="dungeon-count">(${dungeons.length})</span>
|
||||
</div>
|
||||
<div class="collapse-indicator">
|
||||
<i class="fas fa-chevron-down" id="${sectionId}-indicator"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dungeon-content" id="${sectionId}">
|
||||
`;
|
||||
|
||||
dungeons.forEach(dungeon => {
|
||||
const canEnter = this.canEnterDungeon(dungeon);
|
||||
const statusClass = canEnter ? 'available' : 'locked';
|
||||
const energyCost = dungeon.energyCost || 0;
|
||||
const healthType = dungeon.healthType || 'player';
|
||||
const healthIcon = healthType === 'ship' ? '🚀' : '👤';
|
||||
|
||||
// Each dungeon in its own individual container using proper CSS classes
|
||||
html += `
|
||||
<div class="dungeon-item ${statusClass}" data-dungeon-id="${dungeon.id}">
|
||||
<div class="dungeon-name">${dungeon.name}</div>
|
||||
<div class="dungeon-difficulty ${difficulty}">
|
||||
<i class="${difficultyIcon}"></i> ${difficulty} - ${energyCost} Energy
|
||||
</div>
|
||||
<div class="dungeon-description">${dungeon.description}</div>
|
||||
<div class="health-type">${healthIcon}</div>
|
||||
<div class="dungeon-enemies">
|
||||
<strong>Enemies:</strong>
|
||||
<div class="enemy-list">
|
||||
${this.generateEnemyList(dungeon.enemyTypes || [])}
|
||||
</div>
|
||||
</div>
|
||||
<button class="dungeon-btn" ${!canEnter ? 'disabled' : ''}
|
||||
onclick="startDungeon('${dungeon.id}')">
|
||||
${canEnter ? 'Enter Dungeon' : 'Locked'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Close the section
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
dungeonListElement.innerHTML = html;
|
||||
|
||||
// Initialize default collapse states
|
||||
this.initializeDungeonSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dungeon sections with saved collapse states
|
||||
*/
|
||||
initializeDungeonSections() {
|
||||
// Default states: tutorial and easy expanded, others collapsed
|
||||
const defaultStates = {
|
||||
'dungeon-section-tutorial': false, // expanded
|
||||
'dungeon-section-easy': false, // expanded
|
||||
'dungeon-section-medium': true, // collapsed
|
||||
'dungeon-section-hard': true, // collapsed
|
||||
'dungeon-section-extreme': true // collapsed
|
||||
};
|
||||
|
||||
Object.entries(defaultStates).forEach(([sectionId, defaultCollapsed]) => {
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (section && indicator) {
|
||||
// Use saved state if available, otherwise use default
|
||||
const shouldCollapse = this.collapseStates.has(sectionId) ?
|
||||
this.collapseStates.get(sectionId) : defaultCollapsed;
|
||||
|
||||
if (shouldCollapse) {
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle dungeon section collapse/expand
|
||||
*/
|
||||
toggleDungeonSection(sectionId) {
|
||||
// Check if game and systems are available
|
||||
if (!window.game || !window.game.systems || !window.game.systems.dungeonSystem) {
|
||||
console.warn('[DUNGEON SYSTEM] Game systems not available for toggle');
|
||||
return;
|
||||
}
|
||||
|
||||
const section = document.getElementById(sectionId);
|
||||
const indicator = document.getElementById(`${sectionId}-indicator`);
|
||||
|
||||
if (!section || !indicator) return;
|
||||
|
||||
const isCollapsed = section.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Expand
|
||||
section.classList.remove('collapsed');
|
||||
indicator.classList.remove('fa-chevron-right');
|
||||
indicator.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
// Collapse
|
||||
section.classList.add('collapsed');
|
||||
indicator.classList.remove('fa-chevron-down');
|
||||
indicator.classList.add('fa-chevron-right');
|
||||
}
|
||||
|
||||
// Save the state
|
||||
this.collapseStates.set(sectionId, !isCollapsed);
|
||||
|
||||
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get difficulty icon for dungeon
|
||||
*/
|
||||
getDifficultyIcon(difficulty) {
|
||||
const icons = {
|
||||
tutorial: 'fas fa-graduation-cap',
|
||||
easy: 'fas fa-smile',
|
||||
medium: 'fas fa-meh',
|
||||
hard: 'fas fa-frown',
|
||||
extreme: 'fas fa-skull'
|
||||
};
|
||||
return icons[difficulty] || 'fas fa-question';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate enemy list HTML for dungeon
|
||||
*/
|
||||
generateEnemyList(enemyTypes) {
|
||||
if (!enemyTypes || enemyTypes.length === 0) {
|
||||
return '<span class="no-enemies">No enemies</span>';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
enemyTypes.forEach(enemyType => {
|
||||
const enemy = this.getEnemyTemplate(enemyType);
|
||||
if (enemy) {
|
||||
html += `
|
||||
<div class="enemy-item">
|
||||
<span class="enemy-name">${enemy.name}</span>
|
||||
<div class="enemy-stats">
|
||||
<span class="health">❤️ ${enemy.health}</span>
|
||||
<span class="attack">⚔️ ${enemy.attack}</span>
|
||||
<span class="defense">🛡️ ${enemy.defense}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can enter dungeon
|
||||
*/
|
||||
canEnterDungeon(dungeon) {
|
||||
if (!this.game.systems.player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const playerLevel = this.game.systems.player.stats?.level || 1;
|
||||
const minLevel = dungeon.minLevel || 1;
|
||||
const maxLevel = dungeon.maxLevel || 999;
|
||||
const energyCost = dungeon.energyCost || 0;
|
||||
const playerEnergy = this.game.systems.player.attributes?.energy || 0;
|
||||
|
||||
return playerLevel >= minLevel &&
|
||||
playerLevel <= maxLevel &&
|
||||
playerEnergy >= energyCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit current dungeon
|
||||
*/
|
||||
exitDungeon() {
|
||||
console.log('[DUNGEON SYSTEM] Exiting dungeon');
|
||||
|
||||
if (this.currentDungeon) {
|
||||
// Send exit packet to server
|
||||
if (this.game.socket) {
|
||||
this.game.socket.emit('exit_dungeon', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset local state
|
||||
this.currentDungeon = null;
|
||||
this.currentRoom = null;
|
||||
this.isExploring = false;
|
||||
this.dungeonProgress = 0;
|
||||
|
||||
// Update UI to show dungeon list
|
||||
this.updateUI();
|
||||
|
||||
// Show notification
|
||||
if (this.game && this.game.showNotification) {
|
||||
this.game.showNotification('Exited dungeon', 'info', 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next room (for rooms without enemies)
|
||||
*/
|
||||
moveToNextRoom() {
|
||||
console.log('[DUNGEON SYSTEM] Moving to next room');
|
||||
|
||||
if (!this.currentDungeon) {
|
||||
console.warn('[DUNGEON SYSTEM] No active dungeon to continue');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request next room from server
|
||||
if (this.game.socket) {
|
||||
this.game.socket.emit('next_room', {
|
||||
instanceId: this.currentDungeon.id,
|
||||
userId: this.game.systems.player?.id || 'anonymous'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI with current dungeon information
|
||||
*/
|
||||
updateUI() {
|
||||
if (this.game.systems.ui) {
|
||||
this.game.systems.ui.updateDungeonUI({
|
||||
currentDungeon: this.currentDungeon,
|
||||
currentRoom: this.currentRoom,
|
||||
progress: this.dungeonProgress,
|
||||
isExploring: this.isExploring
|
||||
});
|
||||
} else {
|
||||
console.warn('[DUNGEON SYSTEM] UI manager not available in game.systems.ui');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize system and load server data
|
||||
*/
|
||||
async initialize() {
|
||||
console.log('[DUNGEON SYSTEM] Initializing client dungeon system...');
|
||||
|
||||
// Set up socket listeners if not already done
|
||||
if (!this.game.socket) {
|
||||
console.warn('[DUNGEON SYSTEM] Socket not available during initialization, will retry...');
|
||||
// Retry after a short delay
|
||||
setTimeout(() => {
|
||||
if (this.game.socket) {
|
||||
this.setupSocketListeners();
|
||||
this.loadServerData();
|
||||
} else {
|
||||
console.error('[DUNGEON SYSTEM] Socket still not available after retry');
|
||||
}
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupSocketListeners();
|
||||
await this.loadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
// Export DungeonSystem to global scope
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DungeonSystem = DungeonSystem;
|
||||
}
|
||||
|
||||
// Export for use in GameEngine
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DungeonSystem;
|
||||
}
|
||||
@ -1,378 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Idle System
|
||||
* Manages offline progression and idle mechanics
|
||||
*/
|
||||
|
||||
class IdleSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Idle settings
|
||||
this.maxOfflineTime = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
||||
this.lastActiveTime = Date.now();
|
||||
this.accumulatedTime = 0; // Track time for resource generation
|
||||
|
||||
// Idle production rates (online rates)
|
||||
this.productionRates = {
|
||||
credits: 0.1, // 1 credit every 10 seconds (0.1 per second)
|
||||
experience: 0, // no auto experience - only from dungeons
|
||||
energy: 1/300 // 1 energy every 5 minutes (1/300 per second)
|
||||
};
|
||||
|
||||
// Offline rates (different from online rates)
|
||||
this.offlineProductionRates = {
|
||||
credits: 1/60, // 1 credit every 1 minute (1/60 per second)
|
||||
experience: 0, // no experience offline - only from dungeons
|
||||
energy: 1/300 // 1 energy every 5 minutes (same as online)
|
||||
};
|
||||
|
||||
// Offline rewards
|
||||
this.offlineRewards = {
|
||||
credits: 0,
|
||||
experience: 0,
|
||||
energy: 0,
|
||||
items: []
|
||||
};
|
||||
|
||||
// Idle bonuses
|
||||
this.bonuses = {
|
||||
premium: 1.0,
|
||||
guild: 1.0,
|
||||
research: 1.0
|
||||
};
|
||||
|
||||
// Idle achievements
|
||||
this.achievements = {
|
||||
totalOfflineTime: 0,
|
||||
maxOfflineSession: 0,
|
||||
totalIdleCredits: 0,
|
||||
totalIdleExperience: 0
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Calculate offline progress if returning
|
||||
this.calculateOfflineProgress();
|
||||
}
|
||||
|
||||
calculateOfflineProgress(offlineTime = null) {
|
||||
const currentTime = Date.now();
|
||||
const actualOfflineTime = offlineTime || (currentTime - this.lastActiveTime);
|
||||
|
||||
// Cap offline time to maximum
|
||||
const cappedOfflineTime = Math.min(actualOfflineTime, this.maxOfflineTime);
|
||||
|
||||
if (cappedOfflineTime < 60000) { // Less than 1 minute
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate production
|
||||
const totalBonus = this.getTotalBonus();
|
||||
const productionSeconds = cappedOfflineTime / 1000;
|
||||
|
||||
this.offlineRewards = {
|
||||
credits: Math.floor(this.productionRates.credits * productionSeconds * totalBonus),
|
||||
experience: Math.floor(this.productionRates.experience * productionSeconds * totalBonus),
|
||||
energy: Math.min(
|
||||
this.game.systems.player.attributes.maxEnergy,
|
||||
Math.floor(this.productionRates.energy * productionSeconds)
|
||||
),
|
||||
items: this.generateIdleItems(cappedOfflineTime)
|
||||
};
|
||||
|
||||
// Update achievements
|
||||
this.achievements.totalOfflineTime += cappedOfflineTime;
|
||||
this.achievements.maxOfflineSession = Math.max(this.achievements.maxOfflineSession, cappedOfflineTime);
|
||||
this.achievements.totalIdleCredits += this.offlineRewards.credits;
|
||||
this.achievements.totalIdleExperience += this.offlineRewards.experience;
|
||||
|
||||
// Show offline rewards notification
|
||||
this.showOfflineRewards(cappedOfflineTime);
|
||||
}
|
||||
|
||||
getTotalBonus() {
|
||||
return this.bonuses.premium * this.bonuses.guild * this.bonuses.research;
|
||||
}
|
||||
|
||||
generateIdleItems(offlineTime) {
|
||||
const items = [];
|
||||
const hours = offlineTime / (1000 * 60 * 60);
|
||||
|
||||
// Chance to find items based on offline time
|
||||
const itemChance = Math.min(0.5, hours * 0.05);
|
||||
|
||||
if (Math.random() < itemChance) {
|
||||
const itemCount = Math.floor(hours / 2) + 1;
|
||||
|
||||
for (let i = 0; i < itemCount; i++) {
|
||||
const rarity = this.getRandomItemRarity();
|
||||
const item = this.game.systems.inventory.generateItem('consumable', rarity);
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getRandomItemRarity() {
|
||||
const roll = Math.random();
|
||||
if (roll < 0.05) return 'legendary';
|
||||
if (roll < 0.15) return 'epic';
|
||||
if (roll < 0.35) return 'rare';
|
||||
if (roll < 0.65) return 'uncommon';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
showOfflineRewards(offlineTime) {
|
||||
const timeString = this.game.formatTime(offlineTime);
|
||||
|
||||
this.game.showNotification(
|
||||
`Welcome back! You were offline for ${timeString}`,
|
||||
'info',
|
||||
5000
|
||||
);
|
||||
|
||||
// Format rewards message
|
||||
let rewardsMessage = 'Offline Rewards:\n';
|
||||
if (this.offlineRewards.credits > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.credits)} credits\n`;
|
||||
}
|
||||
if (this.offlineRewards.experience > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.experience)} XP\n`;
|
||||
}
|
||||
if (this.offlineRewards.energy > 0) {
|
||||
rewardsMessage += `+${this.game.formatNumber(this.offlineRewards.energy)} energy\n`;
|
||||
}
|
||||
if (this.offlineRewards.items.length > 0) {
|
||||
rewardsMessage += `+${this.offlineRewards.items.length} items\n`;
|
||||
}
|
||||
|
||||
this.game.showNotification(rewardsMessage, 'success', 5000);
|
||||
}
|
||||
|
||||
claimOfflineRewards() {
|
||||
// In multiplayer mode, use server communication
|
||||
if (window.smartSaveManager?.isMultiplayer) {
|
||||
this.game.showNotification('Claiming offline rewards from server...', 'info', 2000);
|
||||
|
||||
// Send request to server
|
||||
if (window.game && window.game.socket) {
|
||||
window.game.socket.emit('claimOfflineRewards', {});
|
||||
} else {
|
||||
this.game.showNotification('Not connected to server', 'error', 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Singleplayer mode - use local logic
|
||||
if (this.offlineRewards.credits === 0 &&
|
||||
this.offlineRewards.experience === 0 &&
|
||||
this.offlineRewards.items.length === 0) {
|
||||
this.game.showNotification('No offline rewards to claim', 'info', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Give rewards
|
||||
if (this.offlineRewards.credits > 0) {
|
||||
this.game.systems.economy.addCredits(this.offlineRewards.credits, 'offline');
|
||||
}
|
||||
|
||||
if (this.offlineRewards.experience > 0) {
|
||||
this.game.systems.player.addExperience(this.offlineRewards.experience);
|
||||
}
|
||||
|
||||
if (this.offlineRewards.energy > 0) {
|
||||
this.game.systems.player.restoreEnergy(this.offlineRewards.energy);
|
||||
}
|
||||
|
||||
// Add items to inventory
|
||||
if (this.offlineRewards.items.length > 0) {
|
||||
const inventory = this.game.systems.inventory;
|
||||
this.offlineRewards.items.forEach(item => {
|
||||
inventory.addItem(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset offline rewards
|
||||
this.offlineRewards = {
|
||||
credits: 0,
|
||||
experience: 0,
|
||||
energy: 0,
|
||||
items: []
|
||||
};
|
||||
|
||||
this.game.showNotification('Offline rewards claimed!', 'success', 3000);
|
||||
}
|
||||
|
||||
// Active idle production
|
||||
update(deltaTime) {
|
||||
if (this.game.state.paused) return;
|
||||
|
||||
// Use real computer time delta
|
||||
const seconds = deltaTime / 1000;
|
||||
const totalBonus = this.getTotalBonus();
|
||||
|
||||
// Only add resources once per second, not every frame
|
||||
this.accumulatedTime += seconds;
|
||||
|
||||
if (this.accumulatedTime >= 1.0) {
|
||||
// Calculate active production
|
||||
const activeCredits = Math.floor(this.productionRates.credits * totalBonus);
|
||||
const activeExperience = Math.floor(this.productionRates.experience * totalBonus);
|
||||
// const activeEnergy = this.productionRates.energy * totalBonus * 0.1; // Energy is handled differently
|
||||
|
||||
// Add resources
|
||||
if (activeCredits > 0) {
|
||||
this.game.systems.economy.addCredits(activeCredits, 'idle');
|
||||
}
|
||||
if (activeExperience > 0) {
|
||||
this.game.systems.player.addExperience(activeExperience);
|
||||
}
|
||||
|
||||
// Regenerate energy
|
||||
this.game.systems.player.restoreEnergy(this.productionRates.energy);
|
||||
|
||||
// Reset accumulated time, keeping any remainder
|
||||
this.accumulatedTime -= 1.0;
|
||||
|
||||
// Debugging: Log when resources are added
|
||||
// console.debug(`[IDLE] Added ${activeCredits} credits and ${activeExperience} XP. Accumulated time: ${this.accumulatedTime.toFixed(2)}s`);
|
||||
}
|
||||
|
||||
// Update last active time for offline calculations
|
||||
this.lastActiveTime = Date.now();
|
||||
}
|
||||
|
||||
// Upgrade production rates
|
||||
upgradeProduction(type) {
|
||||
const upgradeCosts = {
|
||||
credits: 100,
|
||||
experience: 150,
|
||||
energy: 80
|
||||
};
|
||||
|
||||
const cost = upgradeCosts[type];
|
||||
if (!cost || this.game.systems.economy.credits < cost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.game.systems.economy.removeCredits(cost);
|
||||
|
||||
switch (type) {
|
||||
case 'credits':
|
||||
this.productionRates.credits += 2;
|
||||
break;
|
||||
case 'experience':
|
||||
this.productionRates.experience += 1;
|
||||
break;
|
||||
case 'energy':
|
||||
this.productionRates.energy += 0.2;
|
||||
break;
|
||||
}
|
||||
|
||||
this.game.showNotification(`Production upgraded: ${type}!`, 'success', 3000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bonus management
|
||||
setBonus(type, value) {
|
||||
if (this.bonuses[type] !== undefined) {
|
||||
this.bonuses[type] = value;
|
||||
this.game.showNotification(`${type} bonus set to ${value}x`, 'info', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Achievement checking
|
||||
checkAchievements() {
|
||||
const achievements = [
|
||||
{
|
||||
id: 'idle_warrior',
|
||||
name: 'Idle Warrior',
|
||||
description: 'Earn 1,000,000 credits from idle',
|
||||
condition: () => this.achievements.totalIdleCredits >= 1000000,
|
||||
reward: { gems: 50, experience: 1000 }
|
||||
},
|
||||
{
|
||||
id: 'time_master',
|
||||
name: 'Time Master',
|
||||
description: 'Accumulate 24 hours of offline time',
|
||||
condition: () => this.achievements.totalOfflineTime >= 24 * 60 * 60 * 1000,
|
||||
reward: { gems: 25, experience: 500 }
|
||||
},
|
||||
{
|
||||
id: 'marathon_idle',
|
||||
name: 'Marathon Idle',
|
||||
description: 'Be offline for more than 12 hours at once',
|
||||
condition: () => this.achievements.maxOfflineSession >= 12 * 60 * 60 * 1000,
|
||||
reward: { gems: 100, experience: 2000 }
|
||||
}
|
||||
];
|
||||
|
||||
achievements.forEach(achievement => {
|
||||
if (achievement.condition()) {
|
||||
this.unlockAchievement(achievement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unlockAchievement(achievement) {
|
||||
this.game.showNotification(`Achievement Unlocked: ${achievement.name}!`, 'success', 5000);
|
||||
this.game.showNotification(achievement.description, 'info', 3000);
|
||||
|
||||
// Give rewards
|
||||
if (achievement.reward.gems) {
|
||||
this.game.systems.economy.addGems(achievement.reward.gems, 'achievement');
|
||||
}
|
||||
|
||||
if (achievement.reward.experience) {
|
||||
this.game.systems.player.addExperience(achievement.reward.experience);
|
||||
}
|
||||
}
|
||||
|
||||
// UI updates
|
||||
updateUI() {
|
||||
const offlineTimeElement = document.getElementById('offlineTime');
|
||||
const offlineResourcesElement = document.getElementById('offlineResources');
|
||||
const claimOfflineBtn = document.getElementById('claimOfflineBtn');
|
||||
|
||||
if (offlineTimeElement) {
|
||||
const totalRewards = this.offlineRewards.credits +
|
||||
this.offlineRewards.experience +
|
||||
(this.offlineRewards.items.length * 100);
|
||||
offlineTimeElement.textContent = totalRewards > 0 ? 'Available' : 'None';
|
||||
}
|
||||
|
||||
if (offlineResourcesElement) {
|
||||
const totalRewards = this.offlineRewards.credits +
|
||||
this.offlineRewards.experience +
|
||||
(this.offlineRewards.items.length * 100);
|
||||
offlineResourcesElement.textContent = this.game.formatNumber(totalRewards);
|
||||
}
|
||||
|
||||
if (claimOfflineBtn) {
|
||||
const hasRewards = this.offlineRewards.credits > 0 ||
|
||||
this.offlineRewards.experience > 0 ||
|
||||
this.offlineRewards.items.length > 0;
|
||||
claimOfflineBtn.disabled = !hasRewards;
|
||||
}
|
||||
}
|
||||
|
||||
// Save/Load
|
||||
save() {
|
||||
return {
|
||||
lastActiveTime: this.lastActiveTime,
|
||||
productionRates: this.productionRates,
|
||||
bonuses: this.bonuses,
|
||||
achievements: this.achievements,
|
||||
offlineRewards: this.offlineRewards
|
||||
};
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.lastActiveTime) this.lastActiveTime = data.lastActiveTime;
|
||||
if (data.productionRates) this.productionRates = { ...this.productionRates, ...data.productionRates };
|
||||
if (data.bonuses) this.bonuses = { ...this.bonuses, ...data.bonuses };
|
||||
if (data.achievements) this.achievements = { ...this.achievements, ...data.achievements };
|
||||
if (data.offlineRewards) this.offlineRewards = data.offlineRewards;
|
||||
}
|
||||
}
|
||||
@ -1,468 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Item System
|
||||
* Dynamically loads and manages items from the GameServer
|
||||
*/
|
||||
|
||||
class ItemSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Item storage
|
||||
this.itemCatalog = new Map(); // itemId -> item data
|
||||
this.shopItems = []; // Array of shop items (legacy)
|
||||
this.shopItemsByCategory = {}; // Categorized shop items (new structure)
|
||||
this.lastUpdated = null;
|
||||
|
||||
// Loading state
|
||||
this.isLoading = false;
|
||||
this.loadPromise = null;
|
||||
|
||||
// Event listeners
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the item system and load data from server
|
||||
*/
|
||||
async initialize() {
|
||||
console.log('[ITEM SYSTEM] Initializing client item system');
|
||||
|
||||
if (this.loadPromise) {
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
this.loadPromise = this.loadFromServer();
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all items from the GameServer
|
||||
*/
|
||||
async loadFromServer() {
|
||||
if (this.isLoading) {
|
||||
console.log('[ITEM SYSTEM] Already loading items from server');
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
console.log('[ITEM SYSTEM] Loading items from GameServer - Multiplayer Mode');
|
||||
console.log('[ITEM SYSTEM] Socket connection status:', !!window.game?.socket);
|
||||
|
||||
if (!window.game || !window.game.socket) {
|
||||
throw new Error('Not connected to server - multiplayer mode requires server connection');
|
||||
}
|
||||
|
||||
// Load shop items from server
|
||||
const shopItems = await this.fetchShopItems();
|
||||
|
||||
// Handle new shop structure (categorized) vs old structure (flat array)
|
||||
let totalItems = 0;
|
||||
if (Array.isArray(shopItems)) {
|
||||
// Old structure: flat array
|
||||
totalItems = shopItems.length;
|
||||
console.log('[ITEM SYSTEM] Received', totalItems, 'items from server (old structure)');
|
||||
this.processServerItems(shopItems);
|
||||
this.shopItemsByCategory = {}; // Clear categorized data
|
||||
} else if (shopItems && typeof shopItems === 'object') {
|
||||
// New structure: categorized object
|
||||
totalItems = Object.values(shopItems).reduce((sum, categoryItems) => sum + categoryItems.length, 0);
|
||||
console.log('[ITEM SYSTEM] Received', totalItems, 'items from server (new structure)');
|
||||
console.log('[ITEM SYSTEM] Categories:', Object.keys(shopItems));
|
||||
|
||||
// Store categorized data
|
||||
this.shopItemsByCategory = shopItems;
|
||||
|
||||
// Flatten all items for processing
|
||||
const allItems = Object.values(shopItems).flat();
|
||||
this.processServerItems(allItems);
|
||||
} else {
|
||||
console.warn('[ITEM SYSTEM] Invalid shop items structure received:', typeof shopItems);
|
||||
totalItems = 0;
|
||||
this.shopItemsByCategory = {};
|
||||
}
|
||||
|
||||
this.lastUpdated = Date.now();
|
||||
console.log(`[ITEM SYSTEM] Successfully loaded ${this.itemCatalog.size} items from server`);
|
||||
console.log('[ITEM SYSTEM] Item categories loaded:', Object.keys(this.itemCatalog).length);
|
||||
|
||||
// Emit loaded event
|
||||
this.emit('itemsLoaded', {
|
||||
itemCount: this.itemCatalog.size,
|
||||
shopItemCount: this.shopItems.length,
|
||||
timestamp: this.lastUpdated
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ITEM SYSTEM] Failed to load items from server:', error);
|
||||
console.error('[ITEM SYSTEM] Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
socketConnected: !!window.game?.socket,
|
||||
socketId: window.game?.socket?.id
|
||||
});
|
||||
|
||||
// No fallback - emit error event
|
||||
this.emit('itemsLoadError', error);
|
||||
return false;
|
||||
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch shop items from the GameServer
|
||||
*/
|
||||
async fetchShopItems() {
|
||||
console.log('[ITEM SYSTEM] Starting fetchShopItems');
|
||||
|
||||
if (!window.game || !window.game.socket) {
|
||||
console.error('[ITEM SYSTEM] No socket connection available');
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Socket ID:', window.game.socket.id);
|
||||
console.log('[ITEM SYSTEM] Socket connected:', window.game.socket.connected);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('[ITEM SYSTEM] Server request timeout after 10 seconds');
|
||||
window.game.socket.off('shopItemsReceived', handleResponse);
|
||||
reject(new Error('Server request timeout'));
|
||||
}, 10000);
|
||||
|
||||
// Test server connection first
|
||||
console.log('[ITEM SYSTEM] Testing server connection...');
|
||||
window.game.socket.emit('ping', { timestamp: Date.now() });
|
||||
|
||||
// Listen for ping response
|
||||
const pingHandler = (data) => {
|
||||
console.log('[ITEM SYSTEM] Ping response received:', data);
|
||||
console.log('[ITEM SYSTEM] Server is responding! Ping roundtrip:', Date.now() - data.received, 'ms');
|
||||
window.game.socket.off('ping', pingHandler);
|
||||
window.game.socket.off('pong', pingHandler);
|
||||
};
|
||||
window.game.socket.on('ping', pingHandler);
|
||||
|
||||
// Listen for pong response (backup)
|
||||
const pongHandler = (data) => {
|
||||
console.log('[ITEM SYSTEM] Pong response received:', data);
|
||||
console.log('[ITEM SYSTEM] Server is responding! Pong roundtrip:', Date.now() - data.timestamp, 'ms');
|
||||
window.game.socket.off('pong', pongHandler);
|
||||
};
|
||||
window.game.socket.on('pong', pongHandler);
|
||||
|
||||
// Request shop items from server
|
||||
console.log('[ITEM SYSTEM] Emitting getShopItems request');
|
||||
console.log('[ITEM SYSTEM] Socket state:', {
|
||||
connected: window.game.socket.connected,
|
||||
id: window.game.socket.id
|
||||
});
|
||||
|
||||
window.game.socket.emit('getShopItems', {});
|
||||
console.log('[ITEM SYSTEM] Request sent, waiting for response...');
|
||||
|
||||
// Listen for response
|
||||
const handleResponse = (data) => {
|
||||
console.log('[ITEM SYSTEM] Received shopItemsReceived response:', data);
|
||||
clearTimeout(timeout);
|
||||
window.game.socket.off('shopItemsReceived', handleResponse);
|
||||
|
||||
console.log('[ITEM SYSTEM] Response success:', data.success);
|
||||
console.log('[ITEM SYSTEM] Response shopItems keys:', data.shopItems ? Object.keys(data.shopItems) : 'none');
|
||||
|
||||
if (data.success) {
|
||||
console.log('[ITEM SYSTEM] Successfully received shop data');
|
||||
console.log('[ITEM SYSTEM] Response timestamp:', data.timestamp);
|
||||
|
||||
// Log item counts per category
|
||||
if (data.shopItems) {
|
||||
Object.entries(data.shopItems).forEach(([category, items]) => {
|
||||
console.log(`[ITEM SYSTEM] ${category}: ${items.length} items`);
|
||||
});
|
||||
}
|
||||
|
||||
resolve(data.shopItems || {});
|
||||
} else {
|
||||
console.error('[ITEM SYSTEM] Server returned error:', data.error);
|
||||
reject(new Error(data.error || 'Failed to load shop items'));
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ITEM SYSTEM] Setting up shopItemsReceived listener');
|
||||
window.game.socket.on('shopItemsReceived', handleResponse);
|
||||
|
||||
// Verify the listener was set up
|
||||
const listeners = window.game.socket.listeners('shopItemsReceived');
|
||||
console.log('[ITEM SYSTEM] shopItemsReceived listeners count:', listeners.length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch specific item details from server
|
||||
*/
|
||||
async fetchItemDetails(itemId) {
|
||||
if (!window.game || !window.game.socket) {
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Server request timeout'));
|
||||
}, 5000);
|
||||
|
||||
// Request item details from server
|
||||
window.game.socket.emit('getItemDetails', { itemId });
|
||||
|
||||
// Listen for response
|
||||
const handleResponse = (data) => {
|
||||
clearTimeout(timeout);
|
||||
window.game.socket.off('itemDetailsReceived', handleResponse);
|
||||
|
||||
if (data.success) {
|
||||
// Cache the item
|
||||
this.itemCatalog.set(itemId, data.item);
|
||||
resolve(data.item);
|
||||
} else {
|
||||
reject(new Error(data.error || 'Item not found'));
|
||||
}
|
||||
};
|
||||
|
||||
window.game.socket.on('itemDetailsReceived', handleResponse);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process items received from server
|
||||
*/
|
||||
processServerItems(items) {
|
||||
// Safety check for items parameter
|
||||
if (!items || !Array.isArray(items)) {
|
||||
console.error('[ITEM SYSTEM] Invalid items parameter:', items);
|
||||
console.error('[ITEM SYSTEM] Expected array, got:', typeof items);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Processing', items.length, 'items from server');
|
||||
console.log('[ITEM SYSTEM] Sample items:', items.slice(0, 3));
|
||||
|
||||
this.itemCatalog.clear();
|
||||
this.shopItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
// Store in catalog
|
||||
this.itemCatalog.set(item.id, item);
|
||||
|
||||
// Add to shop items if available for shop
|
||||
if (item.categories && item.categories.includes('shop')) {
|
||||
this.shopItems.push(item);
|
||||
}
|
||||
|
||||
// console.log('[ITEM SYSTEM] Added item:', {
|
||||
// id: item.id,
|
||||
// name: item.name,
|
||||
// type: item.type,
|
||||
// rarity: item.rarity,
|
||||
// price: item.price,
|
||||
// categories: item.categories
|
||||
// });
|
||||
}
|
||||
|
||||
console.log('[ITEM SYSTEM] Processing complete - Catalog:', this.itemCatalog.size, 'Shop items:', this.shopItems.length);
|
||||
console.log('[ITEM SYSTEM] Shop items by type:', this.shopItems.reduce((acc, item) => {
|
||||
acc[item.type] = (acc[item.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item by ID
|
||||
*/
|
||||
getItem(itemId) {
|
||||
// Return from cache if available
|
||||
if (this.itemCatalog.has(itemId)) {
|
||||
return this.itemCatalog.get(itemId);
|
||||
}
|
||||
|
||||
// Try to fetch from server if not cached
|
||||
if (window.game && window.game.socket) {
|
||||
this.fetchItemDetails(itemId).catch(error => {
|
||||
console.warn(`[ITEM SYSTEM] Failed to fetch item ${itemId}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all shop items
|
||||
*/
|
||||
getShopItems() {
|
||||
return [...this.shopItems];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shop items by category (new structure)
|
||||
*/
|
||||
getShopItemsByCategory() {
|
||||
return this.shopItemsByCategory || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by category
|
||||
*/
|
||||
getItemsByCategory(category) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item =>
|
||||
item.type === category || (item.categories && item.categories.includes(category))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by type
|
||||
*/
|
||||
getItemsByType(type) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item => item.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by rarity
|
||||
*/
|
||||
getItemsByRarity(rarity) {
|
||||
return Array.from(this.itemCatalog.values()).filter(item => item.rarity === rarity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can use item based on requirements
|
||||
*/
|
||||
canPlayerUseItem(item, playerLevel = null) {
|
||||
if (!item.requirements) return true;
|
||||
|
||||
// Get player level if not provided
|
||||
if (playerLevel === null && window.game && window.game.systems && window.game.systems.player) {
|
||||
playerLevel = window.game.systems.player.level;
|
||||
}
|
||||
|
||||
// Check level requirement
|
||||
if (item.requirements.level && playerLevel < item.requirements.level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add other requirement checks here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered shop items for current player
|
||||
*/
|
||||
getAvailableShopItems() {
|
||||
return this.shopItems.filter(item => this.canPlayerUseItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format item price for display
|
||||
*/
|
||||
formatPrice(item) {
|
||||
if (!item.price) return 'Free';
|
||||
|
||||
const currency = item.currency || 'credits';
|
||||
const price = this.game.formatNumber(item.price);
|
||||
|
||||
return `${price} ${currency}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item rarity color
|
||||
*/
|
||||
getRarityColor(rarity) {
|
||||
const colors = {
|
||||
common: '#888888',
|
||||
uncommon: '#00ff00',
|
||||
rare: '#0088ff',
|
||||
legendary: '#ff8800',
|
||||
epic: '#ff00ff'
|
||||
};
|
||||
|
||||
return colors[rarity] || '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh items from server
|
||||
*/
|
||||
async refresh() {
|
||||
console.log('[ITEM SYSTEM] Refreshing items from server');
|
||||
return this.loadFromServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event system
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
this.eventListeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
for (const callback of this.eventListeners.get(event)) {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`[ITEM SYSTEM] Error in event listener for ${event}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* catalog getter — alias for shopItemsByCategory, used by Economy.updateShopUI
|
||||
*/
|
||||
get catalog() {
|
||||
return this.shopItemsByCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
totalItems: this.itemCatalog.size,
|
||||
shopItems: this.shopItems.length,
|
||||
lastUpdated: this.lastUpdated,
|
||||
isLoading: this.isLoading,
|
||||
socketConnected: !!(window.game?.socket),
|
||||
socketId: window.game?.socket?.id
|
||||
};
|
||||
|
||||
// Add category breakdown
|
||||
stats.categories = {};
|
||||
for (const item of this.itemCatalog.values()) {
|
||||
stats.categories[item.type] = (stats.categories[item.type] || 0) + 1;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ItemSystem;
|
||||
} else {
|
||||
window.ItemSystem = ItemSystem;
|
||||
}
|
||||
@ -1,247 +0,0 @@
|
||||
class ShipSystem {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.ships = [];
|
||||
this.currentShip = null;
|
||||
this.initializeShips();
|
||||
}
|
||||
|
||||
initializeShips() {
|
||||
// Initialize with player's current ship instead of static data
|
||||
this.ships = [];
|
||||
|
||||
// Wait for game systems to be ready, then sync with player ship
|
||||
setTimeout(() => {
|
||||
this.syncWithPlayerShip();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
syncWithPlayerShip() {
|
||||
const player = this.game.systems.player;
|
||||
if (!player || !player.ship) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player && player.ship) {
|
||||
// Create ship object from player's current ship
|
||||
const playerShip = {
|
||||
id: 'current_ship',
|
||||
name: player.ship.name || 'Starter Cruiser',
|
||||
class: player.ship.class || 'Cruiser',
|
||||
level: player.ship.level || 1,
|
||||
health: player.ship.health || player.ship.maxHealth || 100,
|
||||
maxHealth: player.ship.maxHealth || 100,
|
||||
attack: player.ship.attack || player.attributes.attack || 10,
|
||||
defense: player.ship.defence || player.attributes.defense || 5,
|
||||
speed: player.ship.speed || player.attributes.speed || 10,
|
||||
image: player.ship.texture || 'assets/textures/ships/starter_cruiser.png',
|
||||
status: 'active',
|
||||
experience: 0,
|
||||
requiredExp: 100,
|
||||
rarity: 'Common'
|
||||
};
|
||||
|
||||
this.ships = [playerShip];
|
||||
this.currentShip = playerShip;
|
||||
|
||||
// Update the display immediately
|
||||
this.updateCurrentShipDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
renderShips() {
|
||||
const shipGrid = document.getElementById('shipGrid');
|
||||
if (!shipGrid) return;
|
||||
|
||||
shipGrid.innerHTML = '';
|
||||
|
||||
this.ships.forEach(ship => {
|
||||
const shipCard = this.createShipCard(ship);
|
||||
shipGrid.appendChild(shipCard);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ship image URL from server or local
|
||||
*/
|
||||
getShipImageUrl(ship) {
|
||||
if (!ship) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
// For multiplayer, get from server
|
||||
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||
return `${serverUrl}/images/ships/${ship.id}.png`;
|
||||
}
|
||||
|
||||
// For singleplayer, use local path
|
||||
return ship.image || ship.texture || 'assets/textures/ships/starter_cruiser.png';
|
||||
}
|
||||
|
||||
createShipCard(ship) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `ship-card ${ship.status === 'active' ? 'active' : ''}`;
|
||||
card.dataset.shipId = ship.id;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="ship-card-header">
|
||||
<img src="${this.getShipImageUrl(ship)}" alt="${ship.name}"
|
||||
onerror="this.src='${window.smartSaveManager?.isMultiplayer ? 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png' : 'assets/textures/missing-texture.png'}'"
|
||||
class="ship-card-image">
|
||||
<div class="ship-card-info">
|
||||
<div class="ship-card-rarity ${ship.rarity.toLowerCase()}">${ship.rarity}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
updateCurrentShipDisplay() {
|
||||
// Use player's ship data instead of this.currentShip
|
||||
const player = this.game.systems.player;
|
||||
if (!player || !player.ship) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = {
|
||||
currentShipImage: document.getElementById('currentShipImage'),
|
||||
currentShipName: document.getElementById('currentShipName'),
|
||||
currentShipClass: document.getElementById('currentShipClass'),
|
||||
currentShipLevel: document.getElementById('currentShipLevel'),
|
||||
currentShipHealth: document.getElementById('currentShipHealth'),
|
||||
currentShipAttack: document.getElementById('currentShipAttack'),
|
||||
currentShipDefense: document.getElementById('currentShipDefense'),
|
||||
currentShipSpeed: document.getElementById('currentShipSpeed')
|
||||
};
|
||||
|
||||
// Use player's ship data
|
||||
const ship = player.ship;
|
||||
|
||||
if (elements.currentShipImage) {
|
||||
// Use server image for multiplayer, local for singleplayer
|
||||
let imagePath;
|
||||
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || 'http://localhost:3002';
|
||||
imagePath = `${serverUrl}/images/ships/${ship.class || 'starter_cruiser'}.png`;
|
||||
} else {
|
||||
imagePath = ship.texture || `assets/textures/ships/starter_cruiser.png`;
|
||||
}
|
||||
|
||||
elements.currentShipImage.src = imagePath;
|
||||
elements.currentShipImage.alt = ship.name;
|
||||
elements.currentShipImage.onerror = function() {
|
||||
this.src = window.smartSaveManager?.isMultiplayer ?
|
||||
'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png' :
|
||||
'assets/textures/missing-texture.png';
|
||||
};
|
||||
}
|
||||
if (elements.currentShipName) elements.currentShipName.textContent = ship.name;
|
||||
if (elements.currentShipClass) elements.currentShipClass.textContent = ship.class || 'Unknown';
|
||||
if (elements.currentShipLevel) elements.currentShipLevel.textContent = ship.level || 1;
|
||||
if (elements.currentShipHealth) elements.currentShipHealth.textContent = `${ship.health}/${ship.maxHealth}`;
|
||||
if (elements.currentShipAttack) elements.currentShipAttack.textContent = ship.attack || 0;
|
||||
if (elements.currentShipDefense) elements.currentShipDefense.textContent = ship.defence || ship.defense || 0;
|
||||
if (elements.currentShipSpeed) elements.currentShipSpeed.textContent = ship.speed || 0;
|
||||
}
|
||||
|
||||
switchShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship || ship.status === 'active') return;
|
||||
|
||||
// Deactivate current ship
|
||||
if (this.currentShip) {
|
||||
this.currentShip.status = 'inactive';
|
||||
}
|
||||
|
||||
// Activate new ship
|
||||
ship.status = 'active';
|
||||
this.currentShip = ship;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
// Show notification
|
||||
this.game.showNotification(`Switched to ${ship.name}!`, 'success', 3000);
|
||||
}
|
||||
|
||||
upgradeShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship) return;
|
||||
|
||||
const upgradeCost = ship.level * 1000;
|
||||
|
||||
if (this.game.systems.economy.getCredits() < upgradeCost) {
|
||||
this.game.showNotification(`Not enough credits! Need ${upgradeCost} credits.`, 'error', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Upgrade ship
|
||||
this.game.systems.economy.removeCredits(upgradeCost);
|
||||
ship.level++;
|
||||
ship.maxHealth += 10;
|
||||
ship.health = ship.maxHealth; // Full heal on upgrade
|
||||
ship.attack += 2;
|
||||
ship.defense += 1;
|
||||
ship.speed += 1;
|
||||
ship.requiredExp = ship.level * 100;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
this.game.showNotification(`${ship.name} upgraded to level ${ship.level}!`, 'success', 3000);
|
||||
}
|
||||
|
||||
repairShip(shipId) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship || ship.health >= ship.maxHealth) return;
|
||||
|
||||
const repairCost = Math.floor((ship.maxHealth - ship.health) * 0.5);
|
||||
|
||||
if (this.game.systems.economy.getCredits() < repairCost) {
|
||||
this.game.showNotification(`Not enough credits! Need ${repairCost} credits.`, 'error', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Repair ship
|
||||
this.game.systems.economy.removeCredits(repairCost);
|
||||
ship.health = ship.maxHealth;
|
||||
|
||||
// Update displays
|
||||
this.renderShips();
|
||||
this.updateCurrentShipDisplay();
|
||||
|
||||
this.game.showNotification(`${ship.name} fully repaired!`, 'success', 3000);
|
||||
}
|
||||
|
||||
addExperience(shipId, amount) {
|
||||
const ship = this.ships.find(s => s.id === shipId);
|
||||
if (!ship) return;
|
||||
|
||||
ship.experience += amount;
|
||||
|
||||
// Check for level up
|
||||
while (ship.experience >= ship.requiredExp) {
|
||||
ship.experience -= ship.requiredExp;
|
||||
this.upgradeShip(shipId);
|
||||
}
|
||||
|
||||
this.renderShips();
|
||||
if (this.currentShip && this.currentShip.id === shipId) {
|
||||
this.updateCurrentShipDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
getShip(shipId) {
|
||||
return this.ships.find(s => s.id === shipId);
|
||||
}
|
||||
|
||||
getCurrentShip() {
|
||||
return this.currentShip;
|
||||
}
|
||||
|
||||
getAllShips() {
|
||||
return this.ships;
|
||||
}
|
||||
}
|
||||
@ -1,407 +0,0 @@
|
||||
/**
|
||||
* Galaxy Strike Online - Client Skill System
|
||||
* Skill definitions are loaded from the server; this file handles
|
||||
* local progression tracking, UI rendering, and skill-point spending.
|
||||
*/
|
||||
|
||||
class SkillSystem {
|
||||
constructor(gameEngine) {
|
||||
this.game = gameEngine;
|
||||
|
||||
// Populated after server responds to 'get_skills'
|
||||
this.skills = { combat: {}, science: {}, crafting: {} };
|
||||
|
||||
this.categories = {
|
||||
combat: 'Combat',
|
||||
science: 'Science',
|
||||
crafting: 'Crafting'
|
||||
};
|
||||
|
||||
this.experienceRates = { combat: 1.0, science: 0.8, crafting: 0.6 };
|
||||
this.activeBuffs = {};
|
||||
|
||||
// Loading state
|
||||
this._loaded = false;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Initialisation — request skill definitions from the server
|
||||
// ------------------------------------------------------------------ //
|
||||
async initialize() {
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
console.log('[SKILL SYSTEM] Requesting skill definitions from server');
|
||||
|
||||
if (!window.game?.socket) {
|
||||
console.warn('[SKILL SYSTEM] No socket connection — skills will load when connected');
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const serverSkills = await this._fetchSkillsFromServer();
|
||||
this._applyServerDefinitions(serverSkills);
|
||||
this._loaded = true;
|
||||
console.log('[SKILL SYSTEM] Skill definitions loaded from server');
|
||||
} catch (err) {
|
||||
console.error('[SKILL SYSTEM] Failed to load skills from server:', err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
_fetchSkillsFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = window.game.socket;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
socket.off('skills_data', handler);
|
||||
reject(new Error('Skill data request timed out'));
|
||||
}, 10000);
|
||||
|
||||
const handler = (data) => {
|
||||
clearTimeout(timeout);
|
||||
socket.off('skills_data', handler);
|
||||
if (data && (Array.isArray(data) || typeof data === 'object')) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error('Invalid skill data from server'));
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('skills_data', handler);
|
||||
socket.emit('get_skills');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge server skill definitions into the local skill map.
|
||||
* Preserves any progress already loaded from playerData.
|
||||
*/
|
||||
_applyServerDefinitions(serverSkills) {
|
||||
// Server may return an array or a categorised object
|
||||
const asList = Array.isArray(serverSkills)
|
||||
? serverSkills
|
||||
: Object.values(serverSkills).flat();
|
||||
|
||||
// Reset to empty categories first
|
||||
this.skills = { combat: {}, science: {}, crafting: {} };
|
||||
|
||||
for (const skill of asList) {
|
||||
const cat = skill.category || 'combat';
|
||||
if (!this.skills[cat]) this.skills[cat] = {};
|
||||
|
||||
// Keep any existing progress if already loaded from save data
|
||||
const existing = this.skills[cat][skill.id] || {};
|
||||
this.skills[cat][skill.id] = {
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
maxLevel: skill.maxLevel || 100,
|
||||
effects: skill.bonuses || skill.effects || {},
|
||||
icon: skill.icon || 'fa-star',
|
||||
// Progress fields — kept from existing save data if present
|
||||
currentLevel: existing.currentLevel ?? 0,
|
||||
experience: existing.experience ?? 0,
|
||||
experienceToNext: existing.experienceToNext ?? (skill.experiencePerLevel || 1000),
|
||||
unlocked: existing.unlocked ?? (skill.defaultUnlocked !== false),
|
||||
requiredLevel: skill.requiredLevel ?? null
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[SKILL SYSTEM] Applied server definitions. Categories:', Object.keys(this.skills));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Skill progression
|
||||
// ------------------------------------------------------------------ //
|
||||
addSkillExperience(category, skillId, amount) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
|
||||
|
||||
skill.experience += amount;
|
||||
|
||||
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
|
||||
this.levelUpSkill(category, skillId);
|
||||
}
|
||||
|
||||
this.applySkillEffects();
|
||||
return true;
|
||||
}
|
||||
|
||||
levelUpSkill(category, skillId) {
|
||||
const skill = this.skills[category][skillId];
|
||||
const excess = skill.experience - skill.experienceToNext;
|
||||
|
||||
skill.currentLevel++;
|
||||
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
|
||||
skill.experience = Math.max(0, excess);
|
||||
|
||||
this.applySkillEffects();
|
||||
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
|
||||
}
|
||||
|
||||
upgradeSkill(category, skillId) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
const player = this.game.systems.player;
|
||||
|
||||
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
|
||||
if (!skill.unlocked) { this.game.showNotification('Skill is locked', 'error', 3000); return false; }
|
||||
if (skill.currentLevel >= skill.maxLevel){ this.game.showNotification('Skill is at maximum level', 'warning', 3000); return false; }
|
||||
if (player.stats.skillPoints < 1) { this.game.showNotification('Not enough skill points', 'error', 3000); return false; }
|
||||
|
||||
player.stats.skillPoints--;
|
||||
this.levelUpSkill(category, skillId);
|
||||
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
unlockSkill(category, skillId) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
const player = this.game.systems.player;
|
||||
|
||||
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
|
||||
if (skill.unlocked) { this.game.showNotification('Skill is already unlocked', 'warning', 3000); return false; }
|
||||
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
|
||||
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
if (player.stats.skillPoints < 2) {
|
||||
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
player.stats.skillPoints -= 2;
|
||||
skill.unlocked = true;
|
||||
skill.currentLevel = 1;
|
||||
this.applySkillEffects();
|
||||
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
|
||||
return true;
|
||||
}
|
||||
|
||||
applySkillEffects() {
|
||||
const player = this.game.systems.player;
|
||||
this.resetToBaseStats();
|
||||
|
||||
for (const category of Object.values(this.skills)) {
|
||||
for (const skill of Object.values(category)) {
|
||||
if (!skill.unlocked || skill.currentLevel <= 0) continue;
|
||||
|
||||
for (const [effect, value] of Object.entries(skill.effects || {})) {
|
||||
const total = value * skill.currentLevel;
|
||||
switch (effect) {
|
||||
case 'attack': player.attributes.attack += total; break;
|
||||
case 'defense': player.attributes.defense += total; break;
|
||||
case 'speed': player.attributes.speed += total; break;
|
||||
case 'health':
|
||||
case 'maxHealth': player.attributes.maxHealth += total; break;
|
||||
case 'maxEnergy': player.attributes.maxEnergy += total; break;
|
||||
case 'criticalChance': player.attributes.criticalChance += total; break;
|
||||
case 'criticalDamage': player.attributes.criticalDamage += total; break;
|
||||
default:
|
||||
if (!this.activeBuffs[effect]) this.activeBuffs[effect] = 0;
|
||||
this.activeBuffs[effect] += total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.updateUI();
|
||||
}
|
||||
|
||||
resetToBaseStats() {
|
||||
const player = this.game.systems.player;
|
||||
const lvl = player.stats.level || 1;
|
||||
Object.assign(player.attributes, {
|
||||
attack: 10 + (lvl - 1) * 2,
|
||||
defense: 5 + (lvl - 1) * 1,
|
||||
speed: 10,
|
||||
maxHealth: 100 + (lvl - 1) * 10,
|
||||
maxEnergy: 100 + (lvl - 1) * 5,
|
||||
criticalChance: 0.05,
|
||||
criticalDamage: 1.5
|
||||
});
|
||||
this.activeBuffs = {};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Combat / science / crafting XP helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
awardCombatExperience(amount) {
|
||||
this.addSkillExperience('combat', 'weapons_mastery', amount);
|
||||
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
|
||||
}
|
||||
|
||||
awardScienceExperience(amount) {
|
||||
this.addSkillExperience('science', 'energy_manipulation', amount);
|
||||
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
|
||||
}
|
||||
|
||||
awardCraftingExperience(amount) {
|
||||
this.addSkillExperience('crafting', 'weapons_crafting', amount);
|
||||
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Queries
|
||||
// ------------------------------------------------------------------ //
|
||||
getSkillLevel(category, skillId) {
|
||||
// Support single-arg form used by CraftingSystem: getSkillLevel('crafting')
|
||||
if (skillId === undefined) {
|
||||
let max = 0;
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[category]) max = Math.max(max, cat[category].currentLevel || 0);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return this.skills[category]?.[skillId]?.currentLevel || 0;
|
||||
}
|
||||
|
||||
getSkillExperience(skillId) {
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[skillId]) return cat[skillId].experience || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getExperienceNeeded(skillId) {
|
||||
for (const cat of Object.values(this.skills)) {
|
||||
if (cat[skillId]) return cat[skillId].experienceToNext || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
hasSkill(category, skillId, minimumLevel = 1) {
|
||||
const skill = this.skills[category]?.[skillId];
|
||||
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
|
||||
}
|
||||
|
||||
getSkillBonus(effect) {
|
||||
return this.activeBuffs[effect] || 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// UI
|
||||
// ------------------------------------------------------------------ //
|
||||
updateUI() {
|
||||
this.updateSkillsGrid();
|
||||
this.updateSkillPointsDisplay();
|
||||
}
|
||||
|
||||
updateSkillsGrid() {
|
||||
const grid = document.getElementById('skillsGrid');
|
||||
if (!grid) return;
|
||||
|
||||
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
|
||||
const skills = this.skills[activeCategory] || {};
|
||||
|
||||
if (!this._loaded) {
|
||||
grid.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading skills from server...</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
Object.entries(skills).forEach(([skillId, skill]) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
||||
|
||||
const progressPercent = skill.currentLevel > 0
|
||||
? (skill.experience / skill.experienceToNext) * 100
|
||||
: 0;
|
||||
|
||||
const iconClass = this.game.systems.textureManager
|
||||
? this.game.systems.textureManager.getIcon(skill.icon)
|
||||
: (skill.icon || 'fa-question');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="skill-header">
|
||||
<div class="skill-icon"><i class="fas ${iconClass}"></i></div>
|
||||
<div class="skill-info">
|
||||
<div class="skill-name">${skill.name}</div>
|
||||
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-description">${skill.description}</div>
|
||||
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
|
||||
<div class="skill-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width:${progressPercent}%"></div>
|
||||
</div>
|
||||
<span>${skill.experience}/${skill.experienceToNext} XP</span>
|
||||
</div>
|
||||
` : skill.currentLevel >= skill.maxLevel ? `
|
||||
<div class="skill-max-level"><span>MAX LEVEL</span></div>
|
||||
` : ''}
|
||||
<div class="skill-actions">
|
||||
${!skill.unlocked ? `
|
||||
<button class="btn btn-warning" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.unlockSkill('${activeCategory}','${skillId}')">
|
||||
Unlock (2 Points)
|
||||
</button>
|
||||
` : skill.currentLevel < skill.maxLevel ? `
|
||||
<button class="btn btn-primary" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.upgradeSkill('${activeCategory}','${skillId}')">
|
||||
Upgrade (1 Point)
|
||||
</button>
|
||||
` : `<span class="max-level">MAX LEVEL</span>`}
|
||||
</div>
|
||||
${skill.requiredLevel && !skill.unlocked ? `
|
||||
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
grid.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
updateSkillPointsDisplay() {
|
||||
const player = this.game.systems.player;
|
||||
document.querySelectorAll('.skill-points').forEach(el => {
|
||||
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Save / Load
|
||||
// ------------------------------------------------------------------ //
|
||||
save() {
|
||||
return { skills: this.skills, activeBuffs: this.activeBuffs };
|
||||
}
|
||||
|
||||
load(data) {
|
||||
if (data.skills) {
|
||||
for (const [category, skills] of Object.entries(data.skills)) {
|
||||
if (!this.skills[category]) this.skills[category] = {};
|
||||
for (const [skillId, skillData] of Object.entries(skills)) {
|
||||
if (this.skills[category][skillId]) {
|
||||
Object.assign(this.skills[category][skillId], skillData);
|
||||
} else {
|
||||
// Store progress even before server definitions arrive
|
||||
this.skills[category][skillId] = { ...skillData };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.activeBuffs) this.activeBuffs = data.activeBuffs;
|
||||
this.applySkillEffects();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.activeBuffs = {};
|
||||
for (const category of Object.values(this.skills)) {
|
||||
for (const skill of Object.values(category)) {
|
||||
skill.currentLevel = 0;
|
||||
skill.experience = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() { this.reset(); }
|
||||
}
|
||||
4688
Galaxy-Strike-Online-main/Client/package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
console.log('[PRELOAD] Preload script starting');
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
console.log('[PRELOAD] Electron modules imported successfully');
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Window controls
|
||||
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
||||
closeWindow: () => ipcRenderer.send('close-window'),
|
||||
toggleFullscreen: () => ipcRenderer.send('toggle-fullscreen'),
|
||||
|
||||
// Logging
|
||||
log: (level, message, data) => ipcRenderer.send('log-message', { level, message, data }),
|
||||
|
||||
// Save operations
|
||||
createSaveFolders: (saveSlots) => ipcRenderer.invoke('create-save-folders', saveSlots),
|
||||
testFileAccess: (slotPath) => ipcRenderer.invoke('test-file-access', slotPath),
|
||||
saveGame: (slot, saveData) => ipcRenderer.invoke('save-game', slot, saveData),
|
||||
loadGame: (slot) => ipcRenderer.invoke('load-game', slot),
|
||||
|
||||
// System operations
|
||||
getPath: (name) => ipcRenderer.invoke('get-path', name)
|
||||
});
|
||||
|
||||
console.log('[PRELOAD] electronAPI exposed via contextBridge successfully');
|
||||
} catch (error) {
|
||||
console.error('[PRELOAD] Failed to expose electronAPI:', error);
|
||||
console.error('[PRELOAD] Error stack:', error.stack);
|
||||
}
|
||||
@ -1,216 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Component Styles (mobile-first)
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
padding:.6rem 1.1rem;border:none;border-radius:8px;
|
||||
font-family:'Space Mono',monospace;font-weight:700;cursor:pointer;
|
||||
transition:all .25s;text-transform:uppercase;letter-spacing:1px;
|
||||
position:relative;overflow:hidden;font-size:.82rem;display:inline-flex;align-items:center;gap:.35rem;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);transition:left .5s}
|
||||
.btn:hover::before{left:100%}
|
||||
.btn-primary{background:var(--gradient-primary);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,212,255,.3)}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 5px 18px rgba(0,212,255,.4)}
|
||||
.btn-secondary{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color)}
|
||||
.btn-secondary:hover{border-color:var(--primary-color);background:var(--hover-bg)}
|
||||
.btn-success{background:linear-gradient(135deg,#00ff88,#00cc66);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,255,136,.3)}
|
||||
.btn-warning{background:linear-gradient(135deg,#ffaa00,#ff8800);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,170,0,.3)}
|
||||
.btn-danger,.btn-error{background:linear-gradient(135deg,#ff3366,#ff0033);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,51,102,.3)}
|
||||
.btn-info{background:linear-gradient(135deg,#4fc3f7,#0288d1);color:var(--bg-primary)}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none!important}
|
||||
/* Larger tap targets on touch */
|
||||
@media(pointer:coarse){.btn{min-height:42px;padding:.6rem 1.2rem}}
|
||||
|
||||
/* ── Health / Progress bars ──────────────────────────────────────────── */
|
||||
.health-bar{margin:.75rem 0;padding:.6rem;border-radius:8px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08)}
|
||||
.health-label{font-size:.82rem;font-weight:600;margin-bottom:.4rem;text-transform:uppercase;letter-spacing:1px}
|
||||
.ship-health .health-label{color:#4a9eff}
|
||||
.player-health .health-label{color:#4ade80}
|
||||
.ship-health-fill{background:linear-gradient(90deg,#4a9eff,#00d4ff);border-radius:4px;transition:width .3s}
|
||||
.player-health-fill{background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:4px;transition:width .3s}
|
||||
.health-bar span{display:block;text-align:center;margin-top:.4rem;font-size:.8rem;color:rgba(255,255,255,.75)}
|
||||
.progress-bar{width:100%;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;background:var(--gradient-primary);border-radius:4px;transition:width .3s;position:relative}
|
||||
.progress-fill::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:progressShine 2s infinite}
|
||||
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
|
||||
/* ── Ship stats ──────────────────────────────────────────────────────── */
|
||||
.current-ship-stats{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:.75rem;margin-bottom:.75rem}
|
||||
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:.4rem 0;border-bottom:1px solid rgba(255,255,255,.05)}
|
||||
.stat-row:last-child{border-bottom:none}
|
||||
.stat-row .stat-label{color:#4a9eff;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:.8rem}
|
||||
.stat-row .stat-value{color:#fff;font-weight:700;font-size:.85rem}
|
||||
|
||||
/* ── Modals ──────────────────────────────────────────────────────────── */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:flex-end;justify-content:center;z-index:1000;backdrop-filter:blur(4px)}
|
||||
.modal{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px 16px 0 0;width:100%;max-height:90dvh;overflow-y:auto;box-shadow:0 -10px 40px rgba(0,0,0,.5);animation:modalSlideUp .3s ease}
|
||||
@keyframes modalSlideUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
|
||||
.modal-header{padding:1rem 1.25rem;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center}
|
||||
.modal-header h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;font-size:.95rem}
|
||||
.modal-close{background:transparent;border:none;color:var(--text-secondary);font-size:1.4rem;cursor:pointer;padding:.4rem;transition:all .25s;border-radius:6px;-webkit-tap-highlight-color:transparent}
|
||||
.modal-close:hover{color:var(--error-color);transform:rotate(90deg)}
|
||||
.modal-body{padding:1rem 1.25rem}
|
||||
|
||||
/* Alert modal */
|
||||
.alert-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
|
||||
.alert-modal .alert-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
|
||||
.alert-modal .modal-footer{padding:0 1.25rem 1.25rem;text-align:center}
|
||||
.alert-modal .btn-alert{background:var(--gradient-primary);color:#fff;border:none;padding:.65rem 1.75rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.alert-modal.success .modal-header h3{color:var(--success-color)} .alert-modal.success .btn-alert{background:var(--success-color)}
|
||||
.alert-modal.error .modal-header h3{color:var(--error-color)} .alert-modal.error .btn-alert{background:var(--error-color)}
|
||||
.alert-modal.warning .modal-header h3{color:var(--warning-color)} .alert-modal.warning .btn-alert{background:var(--warning-color)}
|
||||
|
||||
/* Confirmation modal */
|
||||
.confirmation-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
|
||||
.confirmation-modal .confirm-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
|
||||
.confirmation-modal .modal-footer{padding:0 1.25rem 1.25rem;display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
|
||||
.confirmation-modal .btn-confirm{background:var(--error-color);color:#fff;border:none;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.confirmation-modal .btn-cancel{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
|
||||
.confirmation-modal .btn-cancel:hover{background:var(--hover-bg);border-color:var(--primary-color)}
|
||||
|
||||
/* Settings */
|
||||
.settings-menu{max-width:600px;margin:0 auto}
|
||||
.settings-section{margin-bottom:1.5rem;padding:1.1rem;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)}
|
||||
.settings-section h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:.75rem;font-size:1rem}
|
||||
.settings-section h4{color:var(--text-primary);font-family:'Orbitron',sans-serif;font-weight:600;margin-bottom:.4rem;font-size:.9rem}
|
||||
.settings-section p{color:var(--text-secondary);margin-bottom:.75rem;line-height:1.5;font-size:.88rem}
|
||||
.setting-group{margin-bottom:1.25rem}
|
||||
.setting-group label{display:block;color:var(--text-primary);font-weight:600;margin-bottom:.4rem;font-size:.88rem}
|
||||
.setting-select{width:100%;padding:.65rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;color:var(--text-primary);font-size:.88rem;cursor:pointer;transition:all .3s;-webkit-appearance:none;appearance:none}
|
||||
.setting-select:hover{border-color:var(--primary-color)}
|
||||
.setting-select:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(74,158,255,.2)}
|
||||
.setting-actions{display:flex;gap:.75rem;justify-content:flex-end;margin-top:.75rem;flex-wrap:wrap}
|
||||
.reset-options{display:flex;flex-direction:column;gap:.75rem}
|
||||
.reset-option{padding:.85rem;background:var(--bg-secondary);border-radius:8px;border:1px solid var(--border-color)}
|
||||
.reset-option h4{color:var(--warning-color);margin-bottom:.4rem}
|
||||
.reset-option ul{margin:.4rem 0;padding-left:1.25rem;color:var(--text-secondary);font-size:.82rem}
|
||||
.reset-option li{margin-bottom:.2rem}
|
||||
|
||||
/* ── Tooltips (desktop only — use title attr on mobile) ──────────────── */
|
||||
@media(min-width:1024px){
|
||||
.tooltip{position:relative;cursor:help}
|
||||
.tooltip::before{content:attr(data-tooltip);position:absolute;bottom:125%;left:50%;transform:translateX(-50%);background:var(--bg-tertiary);color:var(--text-primary);padding:.4rem .85rem;border-radius:6px;border:1px solid var(--border-color);font-size:.78rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .25s;z-index:1000}
|
||||
.tooltip::after{content:'';position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--border-color);opacity:0;pointer-events:none;transition:opacity .25s}
|
||||
.tooltip:hover::before,.tooltip:hover::after{opacity:1}
|
||||
}
|
||||
|
||||
/* ── Notifications ───────────────────────────────────────────────────── */
|
||||
.notification{
|
||||
position:fixed;top:12px;right:12px;left:12px;
|
||||
padding:.85rem 1.1rem;border-radius:10px;border:1px solid var(--border-color);
|
||||
background:var(--bg-secondary);color:var(--text-primary);
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:2000;
|
||||
animation:notifSlideIn .3s ease;font-size:.88rem;
|
||||
max-width:420px;margin:0 auto;
|
||||
}
|
||||
@keyframes notifSlideIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}
|
||||
.notification.success{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.12),rgba(0,204,102,.08))}
|
||||
.notification.warning{border-color:var(--warning-color);background:linear-gradient(135deg,rgba(255,170,0,.12),rgba(255,136,0,.08))}
|
||||
.notification.error {border-color:var(--error-color); background:linear-gradient(135deg,rgba(255,51,102,.12),rgba(255,0,51,.08))}
|
||||
.notification.info {border-color:var(--primary-color);background:linear-gradient(135deg,rgba(0,212,255,.12),rgba(0,153,204,.08))}
|
||||
@media(min-width:640px){
|
||||
.notification{left:auto;right:16px;top:16px;width:300px;margin:0}
|
||||
@keyframes notifSlideIn{from{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}
|
||||
}
|
||||
|
||||
/* ── Inventory slots & item cards ────────────────────────────────────── */
|
||||
.inventory-slot.starbase-bonus-slot::before{content:'+';position:absolute;top:2px;right:2px;background:var(--primary-color);color:#fff;width:12px;height:12px;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center;font-weight:bold}
|
||||
.item-card{width:100%;height:100%;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.4rem;cursor:pointer;transition:all .25s;position:relative;overflow:hidden;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.item-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--gradient-primary);transform:scaleX(0);transition:transform .3s}
|
||||
.item-card:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.item-card:hover::before{transform:scaleX(1)}
|
||||
.item-card.rare{border-color:#0088ff;box-shadow:0 0 8px rgba(0,136,255,.25)}
|
||||
.item-card.epic{border-color:#8833ff;box-shadow:0 0 10px rgba(136,51,255,.25)}
|
||||
.item-card.legendary{border-color:#ff8800;box-shadow:0 0 14px rgba(255,136,0,.25)}
|
||||
.item-icon{flex:1;display:flex;align-items:center;justify-content:center;margin-bottom:.35rem}
|
||||
.item-icon img,.item-icon i{width:50%;height:50%;min-width:50px;min-height:50px;max-width:120px;max-height:120px;object-fit:contain}
|
||||
.item-info{text-align:center;font-size:clamp(.58rem,1.4vw,.82rem);line-height:1.1}
|
||||
.item-name{font-weight:600;margin-bottom:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
|
||||
.item-rarity{font-size:clamp(.52rem,1.1vw,.7rem);opacity:.8}
|
||||
.item-quantity{position:absolute;top:3px;right:3px;background:var(--primary-color);color:#fff;font-size:clamp(.5rem,1.1vw,.68rem);font-weight:bold;padding:2px 5px;border-radius:5px;min-width:18px;text-align:center}
|
||||
|
||||
/* ── Skill items ─────────────────────────────────────────────────────── */
|
||||
.skill-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;cursor:pointer;transition:all .25s}
|
||||
.skill-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.skill-item.locked{opacity:.5;cursor:not-allowed}
|
||||
.skill-item.locked:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
|
||||
.skill-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem}
|
||||
.skill-name{font-weight:700;color:var(--text-primary);font-size:.9rem}
|
||||
.skill-level{color:var(--primary-color);font-weight:700;font-size:.9rem}
|
||||
.skill-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.skill-progress{margin-top:.4rem}
|
||||
|
||||
/* ── Quest items ─────────────────────────────────────────────────────── */
|
||||
.quest-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.9rem;cursor:pointer;transition:all .25s}
|
||||
.quest-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.quest-item.completed{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.08),rgba(0,204,102,.06))}
|
||||
.quest-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;gap:.3rem}
|
||||
.quest-title{font-weight:700;color:var(--text-primary);font-size:.9rem}
|
||||
.quest-reward{color:var(--warning-color);font-weight:700;font-size:.9rem}
|
||||
.quest-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.quest-progress{margin-top:.4rem}
|
||||
|
||||
/* ── Dungeon items ───────────────────────────────────────────────────── */
|
||||
.dungeon-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
|
||||
.dungeon-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.dungeon-item.selected{border-color:var(--primary-color);background:rgba(0,212,255,.15)}
|
||||
.dungeon-name{font-weight:700;color:var(--text-primary);margin-bottom:.2rem;font-size:.9rem}
|
||||
.dungeon-difficulty{font-size:.78rem;margin-bottom:.2rem}
|
||||
.dungeon-difficulty.easy{color:var(--success-color)} .dungeon-difficulty.medium{color:var(--warning-color)} .dungeon-difficulty.hard{color:var(--error-color)}
|
||||
.dungeon-rewards{font-size:.78rem;color:var(--text-secondary)}
|
||||
|
||||
/* Collapsible dungeon sections */
|
||||
.dungeon-section{margin-bottom:.75rem;border:1px solid var(--border-color);border-radius:8px;overflow:hidden;background:var(--bg-secondary)}
|
||||
.difficulty-header.collapsible{padding:.8rem 1rem;cursor:pointer;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;margin:0;border:none;border-radius:0}
|
||||
.difficulty-header.collapsible:hover{background:var(--bg-tertiary)}
|
||||
.header-content{display:flex;align-items:center;gap:.4rem}
|
||||
.header-content i{font-size:1.1rem}
|
||||
.header-content span{font-weight:600;font-size:1rem}
|
||||
.dungeon-count{background:rgba(255,255,255,.18);padding:.15rem .45rem;border-radius:10px;font-size:.75rem;font-weight:600;min-width:1.8rem;text-align:center}
|
||||
.collapse-indicator{transition:transform .3s;background:rgba(255,255,255,.08);width:1.8rem;height:1.8rem;border-radius:50%;display:flex;align-items:center;justify-content:center}
|
||||
.collapse-indicator i{font-size:.82rem;color:rgba(255,255,255,.85)}
|
||||
.dungeon-content{padding:0 .75rem .75rem;max-height:2000px;overflow:hidden;transition:all .3s;opacity:1}
|
||||
.dungeon-content.collapsed{max-height:0;padding:0 .75rem;opacity:0}
|
||||
.difficulty-header.tutorial{background:linear-gradient(135deg,#1a56db,#2c5aa0);color:#fff}
|
||||
.difficulty-header.easy{background:linear-gradient(135deg,var(--success-color),#27ae60);color:#fff}
|
||||
.difficulty-header.medium{background:linear-gradient(135deg,var(--warning-color),#f39c12);color:#fff}
|
||||
.difficulty-header.hard{background:linear-gradient(135deg,var(--error-color),#e74c3c);color:#fff}
|
||||
.difficulty-header.extreme{background:linear-gradient(135deg,#8e44ad,#9b59b6);color:#fff}
|
||||
.dungeon-content .dungeon-item{margin-bottom:.6rem;border-left:4px solid transparent}
|
||||
.dungeon-content .dungeon-item:hover{border-left-color:var(--primary-color);transform:translateX(3px)}
|
||||
.dungeon-content .dungeon-item:last-child{margin-bottom:0}
|
||||
|
||||
/* ── Shop items (legacy card style) ─────────────────────────────────── */
|
||||
.shop-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .25s}
|
||||
.shop-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
|
||||
.shop-item.purchased{opacity:.5;cursor:not-allowed}
|
||||
.shop-item.purchased:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
|
||||
.shop-name{font-weight:700;color:var(--text-primary);margin-bottom:.4rem;font-size:.9rem}
|
||||
.shop-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
|
||||
.shop-price{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.3rem}
|
||||
.shop-cost{color:var(--warning-color);font-weight:700;font-size:.9rem}
|
||||
|
||||
/* ── Loading states ──────────────────────────────────────────────────── */
|
||||
.loading{position:relative;overflow:hidden}
|
||||
.loading::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,212,255,.18),transparent);animation:loadingShine 1.5s infinite}
|
||||
@keyframes loadingShine{0%{left:-100%}100%{left:100%}}
|
||||
|
||||
/* ── Misc animations ─────────────────────────────────────────────────── */
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
||||
@keyframes rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
|
||||
@keyframes glow{0%,100%{box-shadow:0 0 5px rgba(0,212,255,.5)}50%{box-shadow:0 0 18px rgba(0,212,255,.8)}}
|
||||
.pulse{animation:pulse 2s infinite}
|
||||
.bounce{animation:bounce 2s infinite}
|
||||
.rotate{animation:rotate 2s linear infinite}
|
||||
.glow{animation:glow 2s infinite}
|
||||
|
||||
/* ── Desktop modal — centred (override bottom-sheet) ────────────────── */
|
||||
@media(min-width:640px){
|
||||
.modal-overlay{align-items:center}
|
||||
.modal{border-radius:14px;max-width:580px;width:90%;max-height:80dvh}
|
||||
@keyframes modalSlideUp{from{opacity:0;transform:translateY(-30px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
}
|
||||
@ -1,606 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Main Styles
|
||||
Mobile-first. Scales up to tablet (≥640px) and desktop (≥1024px).
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Reset ───────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--primary-color: #00d4ff;
|
||||
--secondary-color: #ff6b35;
|
||||
--accent-color: #ff00ff;
|
||||
--bg-primary: #0a0e1a;
|
||||
--bg-secondary: #151923;
|
||||
--bg-tertiary: #1e2433;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b8c5d6;
|
||||
--text-muted: #6b7c93;
|
||||
--border-color: #2a3241;
|
||||
--success-color: #00ff88;
|
||||
--warning-color: #ffaa00;
|
||||
--error-color: #ff3366;
|
||||
--card-bg: rgba(30,36,51,0.8);
|
||||
--hover-bg: rgba(0,212,255,0.1);
|
||||
--gradient-primary: linear-gradient(135deg,#00d4ff,#0099cc);
|
||||
--gradient-secondary: linear-gradient(135deg,#ff6b35,#ff4500);
|
||||
/* Layout tokens */
|
||||
--header-h: 52px;
|
||||
--nav-h: 56px;
|
||||
--pg: 0.75rem;
|
||||
--r: 10px;
|
||||
}
|
||||
|
||||
html { font-size: 14px; -webkit-text-size-adjust: 100%; }
|
||||
body {
|
||||
font-family: 'Space Mono', monospace;
|
||||
background: var(--bg-primary); color: var(--text-primary);
|
||||
overflow: hidden; height: 100dvh;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%,rgba(0,212,255,.08) 0%,transparent 50%),
|
||||
radial-gradient(circle at 80% 80%,rgba(255,107,53,.08) 0%,transparent 50%),
|
||||
radial-gradient(circle at 40% 20%,rgba(255,0,255,.04) 0%,transparent 50%);
|
||||
}
|
||||
h1,h2,h3,.logo,.section-title,.menu-title { font-family:'Orbitron',sans-serif; }
|
||||
.hidden { display:none!important; }
|
||||
.text-center{text-align:center} .w-full{width:100%}
|
||||
.flex{display:flex} .flex-column{flex-direction:column}
|
||||
.flex-center{align-items:center;justify-content:center}
|
||||
.flex-between{justify-content:space-between} .flex-wrap{flex-wrap:wrap}
|
||||
.mt-1{margin-top:.5rem} .mt-2{margin-top:1rem}
|
||||
.mb-1{margin-bottom:.5rem} .mb-2{margin-bottom:1rem}
|
||||
.p-1{padding:.5rem} .p-2{padding:1rem}
|
||||
|
||||
/* ── Scrollbars ──────────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar{width:5px;height:5px}
|
||||
::-webkit-scrollbar-track{background:var(--bg-tertiary);border-radius:3px}
|
||||
::-webkit-scrollbar-thumb{background:rgba(0,212,255,.35);border-radius:3px}
|
||||
::-webkit-scrollbar-thumb:hover{background:rgba(0,212,255,.6)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
LOADING SCREEN
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.loading-screen {
|
||||
position:fixed;inset:0;background:var(--bg-primary);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
z-index:9999;transition:opacity .5s ease;
|
||||
}
|
||||
.loading-content{text-align:center;width:90%;max-width:340px;padding:2rem}
|
||||
.game-title {
|
||||
font-family:'Orbitron',sans-serif;
|
||||
font-size:clamp(1.6rem,8vw,3rem);font-weight:900;
|
||||
background:var(--gradient-primary);
|
||||
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
|
||||
margin-bottom:2rem;text-transform:uppercase;letter-spacing:3px;
|
||||
}
|
||||
.loading-bar{width:100%;height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden;margin-bottom:1rem}
|
||||
.loading-progress{height:100%;background:var(--gradient-primary);width:0%;transition:width .3s ease;animation:loading-pulse 2s infinite}
|
||||
@keyframes loading-pulse{0%,100%{opacity:1}50%{opacity:.7}}
|
||||
.loading-text{color:var(--text-secondary);font-size:.8rem}
|
||||
.loading-indicator {
|
||||
position:fixed;top:0;left:0;width:100%;height:3px;
|
||||
background:linear-gradient(90deg,var(--primary-color) 0%,rgba(0,212,255,.3) 50%,var(--primary-color) 100%);
|
||||
background-size:200% 100%;animation:loading-gradient 2s ease-in-out infinite;
|
||||
z-index:10000;transition:opacity .3s ease;
|
||||
}
|
||||
.loading-indicator.hidden{opacity:0;pointer-events:none}
|
||||
.loading-indicator.complete{background:linear-gradient(90deg,#4CAF50,#45a049);animation:none}
|
||||
.loading-indicator.error{background:linear-gradient(90deg,#f44336,#d32f2f);animation:none}
|
||||
@keyframes loading-gradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
||||
.loading-status {
|
||||
position:fixed;top:8px;left:50%;transform:translateX(-50%);
|
||||
background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;
|
||||
padding:6px 14px;color:var(--text-primary);font-size:.82rem;z-index:10001;
|
||||
transition:all .3s ease;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
||||
}
|
||||
.loading-status.error{background:linear-gradient(90deg,#f44336,#d32f2f);color:#fff;border-color:#d32f2f}
|
||||
.loading-status.hidden{opacity:0;transform:translateX(-50%) translateY(-20px)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ELECTRON TITLE BAR
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.title-bar {
|
||||
position:fixed;top:0;left:0;right:0;height:32px;
|
||||
background:var(--bg-primary);border-bottom:1px solid var(--border-color);
|
||||
display:none;justify-content:space-between;align-items:center;padding:0 8px;
|
||||
z-index:10000;-webkit-app-region:drag;user-select:none;
|
||||
}
|
||||
body.electron-app .title-bar{display:flex}
|
||||
body.electron-app #app{margin-top:32px}
|
||||
.title-bar-title{font-size:13px;font-weight:600;color:var(--text-primary);font-family:'Orbitron',monospace}
|
||||
.title-bar-right{display:flex;gap:4px}
|
||||
.title-bar-btn{
|
||||
width:24px;height:24px;border:none;background:transparent;
|
||||
color:var(--text-primary);border-radius:4px;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:12px;-webkit-app-region:no-drag;transition:background .2s;
|
||||
}
|
||||
.title-bar-btn:hover{background:var(--bg-secondary)}
|
||||
.title-bar-btn.close-btn:hover{background:#e74c3c;color:#fff}
|
||||
body.fullscreen .title-bar{display:none}
|
||||
body.fullscreen #app{margin-top:0}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
MAIN MENU
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.main-menu {
|
||||
position:fixed;inset:0;background:var(--bg-primary);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow-y:auto;padding:1rem 0;
|
||||
background-image:radial-gradient(circle at 20% 50%,rgba(0,212,255,.1) 0%,transparent 50%),
|
||||
radial-gradient(circle at 80% 80%,rgba(255,107,53,.1) 0%,transparent 50%);
|
||||
}
|
||||
.menu-container {
|
||||
width:95%;max-width:800px;background:var(--card-bg);
|
||||
border-radius:16px;border:1px solid var(--border-color);
|
||||
box-shadow:0 20px 60px rgba(0,0,0,.5);backdrop-filter:blur(10px);overflow:hidden;margin:auto;
|
||||
}
|
||||
.menu-header{text-align:center;padding:clamp(1.5rem,5vw,2.5rem) 1rem 1.2rem;background:var(--gradient-primary)}
|
||||
.menu-title{font-size:clamp(1.4rem,6vw,3rem);font-weight:900;color:var(--text-primary);text-transform:uppercase;letter-spacing:3px;margin-bottom:.5rem;text-shadow:0 0 20px rgba(0,212,255,.5)}
|
||||
.menu-subtitle{font-size:clamp(.85rem,2.5vw,1.2rem);color:var(--text-secondary)}
|
||||
.menu-content{padding:clamp(1rem,4vw,2rem)}
|
||||
.menu-section{animation:fadeInUp .5s ease-out}
|
||||
.section-title{font-size:clamp(1.1rem,4vw,1.8rem);color:var(--primary-color);text-align:center;margin-bottom:1.5rem;text-transform:uppercase;letter-spacing:2px}
|
||||
.login-options{display:flex;flex-direction:column;gap:1rem;margin-bottom:1rem}
|
||||
.btn-large{
|
||||
padding:clamp(.9rem,3vw,1.25rem) clamp(1rem,4vw,2.5rem);
|
||||
font-size:clamp(.9rem,2.5vw,1.2rem);font-weight:600;border-radius:10px;
|
||||
transition:all .3s;width:100%;display:flex;align-items:center;justify-content:center;gap:.5rem;
|
||||
}
|
||||
.login-notice{text-align:center;padding:.75rem;background:rgba(0,212,255,.1);border:1px solid var(--primary-color);border-radius:8px;color:var(--text-secondary);font-size:.85rem}
|
||||
.login-notice i{color:var(--primary-color);margin-right:.4rem}
|
||||
|
||||
/* Server browser */
|
||||
.server-controls{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;gap:.75rem;flex-wrap:wrap}
|
||||
.server-filters{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.filter-select{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.5rem .75rem;color:var(--text-primary);font-family:'Space Mono',monospace;font-size:.85rem;cursor:pointer}
|
||||
.filter-select:focus{outline:none;border-color:var(--primary-color)}
|
||||
.server-list{max-height:min(55vh,360px);overflow-y:auto;border:1px solid var(--border-color);border-radius:10px;background:var(--bg-tertiary);margin-bottom:1rem}
|
||||
.server-item{display:flex;justify-content:space-between;align-items:center;padding:.85rem 1rem;border-bottom:1px solid var(--border-color);cursor:pointer;transition:background .2s,border-left .2s}
|
||||
.server-item:last-child{border-bottom:none}
|
||||
.server-item:hover{background:var(--hover-bg);border-left:4px solid var(--primary-color)}
|
||||
.server-name{font-size:1rem;font-weight:600;margin-bottom:3px}
|
||||
.server-details{font-size:.8rem;color:var(--text-secondary);display:flex;gap:.75rem;flex-wrap:wrap}
|
||||
.server-actions-right{display:flex;align-items:center;gap:.5rem;flex-shrink:0}
|
||||
.server-player-count,.server-region{background:var(--bg-secondary);padding:3px 8px;border-radius:4px;font-size:.75rem;color:var(--text-secondary);border:1px solid var(--border-color)}
|
||||
.server-type{padding:3px 8px;border-radius:4px;font-size:.75rem;font-weight:600;text-transform:uppercase}
|
||||
.server-type.public{background:rgba(0,255,136,.15);color:var(--success-color);border:1px solid var(--success-color)}
|
||||
.server-type.private{background:rgba(255,170,0,.15);color:var(--warning-color);border:1px solid var(--warning-color)}
|
||||
.server-loading,.server-empty{text-align:center;padding:3rem 1rem;color:var(--text-muted)}
|
||||
.server-loading i,.server-empty i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5;display:block}
|
||||
.server-loading i{animation:pulse 1.5s infinite}
|
||||
|
||||
/* Server/Save confirmation — mobile: stacked */
|
||||
.server-confirmation,.save-confirmation,.options-grid{display:flex;flex-direction:column;gap:1rem;margin-bottom:1.5rem}
|
||||
.server-preview,.save-preview,.save-info-display{background:rgba(0,212,255,.08);border:2px solid rgba(0,212,255,.3);border-radius:10px;padding:1rem;font-family:'Space Mono',monospace}
|
||||
.server-preview h3,.save-preview h3,.save-info-display h3{color:var(--primary-color);margin-bottom:.75rem;text-align:center;font-size:1rem}
|
||||
.server-details{color:var(--text-secondary);font-size:.9rem;line-height:1.6}
|
||||
.server-info{margin:6px 0;display:flex;justify-content:space-between;align-items:center}
|
||||
.server-info span{font-weight:600;color:var(--text-primary)}
|
||||
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{display:flex;flex-direction:row;gap:.75rem;flex-wrap:wrap}
|
||||
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{position:static;width:auto;flex:1;min-width:130px}
|
||||
.confirm-navigation,.options-actions,.save-actions{display:flex;justify-content:center;margin-top:1rem}
|
||||
.btn-join-server{background:linear-gradient(135deg,#00ff88,#00cc66)!important;color:#000!important;border:3px solid #00ff88!important;box-shadow:0 6px 20px rgba(0,255,136,.4)!important;font-weight:700!important}
|
||||
.btn-join-server:hover{background:linear-gradient(135deg,#00ffaa,#00ff88)!important;transform:scale(1.04) translateY(-2px);box-shadow:0 8px 25px rgba(0,255,136,.5)!important}
|
||||
.selected-server-info-center,.selected-save-info-center,.options-center{display:flex;flex-direction:column;justify-content:center;align-items:center;flex:1}
|
||||
.save-details{color:#fff;font-size:.9em;line-height:1.6;white-space:pre-wrap}
|
||||
.save-info{margin:3px 0;font-family:'Space Mono',monospace}
|
||||
#saveInfoDetails{color:#fff;font-size:.9em;line-height:1.4}
|
||||
.save-info-content{background:rgba(0,100,200,.2);border-radius:8px;padding:.75rem;margin:0}
|
||||
|
||||
/* Save slots */
|
||||
.save-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||||
.save-slot{background:var(--bg-secondary);border:2px solid var(--border-color);border-radius:10px;padding:1rem;cursor:pointer;transition:all .3s}
|
||||
.save-slot:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 8px 25px rgba(0,212,255,.2)}
|
||||
.slot-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}
|
||||
.slot-number{font-family:'Orbitron',sans-serif;font-weight:700}
|
||||
.slot-status{padding:3px 10px;border-radius:20px;font-size:.75rem;font-weight:600;text-transform:uppercase}
|
||||
.slot-status.empty{background:var(--bg-tertiary);color:var(--text-muted)}
|
||||
.slot-status.has-data{background:var(--success-color);color:var(--bg-primary)}
|
||||
.slot-name{font-weight:700;color:var(--text-primary);margin-bottom:4px}
|
||||
.slot-details{color:var(--text-muted);font-size:.9rem}
|
||||
.slot-btn{width:100%;padding:.55rem;background:var(--gradient-primary);border:none;border-radius:6px;color:var(--text-primary);font-weight:600;cursor:pointer;transition:all .3s}
|
||||
.slot-btn:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(0,212,255,.3)}
|
||||
|
||||
/* Menu footer */
|
||||
.menu-footer{padding:.9rem 1.5rem;background:var(--bg-secondary);border-top:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||
.version-text{color:var(--text-muted);font-size:.8rem}
|
||||
.footer-links{display:flex;gap:1rem}
|
||||
.link-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:.85rem;transition:color .3s}
|
||||
.link-btn:hover{color:var(--primary-color)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
GAME INTERFACE
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
.game-interface{width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
|
||||
|
||||
/* Header */
|
||||
.game-header{
|
||||
height:var(--header-h);background:var(--bg-secondary);
|
||||
border-bottom:1px solid var(--border-color);
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:0 .75rem;backdrop-filter:blur(10px);flex-shrink:0;
|
||||
position:sticky;top:0;z-index:500;
|
||||
}
|
||||
.header-left{display:flex;align-items:center;gap:.6rem;min-width:0}
|
||||
.logo{font-size:clamp(1rem,4vw,1.5rem);font-weight:900;color:var(--primary-color);text-shadow:0 0 10px rgba(0,212,255,.5);white-space:nowrap}
|
||||
.player-info{display:flex;flex-direction:column;gap:.1rem;min-width:0}
|
||||
.player-info>div{display:flex;align-items:center;gap:.25rem;min-width:0}
|
||||
.player-name{font-weight:700;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:18ch}
|
||||
.player-title{font-size:.75rem;color:var(--text-secondary);display:none}
|
||||
.player-username{font-weight:600;color:var(--accent-color);font-size:.78rem}
|
||||
.player-level{font-size:.75rem;color:var(--primary-color);white-space:nowrap}
|
||||
|
||||
.header-center{flex:1;display:flex;justify-content:center;overflow:hidden;min-width:0;padding:0 .4rem}
|
||||
.resources{display:flex;gap:.35rem;align-items:center;overflow-x:auto;flex-wrap:nowrap;scrollbar-width:none;-ms-overflow-style:none;padding:.15rem 0}
|
||||
.resources::-webkit-scrollbar{display:none}
|
||||
.resource{display:flex;align-items:center;gap:.3rem;padding:.28rem .55rem;background:var(--bg-tertiary);border-radius:20px;border:1px solid var(--border-color);white-space:nowrap;font-size:.75rem;transition:border-color .2s;flex-shrink:0}
|
||||
.resource:hover{border-color:var(--primary-color)}
|
||||
.resource i{color:var(--primary-color);font-size:.78rem}
|
||||
|
||||
.header-right{display:flex;gap:.3rem;flex-shrink:0}
|
||||
.header-right .btn{padding:.4rem .55rem;font-size:.8rem;border-radius:7px}
|
||||
|
||||
/* ── TOP NAV — desktop only ──────────────────────────────────────────── */
|
||||
.main-nav{
|
||||
height:46px;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color);
|
||||
display:none;align-items:center;padding:0 .75rem;gap:.3rem;
|
||||
overflow-x:auto;position:sticky;top:var(--header-h);z-index:490;
|
||||
scrollbar-width:none;
|
||||
}
|
||||
.main-nav::-webkit-scrollbar{display:none}
|
||||
.nav-btn{
|
||||
display:flex;align-items:center;gap:.35rem;padding:.38rem .7rem;
|
||||
background:transparent;border:1px solid transparent;border-radius:7px;
|
||||
color:var(--text-secondary);cursor:pointer;transition:all .25s;
|
||||
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;
|
||||
}
|
||||
.nav-btn:hover{background:var(--hover-bg);color:var(--text-primary);border-color:var(--primary-color)}
|
||||
.nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent;font-weight:700}
|
||||
.nav-btn i{font-size:.85rem}
|
||||
|
||||
/* ── BOTTOM NAV — mobile primary navigation ──────────────────────────── */
|
||||
.bottom-nav{
|
||||
position:fixed;bottom:0;left:0;right:0;
|
||||
height:var(--nav-h);
|
||||
background:var(--bg-secondary);border-top:1px solid var(--border-color);
|
||||
display:flex;align-items:stretch;z-index:600;
|
||||
overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;
|
||||
overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px);
|
||||
}
|
||||
.bottom-nav::-webkit-scrollbar{display:none}
|
||||
.bottom-nav-btn{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:2px;padding:.28rem .4rem;background:transparent;border:none;
|
||||
color:var(--text-muted);cursor:pointer;transition:color .2s,background .2s;
|
||||
font-family:'Space Mono',monospace;font-size:.52rem;line-height:1.1;
|
||||
flex:1;min-width:48px;max-width:68px;white-space:nowrap;overflow:hidden;
|
||||
border-top:2px solid transparent;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.bottom-nav-btn i{font-size:1rem;display:block}
|
||||
.bottom-nav-btn.active{color:var(--primary-color);border-top-color:var(--primary-color);background:rgba(0,212,255,.06)}
|
||||
.bottom-nav-more{
|
||||
background:transparent;border:none;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:2px;padding:.28rem .4rem;min-width:48px;max-width:68px;flex-shrink:0;
|
||||
color:var(--text-muted);cursor:pointer;font-size:.52rem;border-top:2px solid transparent;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;font-family:'Space Mono',monospace;
|
||||
}
|
||||
.bottom-nav-more i{font-size:1rem;display:block}
|
||||
.bottom-nav-more:hover,.bottom-nav-more.open{color:var(--primary-color)}
|
||||
|
||||
/* ── NAV DRAWER ──────────────────────────────────────────────────────── */
|
||||
.nav-drawer{
|
||||
position:fixed;bottom:var(--nav-h);left:0;right:0;
|
||||
background:var(--bg-secondary);border-top:1px solid var(--border-color);
|
||||
border-radius:16px 16px 0 0;z-index:590;
|
||||
transform:translateY(100%);transition:transform .3s cubic-bezier(.4,0,.2,1);
|
||||
max-height:58dvh;overflow-y:auto;padding:.75rem 0;
|
||||
box-shadow:0 -8px 32px rgba(0,0,0,.4);
|
||||
}
|
||||
.nav-drawer.open{transform:translateY(0)}
|
||||
.nav-drawer-handle{width:36px;height:4px;background:var(--border-color);border-radius:2px;margin:0 auto .75rem;cursor:pointer}
|
||||
.nav-drawer-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.25rem;padding:0 .75rem .5rem}
|
||||
.nav-drawer-btn{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:4px;padding:.6rem .4rem;
|
||||
background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:10px;
|
||||
color:var(--text-secondary);cursor:pointer;font-size:.68rem;line-height:1.2;
|
||||
transition:all .2s;text-align:center;font-family:'Space Mono',monospace;
|
||||
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
|
||||
}
|
||||
.nav-drawer-btn i{font-size:1.1rem;color:var(--text-muted)}
|
||||
.nav-drawer-btn:hover,.nav-drawer-btn.active{background:var(--hover-bg);border-color:var(--primary-color);color:var(--primary-color)}
|
||||
.nav-drawer-btn.active i{color:var(--primary-color)}
|
||||
.nav-drawer-overlay{position:fixed;inset:0;z-index:580;background:rgba(0,0,0,.45);display:none}
|
||||
.nav-drawer-overlay.open{display:block}
|
||||
|
||||
/* ── MAIN CONTENT ────────────────────────────────────────────────────── */
|
||||
.main-content{
|
||||
flex:1;overflow-y:auto;overflow-x:hidden;
|
||||
padding:var(--pg);background:var(--bg-primary);
|
||||
padding-bottom:calc(var(--nav-h) + var(--pg) + env(safe-area-inset-bottom,0px));
|
||||
-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;
|
||||
}
|
||||
.tab-content{display:none;animation:fadeIn .2s ease}
|
||||
.tab-content.active{display:block}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes fadeInUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* ── DASHBOARD ───────────────────────────────────────────────────────── */
|
||||
.dashboard-grid{display:grid;grid-template-columns:1fr;gap:.75rem;max-width:1200px;margin:0 auto}
|
||||
.card{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;backdrop-filter:blur(10px);transition:border-color .3s,box-shadow .3s}
|
||||
.card:hover{border-color:var(--primary-color);box-shadow:0 0 18px rgba(0,212,255,.15)}
|
||||
.card h3{color:var(--primary-color);margin-bottom:.75rem;font-family:'Orbitron',sans-serif;font-size:.88rem;font-weight:700;text-transform:uppercase;letter-spacing:1px}
|
||||
.fleet-info,.idle-stats,.quick-actions{display:flex;flex-direction:column;gap:.6rem}
|
||||
.ship-status{display:flex;align-items:center;gap:.75rem}
|
||||
.ship-status i{font-size:1.6rem;color:var(--secondary-color)}
|
||||
.player-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:.35rem}
|
||||
.stat{display:flex;justify-content:space-between;padding:.35rem 0;border-bottom:1px solid var(--border-color);font-size:.8rem}
|
||||
.stat:last-child{border-bottom:none}
|
||||
.stat-label{color:var(--text-secondary)} .stat-value{color:var(--primary-color);font-weight:700}
|
||||
|
||||
/* ── DUNGEONS ────────────────────────────────────────────────────────── */
|
||||
.dungeons-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.dungeon-selector{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
|
||||
.dungeon-list{display:flex;flex-direction:column;gap:.4rem}
|
||||
.dungeon-view{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;min-height:180px;display:flex;align-items:center;justify-content:center}
|
||||
.dungeon-placeholder{text-align:center;color:var(--text-muted)}
|
||||
.dungeon-placeholder i{font-size:2.8rem;margin-bottom:.75rem;opacity:.5;display:block}
|
||||
|
||||
/* ── SKILLS ──────────────────────────────────────────────────────────── */
|
||||
.skills-container{max-width:1200px;margin:0 auto}
|
||||
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{
|
||||
display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap;overflow-x:auto;
|
||||
scrollbar-width:none;padding-bottom:.2rem;
|
||||
}
|
||||
.skill-categories::-webkit-scrollbar,.quest-tabs::-webkit-scrollbar,.crafting-categories::-webkit-scrollbar,.shop-categories::-webkit-scrollbar{display:none}
|
||||
.skill-cat-btn,.quest-tab-btn,.crafting-cat-btn,.shop-cat-btn{
|
||||
padding:.4rem .85rem;background:var(--bg-tertiary);border:1px solid var(--border-color);
|
||||
border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;
|
||||
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;flex-shrink:0;
|
||||
}
|
||||
.skill-cat-btn:hover,.quest-tab-btn:hover,.crafting-cat-btn:hover,.shop-cat-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
|
||||
.skill-cat-btn.active,.quest-tab-btn.active,.crafting-cat-btn.active,.shop-cat-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
|
||||
.skills-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.skills-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
|
||||
.skills-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
|
||||
.skill-points-display{background:var(--bg-tertiary);padding:.4rem .75rem;border-radius:7px;border:1px solid var(--border-color)}
|
||||
.skill-points{color:var(--warning-color);font-weight:600;font-size:.95rem}
|
||||
|
||||
/* ── BASE ────────────────────────────────────────────────────────────── */
|
||||
.base-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.base-view,.base-upgrades{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
|
||||
.base-navigation{display:flex;gap:.35rem;margin-bottom:1rem;flex-wrap:wrap}
|
||||
.base-nav-btn{padding:.4rem .8rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;font-size:.78rem;white-space:nowrap;font-family:'Space Mono',monospace}
|
||||
.base-nav-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
|
||||
.base-nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
|
||||
.base-rooms{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:.5rem}
|
||||
.room-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.55rem .4rem;cursor:pointer;transition:all .25s;text-align:center;min-height:70px;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.room-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
|
||||
.room-item i{font-size:1.2rem;margin-bottom:.3rem;color:var(--primary-color)}
|
||||
.room-item h4{margin:0;color:var(--text-primary);font-size:.7rem;font-weight:600}
|
||||
.room-item p{margin:.15rem 0 0;color:var(--text-secondary);font-size:.62rem}
|
||||
|
||||
.starbases-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.starbase-section{display:flex;flex-direction:column}
|
||||
.starbase-section h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
.starbase-list,.starbase-shop{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:45dvh;overflow-y:auto}
|
||||
.starbase-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:.5rem;cursor:pointer;transition:all .25s;max-height:140px;overflow-y:auto}
|
||||
.starbase-item:hover{border-color:var(--primary-color)}
|
||||
.starbase-item h4{margin:0 0 .35rem;color:var(--text-primary);font-size:.85rem}
|
||||
.starbase-item p{margin:0;color:var(--text-secondary);font-size:.78rem}
|
||||
.starbase-item .level{color:var(--primary-color);font-weight:600}
|
||||
.starbase-item .description{margin-top:.25rem;line-height:1.3;max-height:55px;overflow-y:auto;padding-right:.4rem}
|
||||
.starbase-purchase-list{overflow-y:auto;max-height:40dvh}
|
||||
|
||||
.base-visualization-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
#baseCanvas{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);width:100%;height:min(260px,38dvh)}
|
||||
.base-info-overlay{display:flex;flex-direction:column;gap:.5rem}
|
||||
.base-stats-overlay{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:220px;overflow-y:auto}
|
||||
.base-stats-overlay h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
#baseInfoDisplay{color:var(--text-secondary);font-size:.88rem;line-height:1.4}
|
||||
|
||||
.upgrade-list{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;max-height:380px;overflow-y:auto;padding:.3rem}
|
||||
.upgrade-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
|
||||
.upgrade-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
|
||||
.upgrade-item h4{margin:0 0 .3rem;color:var(--text-primary);font-size:.82rem}
|
||||
.upgrade-item p{margin:0;color:var(--text-secondary);font-size:.75rem}
|
||||
.upgrade-item .cost{margin-top:.3rem;color:var(--primary-color);font-weight:600;font-size:.8rem}
|
||||
|
||||
/* ── QUESTS ──────────────────────────────────────────────────────────── */
|
||||
.quests-container{max-width:1200px;margin:0 auto}
|
||||
.quest-list{display:flex;flex-direction:column;gap:.75rem}
|
||||
.quest-difficulty{display:flex;gap:2px;font-size:.88rem;color:#ffd700;margin-right:.75rem}
|
||||
.difficulty-1{color:#4ade80} .difficulty-2{color:#60a5fa} .difficulty-3{color:#f59e0b} .difficulty-4{color:#ef4444}
|
||||
.quest-header-info{display:flex;align-items:center;gap:.75rem}
|
||||
.all-objectives-completed{color:#4ade80;font-weight:600;padding:.4rem;background:rgba(74,222,128,.1);border-radius:4px;text-align:center;font-size:.85rem}
|
||||
.completion-time{font-size:.78rem;color:var(--text-secondary);margin-top:.4rem;text-align:right}
|
||||
.daily-countdown,.weekly-countdown{margin-bottom:1rem;padding:.65rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px}
|
||||
.countdown-container{display:flex;align-items:center;gap:.4rem;justify-content:center;color:var(--text-primary);font-size:.85rem}
|
||||
.countdown-container i{color:var(--primary-color)}
|
||||
|
||||
/* ── INVENTORY ───────────────────────────────────────────────────────── */
|
||||
.inventory-container{display:flex;flex-direction:column;gap:.75rem}
|
||||
.inventory-grid{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;overflow-y:auto;max-height:50dvh}
|
||||
.item-details{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;min-height:110px}
|
||||
.inventory-main{display:flex;gap:1rem;flex:1;min-height:0;flex-direction:column}
|
||||
.inventory-section{flex:1;min-height:0;display:flex;flex-direction:column}
|
||||
.inventory-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
#inventoryGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:.5rem;padding:.35rem}
|
||||
.inventory-slot{width:100%;aspect-ratio:1;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;position:relative}
|
||||
.inventory-slot.starbase-bonus-slot{border:2px solid var(--primary-color);background:rgba(0,212,255,.08)}
|
||||
|
||||
/* Equipment */
|
||||
.equipment-section{margin-bottom:1.5rem;padding:.75rem;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)}
|
||||
.equipment-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
|
||||
.equipment-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:.75rem}
|
||||
.equipment-slot{display:flex;flex-direction:column;align-items:center;text-align:center}
|
||||
.slot-label{font-size:.82rem;color:var(--text-secondary);margin-bottom:.4rem;font-weight:500}
|
||||
.slot-container{width:70px;height:70px;border:2px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary);transition:all .3s;cursor:pointer}
|
||||
.slot-container:hover{border-color:var(--primary-color);box-shadow:0 0 10px rgba(0,123,255,.3)}
|
||||
|
||||
/* ── SHOP ────────────────────────────────────────────────────────────── */
|
||||
.shop-container{max-width:1200px;margin:0 auto}
|
||||
.shop-items{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.shop-item.legacy{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .3s}
|
||||
.shop-item.legacy:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 5px 15px rgba(0,212,255,.2)}
|
||||
.shop-item-content{display:flex!important;align-items:flex-start!important;gap:.75rem!important}
|
||||
.shop-item-image{flex-shrink:0!important;width:64px!important;height:64px!important;border-radius:6px!important;overflow:hidden!important;background:rgba(0,0,0,.3)!important;display:flex!important;align-items:center!important;justify-content:center!important}
|
||||
.shop-item-name{font-size:1rem!important;font-weight:600!important;color:#00d4ff!important;margin-bottom:4px!important}
|
||||
.shop-item-description{color:#fff!important;font-size:.85rem!important;margin-bottom:.5rem!important;line-height:1.4!important}
|
||||
.shop-item-stats{display:flex!important;flex-wrap:wrap!important;gap:.4rem!important;margin-bottom:.5rem!important}
|
||||
.shop-item-price{font-size:.95rem!important;font-weight:600!important;color:#ffd700!important;margin-bottom:.25rem!important}
|
||||
.shop-item-rarity{display:inline-block!important;padding:2px 8px!important;border-radius:4px!important;font-size:.75rem!important;font-weight:600!important;text-transform:uppercase!important;margin-bottom:.5rem!important}
|
||||
.shop-item-rarity.common{background:rgba(128,128,128,.2)!important;color:#808080!important;border:1px solid rgba(128,128,128,.4)!important}
|
||||
.shop-item-rarity.uncommon{background:rgba(0,255,0,.2)!important;color:#00ff00!important;border:1px solid rgba(0,255,0,.4)!important}
|
||||
.shop-item-rarity.rare{background:rgba(0,100,255,.2)!important;color:#0064ff!important;border:1px solid rgba(0,100,255,.4)!important}
|
||||
.shop-item-rarity.epic{background:rgba(128,0,255,.2)!important;color:#8000ff!important;border:1px solid rgba(128,0,255,.4)!important}
|
||||
.shop-item-rarity.legendary{background:rgba(255,128,0,.2)!important;color:#ff8000!important;border:1px solid rgba(255,128,0,.4)!important}
|
||||
.shop-item-purchase-btn{width:100%!important;padding:.5rem 1rem!important;background:var(--gradient-primary)!important;color:#fff!important;border:none!important;border-radius:4px!important;cursor:pointer!important;font-weight:600!important;transition:all .3s!important;margin-top:.5rem!important}
|
||||
.shop-item-purchase-btn:hover:not(.disabled){background:linear-gradient(135deg,#00ffcc,#00ccaa)!important;transform:translateY(-1px)!important}
|
||||
.shop-item-purchase-btn.disabled{background:rgba(100,100,100,.3)!important;color:#666!important;cursor:not-allowed!important}
|
||||
.shop-refresh-info{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||
.refresh-info-left{display:flex;flex-direction:column;gap:.35rem}
|
||||
.refresh-countdown,.purchase-limit-info{display:flex;align-items:center;gap:.4rem;color:var(--text-secondary);font-size:.82rem}
|
||||
.refresh-countdown i{color:var(--primary-color)}
|
||||
.purchase-limit-info i{color:#ffd700}
|
||||
.refresh-shop-btn{background:var(--gradient-primary);color:#fff;border:none;border-radius:6px;padding:.4rem .85rem;font-size:.82rem;cursor:pointer;transition:all .3s;display:flex;align-items:center;gap:.4rem;white-space:nowrap}
|
||||
.refresh-shop-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,212,255,.3)}
|
||||
|
||||
/* ── CRAFTING ────────────────────────────────────────────────────────── */
|
||||
.crafting-container{max-width:1200px;margin:0 auto}
|
||||
.crafting-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
|
||||
.crafting-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
|
||||
.crafting-info{display:flex;gap:.75rem;align-items:center;flex-wrap:wrap}
|
||||
.crafting-level,.crafting-experience{background:var(--bg-tertiary);padding:.38rem .7rem;border-radius:7px;border:1px solid var(--border-color);display:flex;align-items:center;gap:.4rem;color:var(--text-primary);font-size:.78rem}
|
||||
.crafting-level i,.crafting-experience i{color:var(--primary-color)}
|
||||
.crafting-content{display:flex;flex-direction:column;gap:.75rem}
|
||||
.crafting-sidebar{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
|
||||
.crafting-categories h3{color:var(--primary-color);font-size:1rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
|
||||
.crafting-main{display:flex;flex-direction:column;gap:.75rem}
|
||||
.recipe-list{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem;overflow-y:auto;max-height:45dvh}
|
||||
.crafting-details{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
|
||||
.crafting-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
|
||||
.selected-recipe{display:flex;flex-direction:column;align-items:center;text-align:center;color:var(--text-muted)}
|
||||
.selected-recipe i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5}
|
||||
.selected-recipe h3{margin-bottom:.5rem;color:var(--text-secondary)}
|
||||
|
||||
/* Recipe items */
|
||||
.recipe-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.85rem;margin-bottom:.75rem;cursor:pointer;transition:all .3s;position:relative;overflow:hidden}
|
||||
.recipe-item:hover{background:var(--hover-bg);border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.2)}
|
||||
.recipe-item.selected{background:var(--gradient-primary);border-color:var(--primary-color);box-shadow:0 4px 12px rgba(0,212,255,.3)}
|
||||
.recipe-item.locked{opacity:.55;cursor:not-allowed}
|
||||
.recipe-item.locked:hover{transform:none;border-color:var(--border-color)}
|
||||
.recipe-item.can-craft{border-color:var(--success-color)}
|
||||
.recipe-item.can-craft:hover{box-shadow:0 4px 12px rgba(0,255,136,.2)}
|
||||
.recipe-item.missing-materials{opacity:.7;border-color:var(--warning-color)}
|
||||
.recipe-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||
.recipe-header h4{color:var(--text-primary);font-size:.95rem}
|
||||
.recipe-level{background:var(--bg-primary);color:var(--warning-color);padding:3px 7px;border-radius:4px;font-size:.75rem;font-weight:600}
|
||||
.recipe-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.6rem}
|
||||
.recipe-materials{display:flex;gap:.4rem;flex-wrap:wrap}
|
||||
.material-tag{background:var(--bg-primary);color:var(--text-muted);padding:2px 6px;border-radius:4px;font-size:.72rem}
|
||||
.material-item{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.08);font-size:.82rem}
|
||||
.material-item:last-child{border-bottom:none}
|
||||
.material-item.missing{color:var(--error-color)}
|
||||
.material-name{color:var(--text-secondary)} .material-quantity{font-size:.82rem;font-weight:600;color:var(--text-primary)}
|
||||
.material-item.missing .material-quantity{color:var(--error-color);font-weight:600}
|
||||
.missing-materials-text{color:var(--error-color);font-size:.78rem;margin-top:.5rem;padding:.5rem;background:rgba(255,51,102,.1);border-radius:4px;border:1px solid rgba(255,51,102,.3)}
|
||||
.recipe-time{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);font-size:.78rem;margin-top:.4rem}
|
||||
.recipe-time i{color:var(--primary-color)}
|
||||
|
||||
/* ── CONSOLE WINDOW ──────────────────────────────────────────────────── */
|
||||
.console-window{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(580px,95vw);height:min(380px,75dvh);background:var(--bg-secondary);border:2px solid var(--primary-color);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.8);display:none;flex-direction:column;z-index:10000;font-family:'Courier New',monospace}
|
||||
.console-header{background:var(--gradient-primary);padding:10px 15px;border-radius:6px 6px 0 0;display:flex;justify-content:space-between;align-items:center;font-weight:bold;font-size:14px}
|
||||
.console-close{background:none;border:none;color:var(--text-primary);font-size:18px;cursor:pointer;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background .3s}
|
||||
.console-close:hover{background:rgba(255,255,255,.2)}
|
||||
.console-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||||
.console-output{flex:1;padding:12px;overflow-y:auto;background:var(--bg-primary);color:var(--text-primary);font-size:12px;line-height:1.4;border-bottom:1px solid var(--border-color)}
|
||||
.console-line{margin-bottom:4px;word-wrap:break-word}
|
||||
.console-line.console-info{color:var(--primary-color)} .console-line.console-success{color:var(--success-color)} .console-line.console-error{color:var(--error-color)} .console-line.console-warning{color:var(--warning-color)}
|
||||
.console-input-container{padding:10px;background:var(--bg-secondary)}
|
||||
.console-input{width:100%;background:var(--bg-primary);border:1px solid var(--border-color);color:var(--text-primary);padding:7px 11px;font-family:'Courier New',monospace;font-size:12px;border-radius:4px;outline:none}
|
||||
.console-input:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(0,212,255,.2)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
ANIMATIONS
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
TABLET ≥ 640px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:640px){
|
||||
:root{--header-h:56px;--pg:1rem}
|
||||
html{font-size:15px}
|
||||
.player-title{display:inline}
|
||||
.player-name{max-width:22ch}
|
||||
.dashboard-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.skills-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.shop-items{grid-template-columns:repeat(2,1fr)}
|
||||
.crafting-grid{grid-template-columns:repeat(2,1fr)}
|
||||
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr))}
|
||||
.starbases-container{flex-direction:row}
|
||||
.starbase-section{flex:1}
|
||||
.base-rooms{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
|
||||
.upgrade-list{grid-template-columns:repeat(3,1fr)}
|
||||
.nav-drawer-grid{grid-template-columns:repeat(5,1fr)}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ≥ 1024px — switch to top nav
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1024px){
|
||||
:root{--header-h:60px;--pg:1rem}
|
||||
html{font-size:15px}
|
||||
.main-nav{display:flex}
|
||||
.bottom-nav,.bottom-nav-more,.nav-drawer,.nav-drawer-overlay{display:none!important}
|
||||
.main-content{padding-bottom:var(--pg)}
|
||||
.player-title{display:inline}
|
||||
.player-name{max-width:26ch;font-size:.95rem}
|
||||
.resource{font-size:.85rem}
|
||||
.dashboard-grid{grid-template-columns:repeat(3,1fr);gap:1rem}
|
||||
.dungeons-container{flex-direction:row}
|
||||
.dungeon-selector{width:270px;flex-shrink:0;max-height:calc(100dvh - 160px);overflow-y:auto}
|
||||
.dungeon-view{flex:1;min-height:300px}
|
||||
.base-container{flex-direction:row}
|
||||
.base-view{flex:1}
|
||||
.base-upgrades{width:270px;flex-shrink:0}
|
||||
.skills-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.shop-items{grid-template-columns:repeat(3,1fr)}
|
||||
.crafting-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.crafting-content{flex-direction:row}
|
||||
.crafting-sidebar{width:210px;flex-shrink:0}
|
||||
.crafting-main{flex-direction:row;flex:1}
|
||||
.recipe-list{flex:1;max-height:none}
|
||||
.crafting-details{width:290px;flex-shrink:0}
|
||||
.inventory-container{flex-direction:row}
|
||||
.inventory-main{flex-direction:row}
|
||||
.inventory-grid{flex:1;max-height:none}
|
||||
.item-details{width:270px;flex-shrink:0}
|
||||
.starbases-container{flex-direction:row}
|
||||
.starbase-list,.starbase-shop{max-height:calc(100dvh - 250px)}
|
||||
.base-visualization-container{flex-direction:row}
|
||||
#baseCanvas{flex:1;height:auto;min-height:280px}
|
||||
.base-info-overlay{width:270px;flex-shrink:0}
|
||||
.base-stats-overlay{max-height:calc(100dvh - 310px)}
|
||||
.upgrade-list{grid-template-columns:repeat(auto-fill,minmax(170px,1fr))}
|
||||
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}
|
||||
.server-confirmation,.save-confirmation,.options-grid{flex-direction:row;gap:2rem}
|
||||
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{flex-direction:column;gap:.75rem;min-width:175px}
|
||||
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{width:100%;flex:none;min-width:auto}
|
||||
.base-navigation{justify-content:center}
|
||||
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{justify-content:center;flex-wrap:nowrap}
|
||||
.player-stats-grid{grid-template-columns:repeat(2,1fr)}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
WIDE ≥ 1280px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1280px){
|
||||
.dashboard-grid{grid-template-columns:repeat(4,1fr)}
|
||||
.skills-grid{grid-template-columns:repeat(4,1fr)}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
Galaxy Strike Online — Table & Ship Styles (mobile-first)
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Base table styles ───────────────────────────────────────────────── */
|
||||
.dungeon-table,.skills-table,.base-rooms-table,.base-upgrades-table,
|
||||
.ship-gallery-table,.starbase-management-table,.starbase-shop-table,
|
||||
.quests-table,.inventory-table,.shop-table {
|
||||
width:100%;border-collapse:collapse;background:var(--card-bg);
|
||||
border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.25);
|
||||
margin:8px 0;font-size:.8rem;
|
||||
}
|
||||
.dungeon-table th,.skills-table th,.base-rooms-table th,.base-upgrades-table th,
|
||||
.ship-gallery-table th,.starbase-management-table th,.starbase-shop-table th,
|
||||
.quests-table th,.inventory-table th,.shop-table th {
|
||||
background:var(--gradient-primary);color:var(--text-primary);
|
||||
padding:10px 12px;text-align:left;font-weight:600;font-size:.78rem;
|
||||
border-bottom:2px solid rgba(255,255,255,.1);
|
||||
}
|
||||
.dungeon-table td,.skills-table td,.base-rooms-table td,.base-upgrades-table td,
|
||||
.ship-gallery-table td,.starbase-management-table td,.starbase-shop-table td,
|
||||
.quests-table td,.inventory-table td,.shop-table td {
|
||||
padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.07);
|
||||
color:#e0e0e0;font-size:.78rem;
|
||||
}
|
||||
.dungeon-table tr:hover,.skills-table tr:hover,.base-rooms-table tr:hover,
|
||||
.base-upgrades-table tr:hover,.ship-gallery-table tr:hover,
|
||||
.starbase-management-table tr:hover,.starbase-shop-table tr:hover,
|
||||
.quests-table tr:hover,.inventory-table tr:hover,.shop-table tr:hover {
|
||||
background:rgba(102,126,234,.1);transition:background .25s;
|
||||
}
|
||||
|
||||
/* Difficulty colors */
|
||||
.dungeon-table .difficulty-easy{color:#00ff00}
|
||||
.dungeon-table .difficulty-medium{color:#ffff00}
|
||||
.dungeon-table .difficulty-hard{color:#ff9900}
|
||||
.dungeon-table .difficulty-extreme{color:#ff0000}
|
||||
|
||||
/* Skills table */
|
||||
.skills-table .skill-level{font-weight:bold;color:#667eea}
|
||||
.skills-table .skill-progress{width:80px;height:6px;background:rgba(255,255,255,.1);border-radius:3px;overflow:hidden}
|
||||
.skills-table .progress-fill{height:100%;background:linear-gradient(90deg,#667eea,#764ba2);transition:width .3s}
|
||||
|
||||
/* Base tables */
|
||||
.base-rooms-table .room-status-active{color:#00ff00}
|
||||
.base-rooms-table .room-status-inactive{color:#ff0000}
|
||||
.base-rooms-table .room-status-upgrading{color:#ffff00}
|
||||
.base-upgrades-table .upgrade-level{font-weight:bold;color:#667eea}
|
||||
|
||||
/* Action buttons in tables */
|
||||
.dungeon-table .btn-action,.skills-table .btn-action,.base-rooms-table .btn-action,
|
||||
.base-upgrades-table .btn-action,.ship-gallery-table .btn-action,
|
||||
.starbase-management-table .btn-action,.starbase-shop-table .btn-action,
|
||||
.quests-table .btn-action,.inventory-table .btn-action,.shop-table .btn-action {
|
||||
padding:5px 10px;border:none;border-radius:4px;cursor:pointer;
|
||||
font-size:.72rem;font-weight:600;transition:all .25s;text-transform:uppercase;
|
||||
min-height:32px;
|
||||
}
|
||||
.btn-action.btn-primary{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}
|
||||
.btn-action.btn-primary:hover{background:linear-gradient(135deg,#764ba2,#667eea);transform:translateY(-1px)}
|
||||
.btn-action.btn-secondary{background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2)}
|
||||
.btn-action.btn-secondary:hover{background:rgba(255,255,255,.2);transform:translateY(-1px)}
|
||||
.btn-action.btn-success{background:linear-gradient(135deg,#00ff00,#00cc00);color:#fff}
|
||||
.btn-action.btn-danger{background:linear-gradient(135deg,#ff0000,#cc0000);color:#fff}
|
||||
|
||||
/* Specialty table cells */
|
||||
.starbase-management-table .starbase-level{font-weight:bold;color:#667eea}
|
||||
.starbase-shop-table .starbase-cost{font-weight:bold;color:#ffd700}
|
||||
.quests-table .quest-type-main{color:#667eea} .quests-table .quest-type-daily{color:#00ff00}
|
||||
.quests-table .quest-type-procedural{color:#ff9900} .quests-table .quest-type-completed{color:#888}
|
||||
.quests-table .quest-type-failed{color:#ff0000}
|
||||
.quests-table .quest-progress,.inventory-table .item-stats{font-size:.72rem}
|
||||
.inventory-table .item-rarity-common{color:#888} .inventory-table .item-rarity-uncommon{color:#00ff00}
|
||||
.inventory-table .item-rarity-rare{color:#0088ff} .inventory-table .item-rarity-epic{color:#8833ff}
|
||||
.inventory-table .item-rarity-legendary{color:#ff8800}
|
||||
.shop-table .item-price{font-weight:bold;color:#ffd700}
|
||||
.shop-table .item-description{font-size:.72rem;color:#ccc;max-width:180px}
|
||||
|
||||
/* ── Ship grid ───────────────────────────────────────────────────────── */
|
||||
.ship-grid{display:grid;grid-template-columns:1fr;gap:12px;padding:12px 0}
|
||||
.ship-card{
|
||||
background:var(--card-bg);border-radius:12px;padding:12px;
|
||||
border:2px solid var(--primary-color);box-shadow:0 4px 20px rgba(0,0,0,.3);
|
||||
transition:all .25s;position:relative;overflow:hidden;
|
||||
}
|
||||
.ship-card:hover{transform:translateY(-4px);box-shadow:0 8px 28px rgba(0,212,255,.35)}
|
||||
.ship-card.active{border-color:var(--success-color);box-shadow:0 6px 22px rgba(0,255,136,.18)}
|
||||
.ship-card.active::before{content:"ACTIVE";position:absolute;top:8px;right:8px;background:var(--gradient-secondary);color:var(--text-primary);padding:3px 7px;border-radius:4px;font-size:9px;font-weight:bold;text-transform:uppercase;letter-spacing:.5px}
|
||||
|
||||
.ship-card-header{display:flex;flex-direction:row;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.ship-card-image{width:70px;height:70px;border-radius:8px;object-fit:cover;border:2px solid var(--primary-color);flex-shrink:0}
|
||||
.ship-card-info{flex:1;min-width:0}
|
||||
.ship-card-rarity{color:var(--text-secondary);font-size:.72rem;font-weight:bold;text-transform:uppercase;letter-spacing:.5px;padding:3px 7px;border-radius:4px;background:var(--hover-bg);border:1px solid var(--border-color);display:inline-block}
|
||||
.ship-card-rarity.common{color:#888;border-color:#888}
|
||||
.ship-card-rarity.rare{color:var(--primary-color);border-color:var(--primary-color)}
|
||||
.ship-card-rarity.epic{color:var(--accent-color);border-color:var(--accent-color)}
|
||||
.ship-card-rarity.legendary{color:var(--warning-color);border-color:var(--warning-color)}
|
||||
|
||||
.ship-card-stats{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px}
|
||||
.ship-card-stat{display:flex;justify-content:space-between;align-items:center;padding:5px 8px;background:rgba(255,255,255,.04);border-radius:4px;border:1px solid rgba(255,255,255,.08)}
|
||||
.ship-card-stat .stat-label{color:var(--text-muted);font-size:.68rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.ship-card-stat .stat-value{color:var(--text-secondary);font-weight:bold;font-size:.72rem}
|
||||
.ship-card-stat .stat-value.health{color:var(--success-color)} .ship-card-stat .stat-value.attack{color:var(--warning-color)}
|
||||
.ship-card-stat .stat-value.defense{color:var(--accent-color)} .ship-card-stat .stat-value.speed{color:var(--secondary-color)}
|
||||
|
||||
.ship-card-actions{display:flex;gap:8px}
|
||||
.ship-card-actions .btn-action{flex:1;padding:7px 10px;border:none;border-radius:6px;cursor:pointer;font-size:.72rem;font-weight:600;transition:all .25s;text-transform:uppercase;min-height:36px;-webkit-tap-highlight-color:transparent}
|
||||
.btn-action.btn-switch{background:var(--gradient-primary);color:var(--text-primary)}
|
||||
.btn-action.btn-switch:hover{background:var(--gradient-secondary);transform:translateY(-2px)}
|
||||
.btn-action.btn-switch:disabled{background:var(--text-muted);cursor:not-allowed;transform:none}
|
||||
.btn-action.btn-upgrade,.btn-action.btn-repair{background:var(--gradient-secondary);color:var(--text-primary)}
|
||||
.btn-action.btn-upgrade:hover,.btn-action.btn-repair:hover{background:var(--gradient-primary);transform:translateY(-2px)}
|
||||
|
||||
/* Ship layout (current ship + grid) */
|
||||
.ship-layout{display:flex;flex-direction:column;gap:1rem;margin-top:.75rem}
|
||||
.current-ship-section{background:var(--card-bg);border-radius:8px;padding:1rem;border:2px solid var(--primary-color)}
|
||||
.ship-grid-section{flex:1;min-width:0}
|
||||
.ship-grid-section h4,.current-ship-section h4{color:var(--primary-color);margin-bottom:.75rem;font-size:.92rem;text-transform:uppercase;letter-spacing:1px}
|
||||
.current-ship-section h4{text-align:center}
|
||||
.current-ship-display{display:flex;flex-direction:column;align-items:center;gap:1rem;text-align:center}
|
||||
.current-ship-image img{width:100px;height:100px;object-fit:cover;border-radius:8px;border:2px solid var(--primary-color);box-shadow:0 4px 15px rgba(0,212,255,.3)}
|
||||
.current-ship-details{flex:1;text-align:center;min-width:0}
|
||||
.current-ship-details h5{color:var(--text-primary);margin-bottom:.75rem;font-size:1.1rem;font-weight:bold}
|
||||
.current-ship-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.ship-stat{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--hover-bg);border-radius:4px;border:1px solid var(--border-color)}
|
||||
.ship-stat .stat-label{color:var(--text-muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.ship-stat .stat-value{color:var(--text-secondary);font-weight:bold;font-size:.82rem}
|
||||
.ship-stat .stat-value.health{color:var(--success-color)} .ship-stat .stat-value.attack{color:var(--warning-color)}
|
||||
.ship-stat .stat-value.defense{color:var(--accent-color)} .ship-stat .stat-value.speed{color:var(--secondary-color)}
|
||||
.ship-table-section{margin-top:1.5rem}
|
||||
.ship-table-section h4{color:#667eea;margin-bottom:.75rem;font-size:.92rem;text-transform:uppercase;letter-spacing:1px}
|
||||
|
||||
/* ── Console window ──────────────────────────────────────────────────── */
|
||||
.console-window{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(580px,96vw);height:min(380px,70dvh);background:var(--bg-secondary);border:2px solid var(--primary-color);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.8);z-index:10000;display:none;flex-direction:column;font-family:'Courier New',monospace}
|
||||
.console-header{background:var(--gradient-primary);color:var(--text-primary);padding:10px 15px;border-radius:6px 6px 0 0;display:flex;justify-content:space-between;align-items:center;font-weight:bold;font-size:13px}
|
||||
.console-close{background:none;border:none;color:var(--text-primary);font-size:18px;cursor:pointer;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background .2s}
|
||||
.console-close:hover{background:rgba(255,255,255,.2)}
|
||||
.console-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||||
.console-output{flex:1;padding:12px;overflow-y:auto;background:var(--bg-primary);color:var(--text-primary);font-size:12px;line-height:1.4;border-bottom:1px solid var(--border-color)}
|
||||
.console-line{margin-bottom:4px;word-wrap:break-word}
|
||||
.console-error{color:var(--error-color)} .console-success{color:var(--success-color)}
|
||||
.console-warning{color:var(--warning-color)} .console-info{color:var(--primary-color)}
|
||||
.console-input-container{padding:10px;background:var(--bg-secondary)}
|
||||
.console-input{width:100%;background:var(--bg-primary);border:1px solid var(--border-color);color:var(--text-primary);padding:7px 11px;font-family:'Courier New',monospace;font-size:12px;border-radius:4px;outline:none}
|
||||
.console-input:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(0,212,255,.2)}
|
||||
.console-input::placeholder{color:var(--text-muted)}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
TABLET ≥ 640px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:640px){
|
||||
.ship-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.ship-layout{flex-direction:row}
|
||||
.current-ship-section{flex:0 0 320px}
|
||||
.current-ship-display{flex-direction:row;align-items:flex-start;text-align:left}
|
||||
.current-ship-details{text-align:left}
|
||||
.current-ship-details h5{text-align:left}
|
||||
.current-ship-stats{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
DESKTOP ≥ 1024px
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media(min-width:1024px){
|
||||
.ship-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
|
||||
.ship-card-header{flex-direction:column;align-items:center;text-align:center}
|
||||
.ship-card-image{width:80px;height:80px}
|
||||
.current-ship-section{flex:0 0 380px}
|
||||
.current-ship-stats{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
|
||||
/* ── Responsive table fallback — horizontal scroll ───────────────────── */
|
||||
@media(max-width:639px){
|
||||
.dungeon-table,.skills-table,.base-rooms-table,.base-upgrades-table,
|
||||
.ship-gallery-table,.starbase-management-table,.starbase-shop-table,
|
||||
.quests-table,.inventory-table,.shop-table{
|
||||
display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;white-space:nowrap;
|
||||
}
|
||||
.dungeon-table th,.skills-table th,.base-rooms-table th,.base-upgrades-table th,
|
||||
.ship-gallery-table th,.starbase-management-table th,.starbase-shop-table th,
|
||||
.quests-table th,.inventory-table th,.shop-table th{padding:8px 10px;font-size:.72rem}
|
||||
.dungeon-table td,.skills-table td,.base-rooms-table td,.base-upgrades-table td,
|
||||
.ship-gallery-table td,.starbase-management-table td,.starbase-shop-table td,
|
||||
.quests-table td,.inventory-table td,.shop-table td{padding:8px 10px;font-size:.72rem}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 200 KiB |