re-did the entire repo, and added many features and removed lingering features.

This commit is contained in:
Robert MacRae 2026-03-10 10:59:13 -03:00
parent 485b5c3eb8
commit 1495c41e43
1135 changed files with 58178 additions and 17805 deletions

View File

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

View File

@ -1,699 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galaxy Strike Online - Space Idle MMORPG</title>
<link rel="stylesheet" href="styles/main.css?v=2">
<link rel="stylesheet" href="styles/components.css">
<link rel="stylesheet" href="styles/tables.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="js/SimpleLocalServer.js"></script>
<script src="js/LocalServerManager.js"></script>
</head>
<body>
<!-- Custom Title Bar -->
<div id="titleBar" class="title-bar">
<div class="title-bar-left">
<span class="title-bar-title">Galaxy Strike Online</span>
</div>
<div class="title-bar-right">
<button class="title-bar-btn" id="minimizeBtn" title="Minimize">
<i class="fas fa-minus"></i>
</button>
<button class="title-bar-btn" id="fullscreenBtn" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="title-bar-btn close-btn" id="closeBtn" title="Close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="app">
<!-- Main Menu -->
<div id="mainMenu" class="main-menu">
<div class="menu-container">
<div class="menu-header">
<h1 class="menu-title">GALAXY STRIKE ONLINE</h1>
<p class="menu-subtitle">Space Idle MMORPG</p>
</div>
<div class="menu-content">
<!-- Login Section -->
<div id="loginSection" class="menu-section">
<h2 class="section-title">Account Access</h2>
<div class="login-form">
<div class="form-group">
<label for="emailInput">Email</label>
<input type="email" id="emailInput" placeholder="Enter your email" class="form-input">
</div>
<div class="form-group">
<label for="passwordInput">Password</label>
<input type="password" id="passwordInput" placeholder="Enter your password" class="form-input">
</div>
<div class="login-options">
<button class="btn btn-primary btn-large" id="loginBtn">
<i class="fas fa-sign-in-alt"></i>
Login
</button>
<button class="btn btn-secondary btn-large" id="registerBtn">
<i class="fas fa-user-plus"></i>
Register
</button>
</div>
</div>
<div class="login-notice" id="loginNotice">
<p><i class="fas fa-info-circle"></i> Connect to the live server to play</p>
</div>
</div>
<!-- Server Browser Section -->
<div id="serverSection" class="menu-section hidden">
<h2 class="section-title">Server Browser</h2>
<div class="server-controls">
<button class="btn btn-primary" id="createServerBtn">
<i class="fas fa-server"></i>
Start Local Server
</button>
<button class="btn btn-secondary" id="refreshServersBtn">
<i class="fas fa-sync"></i>
Refresh
</button>
<div class="server-filters">
<select id="regionFilter" class="filter-select">
<option value="">All Regions</option>
<option value="us-east">US East</option>
<option value="us-west">US West</option>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
</select>
<select id="typeFilter" class="filter-select">
<option value="">All Types</option>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
</div>
<div class="server-list" id="serverList">
<div class="server-loading" id="serverLoading">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading servers...</p>
</div>
<div class="server-empty hidden" id="serverEmpty">
<i class="fas fa-server"></i>
<p>No servers found. Create your own server to play!</p>
</div>
<!-- Servers will be populated here -->
</div>
<div class="server-actions">
<button class="btn btn-secondary" id="backToLoginBtn">
<i class="fas fa-arrow-left"></i>
Back to Login
</button>
</div>
</div>
<!-- Server Join Confirmation Section -->
<div id="serverConfirmSection" class="menu-section hidden">
<h2 class="section-title">Server Selected</h2>
<div class="server-confirmation">
<div class="confirm-actions-left">
<button class="btn btn-primary btn-large btn-join-server" id="joinServerBtn">
<i class="fas fa-sign-in-alt"></i>
Join Server
</button>
</div>
<div class="selected-server-info-center">
<div class="server-preview">
<h3 id="selectedServerName">Server Name</h3>
<div class="server-details" id="selectedServerDetails">
<p class="server-info">Type: <span id="selectedServerType">Public</span></p>
<p class="server-info">Region: <span id="selectedServerRegion">US East</span></p>
<p class="server-info">Players: <span id="selectedServerPlayers">0/10</span></p>
<p class="server-info">Owner: <span id="selectedServerOwner">Unknown</span></p>
</div>
</div>
</div>
<div class="confirm-actions-right">
<button class="btn btn-info btn-large" id="serverInfoBtn">
<i class="fas fa-info"></i>
More Info
</button>
<button class="btn btn-warning btn-large" id="backToServersBtn">
<i class="fas fa-arrow-left"></i>
Back to List
</button>
</div>
</div>
</div>
<!-- Game Options Section -->
<div id="optionsSection" class="menu-section hidden">
<h2 class="section-title">Game Options</h2>
<div class="options-grid" style="display: flex !important; justify-content: space-between !important; gap: 30px !important; margin-bottom: 30px !important;">
<div class="options-left" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: flex-end !important; min-width: 200px !important;">
<button class="btn btn-primary btn-large" id="continueBtn" style="width: 200px !important;">
<i class="fas fa-gamepad"></i>
Continue
</button>
<button class="btn btn-primary btn-large" id="newGameBtn" style="width: 200px !important;">
<i class="fas fa-play"></i>
New Game
</button>
</div>
<div class="options-center" style="display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; flex: 1 !important; min-width: 300px !important;">
<div class="save-info-display" style="background: rgba(0, 212, 255, 0.1) !important; border: 2px solid rgba(0, 212, 255, 0.3) !important; border-radius: 10px !important; padding: 20px !important; text-align: center !important; width: 100% !important; max-width: 400px !important;">
<h3 style="color: #00d4ff !important; margin-bottom: 15px !important; font-size: 1.2em !important;">Save Information</h3>
<div id="saveInfoDetails" style="color: #ffffff !important; font-size: 0.9em !important; line-height: 1.4 !important;">
<p>Select a save slot to view details</p>
</div>
</div>
</div>
<div class="options-right" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: space-between !important; min-width: 200px !important;">
<button class="btn btn-info btn-large" id="settingsBtn" style="width: 200px !important;">
<i class="fas fa-cog"></i>
Settings
</button>
<button class="btn btn-warning btn-large" id="deleteSaveBtn" style="width: 200px !important;">
<i class="fas fa-trash"></i>
Delete Save
</button>
</div>
</div>
<div class="options-actions">
<button class="btn btn-secondary" id="backToSavesBtn">
<i class="fas fa-arrow-left"></i>
Back to Saves
</button>
</div>
</div>
</div>
<div class="menu-footer">
<p class="version-text">Version 1.0.0</p>
<div class="footer-links">
<button class="link-btn" id="aboutBtn">About</button>
<button class="link-btn" id="helpBtn">Help</button>
</div>
</div>
</div>
</div>
<!-- Loading Screen -->
<div id="loadingScreen" class="loading-screen hidden">
<div class="loading-content">
<h1 class="game-title">GALAXY STRIKE ONLINE</h1>
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<p class="loading-text">Initializing Universe...</p>
</div>
</div>
<!-- Main Game Interface -->
<div id="gameInterface" class="game-interface hidden">
<!-- Header -->
<header class="game-header">
<div class="header-left">
<h1 class="logo">GSO</h1>
<div class="player-info">
<span class="player-name" id="playerName">Commander</span>
<span class="player-level" id="playerLevel">Lv. 1</span>
</div>
</div>
<div class="header-center">
<div class="resources">
<div class="resource">
<i class="fas fa-coins"></i>
<span id="credits">1,000</span>
</div>
<div class="resource">
<i class="fas fa-gem"></i>
<span id="gems">10</span>
</div>
<div class="resource">
<i class="fas fa-bolt"></i>
<span id="energy">100/100</span>
</div>
</div>
</div>
<div class="header-right">
<button class="btn btn-secondary" id="settingsBtn">
<i class="fas fa-cog"></i>
</button>
<button class="btn btn-secondary" id="discordBtn">
<i class="fab fa-discord"></i>
</button>
<button class="btn btn-info" id="localServerBtn" title="Local Server">
<i class="fas fa-server"></i>
</button>
<!-- <button class="btn btn-primary" id="saveBtn" title="Save Game">
<i class="fas fa-save"></i>
</button> -->
<button class="btn btn-warning" id="returnToMenuBtn">
<i class="fas fa-home"></i>
</button>
</div>
</header>
<!-- Navigation -->
<nav class="main-nav">
<button class="nav-btn active" data-tab="dashboard">
<i class="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</button>
<button class="nav-btn" data-tab="dungeons">
<i class="fas fa-dungeon"></i>
<span>Dungeons</span>
</button>
<button class="nav-btn" data-tab="skills">
<i class="fas fa-graduation-cap"></i>
<span>Skills</span>
</button>
<button class="nav-btn" data-tab="base">
<i class="fas fa-home"></i>
<span>Base</span>
</button>
<button class="nav-btn" data-tab="quests">
<i class="fas fa-scroll"></i>
<span>Quests</span>
</button>
<button class="nav-btn" data-tab="inventory">
<i class="fas fa-backpack"></i>
<span>Inventory</span>
</button>
<button class="nav-btn" data-tab="crafting">
<i class="fas fa-hammer"></i>
<span>Crafting</span>
</button>
<button class="nav-btn" data-tab="shop">
<i class="fas fa-store"></i>
<span>Shop</span>
</button>
</nav>
<!-- Main Content Area -->
<main class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="dashboard-tab">
<div class="dashboard-grid">
<div class="card">
<h3>Fleet Status</h3>
<div class="fleet-info">
<div class="ship-status">
<i class="fas fa-rocket"></i>
<div>
<p>Flagship: <span id="flagshipName">Starter Cruiser</span></p>
<p>Health: <span id="shipHealth">100%</span></p>
</div>
</div>
</div>
</div>
<div class="card">
<h3>Idle Progress</h3>
<div class="idle-stats">
<p>Offline Time: <span id="offlineTime">0h 0m</span></p>
<p>Resources Gained: <span id="offlineResources">0</span></p>
<button class="btn btn-primary" id="claimOfflineBtn">Claim Rewards</button>
</div>
<div class="stats-grid">
<div class="stat">
<span class="stat-label">Total Kills</span>
<span class="stat-value" id="totalKills">0</span>
</div>
<div class="stat">
<span class="stat-label">Dungeons Cleared</span>
<span class="stat-value" id="dungeonsCleared">0</span>
</div>
<div class="stat">
<span class="stat-label">Play Time</span>
<span class="stat-value" id="playTime">0h 0m</span>
</div>
</div>
</div>
</div>
</div>
<!-- Dungeons Tab -->
<div class="tab-content" id="dungeons-tab">
<div class="dungeons-container">
<div class="dungeon-selector">
<h2>Select Dungeon</h2>
<div class="dungeon-list" id="dungeonList">
<!-- Dungeons will be generated here -->
</div>
</div>
<div class="dungeon-view" id="dungeonView">
<div class="dungeon-placeholder">
<i class="fas fa-dungeon"></i>
<p>Select a dungeon to begin your adventure</p>
</div>
</div>
</div>
</div>
<!-- Skills Tab -->
<div class="tab-content" id="skills-tab">
<div class="skills-container">
<div class="skills-header">
<h2><i class="fas fa-graduation-cap"></i> Skills</h2>
<div class="skill-points-display">
<span class="skill-points">Skill Points: 0</span>
</div>
</div>
<div class="skill-categories">
<button class="skill-cat-btn active" data-category="combat">Combat</button>
<button class="skill-cat-btn" data-category="science">Science</button>
<button class="skill-cat-btn" data-category="crafting">Crafting</button>
</div>
<div class="skills-grid" id="skillsGrid">
<!-- Skills will be generated here -->
</div>
</div>
</div>
<!-- Base Tab -->
<div class="tab-content" id="base-tab">
<div class="base-navigation">
<button class="base-nav-btn active" data-view="overview">Base Overview</button>
<button class="base-nav-btn" data-view="visualization">Base Visualization</button>
<button class="base-nav-btn" data-view="ships">Ship Gallery</button>
<button class="base-nav-btn" data-view="starbases">Starbases</button>
</div>
<!-- Base Overview -->
<div class="base-view-content" id="base-overview">
<div class="base-container">
<div class="base-view">
<div class="base-info">
<h3>Base Information</h3>
<div class="base-stats">
<div class="stat-item">
<span class="stat-label">Power Usage:</span>
<span class="stat-value" id="basePowerUsage">0/100</span>
</div>
<div class="stat-item">
<span class="stat-label">Storage:</span>
<span class="stat-value" id="baseStorage">1000</span>
</div>
<div class="stat-item">
<span class="stat-label">Production Rate:</span>
<span class="stat-value" id="baseProduction">0/s</span>
</div>
</div>
</div>
<div class="base-rooms" id="baseRooms">
<!-- Base rooms will be generated here -->
</div>
</div>
<div class="base-upgrades">
<h3>Base Upgrades</h3>
<div class="upgrade-list" id="baseUpgrades">
<!-- Upgrades will be generated here -->
</div>
</div>
</div>
</div>
<!-- Base Visualization -->
<div class="base-view-content hidden" id="base-visualization">
<div class="base-visualization-container">
<canvas id="baseCanvas" width="800" height="600"></canvas>
<div class="base-info-overlay">
<div class="base-stats-overlay">
<h3>Base Information</h3>
<div id="baseInfoDisplay"></div>
</div>
</div>
</div>
</div>
<!-- Ship Gallery -->
<div class="base-view-content hidden" id="base-ships">
<div class="ship-gallery-container">
<h3>Your Ships</h3>
<div class="ship-layout">
<!-- Current Ship Section -->
<div class="current-ship-section">
<h4>Current Ship</h4>
<div class="current-ship-display" id="currentShipDisplay" data-debug-id="current-ship-panel">
<div class="current-ship-info">
<div class="current-ship-image">
<img src="assets/textures/ships/starter_cruiser.png" alt="Current Ship" id="currentShipImage">
</div>
<div class="current-ship-details">
<h5 id="currentShipName">Starter Cruiser</h5>
<div class="current-ship-stats">
<div class="ship-stat">
<span class="stat-label">Class:</span>
<span class="stat-value" id="currentShipClass">Light</span>
</div>
<div class="ship-stat">
<span class="stat-label">Level:</span>
<span class="stat-value" id="currentShipLevel">1</span>
</div>
<div class="ship-stat">
<span class="stat-label">Health:</span>
<span class="stat-value" id="currentShipHealth">100/100</span>
</div>
<div class="ship-stat">
<span class="stat-label">Attack:</span>
<span class="stat-value" id="currentShipAttack">10</span>
</div>
<div class="ship-stat">
<span class="stat-label">Defense:</span>
<span class="stat-value" id="currentShipDefense">5</span>
</div>
<div class="ship-stat">
<span class="stat-label">Speed:</span>
<span class="stat-value" id="currentShipSpeed">15</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Ship Grid -->
<div class="ship-grid-section">
<h4>Ships Collected</h4>
<div class="ship-grid" id="shipGrid">
<!-- Ships will be displayed here as cards -->
</div>
</div>
</div>
</div>
</div>
<!-- Starbases -->
<div class="base-view-content hidden" id="base-starbases">
<div class="starbases-container">
<div class="starbase-section">
<h3>Starbase Management</h3>
<div class="starbase-list" id="starbaseList">
<!-- Starbases will be displayed here -->
</div>
</div>
<div class="starbase-section">
<h3>Available Starbases</h3>
<div class="starbase-shop" id="starbasePurchaseList">
<div class="starbase-purchase-list" id="starbasePurchaseItems">
<!-- Available starbases for purchase -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quests Tab -->
<div class="tab-content" id="quests-tab">
<div class="quests-container">
<div class="quest-tabs">
<button class="quest-tab-btn active" data-type="main">Main Story</button>
<button class="quest-tab-btn" data-type="daily">Daily</button>
<button class="quest-tab-btn" data-type="weekly">Weekly</button>
<button class="quest-tab-btn" data-type="completed">Completed</button>
<button class="quest-tab-btn" data-type="failed">Failed Quests</button>
</div>
<div class="daily-countdown" id="dailyCountdown">Daily quests reset in: 00:00:00</div>
<div class="weekly-countdown" id="weeklyCountdown">Weekly quests reset in: 0d 00:00</div>
<div class="quest-list" id="questList">
<!-- Quests will be generated here -->
</div>
</div>
</div>
<!-- Inventory Tab -->
<div class="tab-content" id="inventory-tab">
<div class="inventory-container">
<div class="equipment-section">
<h3>Equipment</h3>
<div class="equipment-slots">
<div class="equipment-slot">
<div class="slot-label">Weapon</div>
<div class="slot-container" id="equip-weapon">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Armor</div>
<div class="slot-container" id="equip-armor">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Engine</div>
<div class="slot-container" id="equip-engine">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Shield</div>
<div class="slot-container" id="equip-shield">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Accessory</div>
<div class="slot-container" id="equip-accessory">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
</div>
</div>
<div class="inventory-main">
<div class="inventory-section">
<h3>Inventory</h3>
<div class="inventory-grid" id="inventoryGrid">
<!-- Inventory items will be generated here -->
</div>
</div>
<div class="item-details" id="itemDetails">
<p>Select an item to view details</p>
</div>
</div>
</div>
</div>
<!-- Crafting Tab -->
<div class="tab-content" id="crafting-tab">
<div class="crafting-container">
<div class="crafting-header">
<h2><i class="fas fa-hammer"></i> Crafting Station</h2>
<div class="crafting-info">
<div class="crafting-level">
<i class="fas fa-level-up-alt"></i>
<span>Crafting Level: </span>
<span id="craftingLevel">1</span>
</div>
<div class="crafting-experience">
<i class="fas fa-star"></i>
<span>Experience: </span>
<span id="craftingExp">0/100</span>
</div>
</div>
</div>
<div class="crafting-categories">
<button class="crafting-cat-btn active" data-category="weapons">Weapons</button>
<button class="crafting-cat-btn" data-category="armor">Armor</button>
<button class="crafting-cat-btn" data-category="items">Items</button>
<button class="crafting-cat-btn" data-category="ships">Ships</button>
</div>
<div class="crafting-grid" id="recipeList">
<!-- Recipes will be generated here -->
</div>
</div>
</div>
<!-- Shop Tab -->
<div class="tab-content" id="shop-tab">
<div class="shop-container">
<div class="shop-header">
<div class="shop-categories">
<button class="shop-cat-btn active" data-category="ships">Ships</button>
<button class="shop-cat-btn" data-category="weapons">Weapons</button>
<button class="shop-cat-btn" data-category="armors">Armors</button>
<!-- <button class="shop-cat-btn" data-category="upgrades">Upgrades</button> -->
<button class="shop-cat-btn" data-category="cosmetics">Cosmetics</button>
<button class="shop-cat-btn" data-category="consumables">Consumables</button>
<button class="shop-cat-btn" data-category="materials">Materials</button>
</div>
</div>
<div class="shop-content">
<div class="shop-items-container">
<div class="shop-items" id="shopItems">
<!-- Shop items will be generated here -->
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Modals -->
<div class="modal-overlay hidden" id="modalOverlay">
<div class="modal" id="modal">
<div class="modal-header">
<h3 id="modalTitle">Modal Title</h3>
<button class="modal-close" id="modalClose">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Modal content will be inserted here -->
</div>
</div>
</div>
</div>
<!-- Loading Progress Indicator -->
<div class="loading-indicator" id="loadingIndicator"></div>
<div class="loading-status hidden" id="loadingStatus">Initializing...</div>
<!-- Scripts -->
<script src="../config/xp-progression.js"></script>
<script src="js/core/DebugLogger.js"></script>
<script src="js/core/Logger.js"></script>
<script src="js/core/TextureManager.js"></script>
<script src="js/core/GameEngine.js"></script>
<script src="js/core/Player.js"></script>
<script src="js/core/Inventory.js"></script>
<script src="js/core/Economy.js"></script>
<script src="js/systems/DungeonSystem.js"></script>
<script src="js/systems/SkillSystem.js"></script>
<script src="js/systems/BaseSystem.js"></script>
<script src="js/systems/QuestSystem.js"></script>
<script src="js/systems/ShipSystem.js"></script>
<script src="js/systems/IdleSystem.js"></script>
<script src="js/systems/CraftingSystem.js"></script>
<script src="js/data/GameData.js"></script>
<script src="js/ui/UIManager.js"></script>
<script src="js/SmartSaveManager.js"></script>
<script src="js/SaveSystemIntegration.js"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="js/GameInitializer.js"></script>
<script src="js/ui/LiveMainMenu.js"></script>
<script src="js/main.js"></script>
<!-- Hidden Console Window -->
<div id="consoleWindow" class="console-window">
<div class="console-header">
<span>Developer Console</span>
<button class="console-close" onclick="toggleConsole()">×</button>
</div>
<div class="console-content">
<div id="consoleOutput" class="console-output"></div>
<div class="console-input-container">
<input type="text" id="consoleInput" class="console-input" placeholder="Type command here..." onkeypress="handleConsoleInput(event)">
</div>
</div>
</div>
</body>
</html>

View File

@ -1,699 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galaxy Strike Online - Space Idle MMORPG</title>
<link rel="stylesheet" href="styles/main.css?v=2">
<link rel="stylesheet" href="styles/components.css">
<link rel="stylesheet" href="styles/tables.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="js/SimpleLocalServer.js"></script>
<script src="js/LocalServerManager.js"></script>
</head>
<body>
<!-- Custom Title Bar -->
<div id="titleBar" class="title-bar">
<div class="title-bar-left">
<span class="title-bar-title">Galaxy Strike Online</span>
</div>
<div class="title-bar-right">
<button class="title-bar-btn" id="minimizeBtn" title="Minimize">
<i class="fas fa-minus"></i>
</button>
<button class="title-bar-btn" id="fullscreenBtn" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="title-bar-btn close-btn" id="closeBtn" title="Close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="app">
<!-- Main Menu -->
<div id="mainMenu" class="main-menu">
<div class="menu-container">
<div class="menu-header">
<h1 class="menu-title">GALAXY STRIKE ONLINE</h1>
<p class="menu-subtitle">Space Idle MMORPG</p>
</div>
<div class="menu-content">
<!-- Login Section -->
<div id="loginSection" class="menu-section">
<h2 class="section-title">Account Access</h2>
<div class="login-form">
<div class="form-group">
<label for="emailInput">Email</label>
<input type="email" id="emailInput" placeholder="Enter your email" class="form-input">
</div>
<div class="form-group">
<label for="passwordInput">Password</label>
<input type="password" id="passwordInput" placeholder="Enter your password" class="form-input">
</div>
<div class="login-options">
<button class="btn btn-primary btn-large" id="loginBtn">
<i class="fas fa-sign-in-alt"></i>
Login
</button>
<button class="btn btn-secondary btn-large" id="registerBtn">
<i class="fas fa-user-plus"></i>
Register
</button>
</div>
</div>
<div class="login-notice" id="loginNotice">
<p><i class="fas fa-info-circle"></i> Connect to the live server to play</p>
</div>
</div>
<!-- Server Browser Section -->
<div id="serverSection" class="menu-section hidden">
<h2 class="section-title">Server Browser</h2>
<div class="server-controls">
<button class="btn btn-primary" id="createServerBtn">
<i class="fas fa-server"></i>
Start Local Server
</button>
<button class="btn btn-secondary" id="refreshServersBtn">
<i class="fas fa-sync"></i>
Refresh
</button>
<div class="server-filters">
<select id="regionFilter" class="filter-select">
<option value="">All Regions</option>
<option value="us-east">US East</option>
<option value="us-west">US West</option>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
</select>
<select id="typeFilter" class="filter-select">
<option value="">All Types</option>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
</div>
<div class="server-list" id="serverList">
<div class="server-loading" id="serverLoading">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading servers...</p>
</div>
<div class="server-empty hidden" id="serverEmpty">
<i class="fas fa-server"></i>
<p>No servers found. Create your own server to play!</p>
</div>
<!-- Servers will be populated here -->
</div>
<div class="server-actions">
<button class="btn btn-secondary" id="backToLoginBtn">
<i class="fas fa-arrow-left"></i>
Back to Login
</button>
</div>
</div>
<!-- Server Join Confirmation Section -->
<div id="serverConfirmSection" class="menu-section hidden">
<h2 class="section-title">Server Selected</h2>
<div class="server-confirmation">
<div class="confirm-actions-left">
<button class="btn btn-primary btn-large btn-join-server" id="joinServerBtn">
<i class="fas fa-sign-in-alt"></i>
Join Server
</button>
</div>
<div class="selected-server-info-center">
<div class="server-preview">
<h3 id="selectedServerName">Server Name</h3>
<div class="server-details" id="selectedServerDetails">
<p class="server-info">Type: <span id="selectedServerType">Public</span></p>
<p class="server-info">Region: <span id="selectedServerRegion">US East</span></p>
<p class="server-info">Players: <span id="selectedServerPlayers">0/10</span></p>
<p class="server-info">Owner: <span id="selectedServerOwner">Unknown</span></p>
</div>
</div>
</div>
<div class="confirm-actions-right">
<button class="btn btn-info btn-large" id="serverInfoBtn">
<i class="fas fa-info"></i>
More Info
</button>
<button class="btn btn-warning btn-large" id="backToServersBtn">
<i class="fas fa-arrow-left"></i>
Back to List
</button>
</div>
</div>
</div>
<!-- Game Options Section -->
<div id="optionsSection" class="menu-section hidden">
<h2 class="section-title">Game Options</h2>
<div class="options-grid" style="display: flex !important; justify-content: space-between !important; gap: 30px !important; margin-bottom: 30px !important;">
<div class="options-left" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: flex-end !important; min-width: 200px !important;">
<button class="btn btn-primary btn-large" id="continueBtn" style="width: 200px !important;">
<i class="fas fa-gamepad"></i>
Continue
</button>
<button class="btn btn-primary btn-large" id="newGameBtn" style="width: 200px !important;">
<i class="fas fa-play"></i>
New Game
</button>
</div>
<div class="options-center" style="display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; flex: 1 !important; min-width: 300px !important;">
<div class="save-info-display" style="background: rgba(0, 212, 255, 0.1) !important; border: 2px solid rgba(0, 212, 255, 0.3) !important; border-radius: 10px !important; padding: 20px !important; text-align: center !important; width: 100% !important; max-width: 400px !important;">
<h3 style="color: #00d4ff !important; margin-bottom: 15px !important; font-size: 1.2em !important;">Save Information</h3>
<div id="saveInfoDetails" style="color: #ffffff !important; font-size: 0.9em !important; line-height: 1.4 !important;">
<p>Select a save slot to view details</p>
</div>
</div>
</div>
<div class="options-right" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: space-between !important; min-width: 200px !important;">
<button class="btn btn-info btn-large" id="settingsBtn" style="width: 200px !important;">
<i class="fas fa-cog"></i>
Settings
</button>
<button class="btn btn-warning btn-large" id="deleteSaveBtn" style="width: 200px !important;">
<i class="fas fa-trash"></i>
Delete Save
</button>
</div>
</div>
<div class="options-actions">
<button class="btn btn-secondary" id="backToSavesBtn">
<i class="fas fa-arrow-left"></i>
Back to Saves
</button>
</div>
</div>
</div>
<div class="menu-footer">
<p class="version-text">Version 1.0.0</p>
<div class="footer-links">
<button class="link-btn" id="aboutBtn">About</button>
<button class="link-btn" id="helpBtn">Help</button>
</div>
</div>
</div>
</div>
<!-- Loading Screen -->
<div id="loadingScreen" class="loading-screen hidden">
<div class="loading-content">
<h1 class="game-title">GALAXY STRIKE ONLINE</h1>
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<p class="loading-text">Initializing Universe...</p>
</div>
</div>
<!-- Main Game Interface -->
<div id="gameInterface" class="game-interface hidden">
<!-- Header -->
<header class="game-header">
<div class="header-left">
<h1 class="logo">GSO</h1>
<div class="player-info">
<span class="player-name" id="playerName">Commander</span>
<span class="player-level" id="playerLevel">Lv. 1</span>
</div>
</div>
<div class="header-center">
<div class="resources">
<div class="resource">
<i class="fas fa-coins"></i>
<span id="credits">1,000</span>
</div>
<div class="resource">
<i class="fas fa-gem"></i>
<span id="gems">10</span>
</div>
<div class="resource">
<i class="fas fa-bolt"></i>
<span id="energy">100/100</span>
</div>
</div>
</div>
<div class="header-right">
<button class="btn btn-secondary" id="settingsBtn">
<i class="fas fa-cog"></i>
</button>
<button class="btn btn-secondary" id="discordBtn">
<i class="fab fa-discord"></i>
</button>
<button class="btn btn-info" id="localServerBtn" title="Local Server">
<i class="fas fa-server"></i>
</button>
<!-- <button class="btn btn-primary" id="saveBtn" title="Save Game">
<i class="fas fa-save"></i>
</button> -->
<button class="btn btn-warning" id="returnToMenuBtn">
<i class="fas fa-home"></i>
</button>
</div>
</header>
<!-- Navigation -->
<nav class="main-nav">
<button class="nav-btn active" data-tab="dashboard">
<i class="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</button>
<button class="nav-btn" data-tab="dungeons">
<i class="fas fa-dungeon"></i>
<span>Dungeons</span>
</button>
<button class="nav-btn" data-tab="skills">
<i class="fas fa-graduation-cap"></i>
<span>Skills</span>
</button>
<button class="nav-btn" data-tab="base">
<i class="fas fa-home"></i>
<span>Base</span>
</button>
<button class="nav-btn" data-tab="quests">
<i class="fas fa-scroll"></i>
<span>Quests</span>
</button>
<button class="nav-btn" data-tab="inventory">
<i class="fas fa-backpack"></i>
<span>Inventory</span>
</button>
<button class="nav-btn" data-tab="crafting">
<i class="fas fa-hammer"></i>
<span>Crafting</span>
</button>
<button class="nav-btn" data-tab="shop">
<i class="fas fa-store"></i>
<span>Shop</span>
</button>
</nav>
<!-- Main Content Area -->
<main class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="dashboard-tab">
<div class="dashboard-grid">
<div class="card">
<h3>Fleet Status</h3>
<div class="fleet-info">
<div class="ship-status">
<i class="fas fa-rocket"></i>
<div>
<p>Flagship: <span id="flagshipName">Starter Cruiser</span></p>
<p>Health: <span id="shipHealth">100%</span></p>
</div>
</div>
</div>
</div>
<div class="card">
<h3>Idle Progress</h3>
<div class="idle-stats">
<p>Offline Time: <span id="offlineTime">0h 0m</span></p>
<p>Resources Gained: <span id="offlineResources">0</span></p>
<button class="btn btn-primary" id="claimOfflineBtn">Claim Rewards</button>
</div>
<div class="stats-grid">
<div class="stat">
<span class="stat-label">Total Kills</span>
<span class="stat-value" id="totalKills">0</span>
</div>
<div class="stat">
<span class="stat-label">Dungeons Cleared</span>
<span class="stat-value" id="dungeonsCleared">0</span>
</div>
<div class="stat">
<span class="stat-label">Play Time</span>
<span class="stat-value" id="playTime">0h 0m</span>
</div>
</div>
</div>
</div>
</div>
<!-- Dungeons Tab -->
<div class="tab-content" id="dungeons-tab">
<div class="dungeons-container">
<div class="dungeon-selector">
<h2>Select Dungeon</h2>
<div class="dungeon-list" id="dungeonList">
<!-- Dungeons will be generated here -->
</div>
</div>
<div class="dungeon-view" id="dungeonView">
<div class="dungeon-placeholder">
<i class="fas fa-dungeon"></i>
<p>Select a dungeon to begin your adventure</p>
</div>
</div>
</div>
</div>
<!-- Skills Tab -->
<div class="tab-content" id="skills-tab">
<div class="skills-container">
<div class="skills-header">
<h2><i class="fas fa-graduation-cap"></i> Skills</h2>
<div class="skill-points-display">
<span class="skill-points">Skill Points: 0</span>
</div>
</div>
<div class="skill-categories">
<button class="skill-cat-btn active" data-category="combat">Combat</button>
<button class="skill-cat-btn" data-category="science">Science</button>
<button class="skill-cat-btn" data-category="crafting">Crafting</button>
</div>
<div class="skills-grid" id="skillsGrid">
<!-- Skills will be generated here -->
</div>
</div>
</div>
<!-- Base Tab -->
<div class="tab-content" id="base-tab">
<div class="base-navigation">
<button class="base-nav-btn active" data-view="overview">Base Overview</button>
<button class="base-nav-btn" data-view="visualization">Base Visualization</button>
<button class="base-nav-btn" data-view="ships">Ship Gallery</button>
<button class="base-nav-btn" data-view="starbases">Starbases</button>
</div>
<!-- Base Overview -->
<div class="base-view-content" id="base-overview">
<div class="base-container">
<div class="base-view">
<div class="base-info">
<h3>Base Information</h3>
<div class="base-stats">
<div class="stat-item">
<span class="stat-label">Power Usage:</span>
<span class="stat-value" id="basePowerUsage">0/100</span>
</div>
<div class="stat-item">
<span class="stat-label">Storage:</span>
<span class="stat-value" id="baseStorage">1000</span>
</div>
<div class="stat-item">
<span class="stat-label">Production Rate:</span>
<span class="stat-value" id="baseProduction">0/s</span>
</div>
</div>
</div>
<div class="base-rooms" id="baseRooms">
<!-- Base rooms will be generated here -->
</div>
</div>
<div class="base-upgrades">
<h3>Base Upgrades</h3>
<div class="upgrade-list" id="baseUpgrades">
<!-- Upgrades will be generated here -->
</div>
</div>
</div>
</div>
<!-- Base Visualization -->
<div class="base-view-content hidden" id="base-visualization">
<div class="base-visualization-container">
<canvas id="baseCanvas" width="800" height="600"></canvas>
<div class="base-info-overlay">
<div class="base-stats-overlay">
<h3>Base Information</h3>
<div id="baseInfoDisplay"></div>
</div>
</div>
</div>
</div>
<!-- Ship Gallery -->
<div class="base-view-content hidden" id="base-ships">
<div class="ship-gallery-container">
<h3>Your Ships</h3>
<div class="ship-layout">
<!-- Current Ship Section -->
<div class="current-ship-section">
<h4>Current Ship</h4>
<div class="current-ship-display" id="currentShipDisplay" data-debug-id="current-ship-panel">
<div class="current-ship-info">
<div class="current-ship-image">
<img src="assets/textures/ships/starter_cruiser.png" alt="Current Ship" id="currentShipImage">
</div>
<div class="current-ship-details">
<h5 id="currentShipName">Starter Cruiser</h5>
<div class="current-ship-stats">
<div class="ship-stat">
<span class="stat-label">Class:</span>
<span class="stat-value" id="currentShipClass">Light</span>
</div>
<div class="ship-stat">
<span class="stat-label">Level:</span>
<span class="stat-value" id="currentShipLevel">1</span>
</div>
<div class="ship-stat">
<span class="stat-label">Health:</span>
<span class="stat-value" id="currentShipHealth">100/100</span>
</div>
<div class="ship-stat">
<span class="stat-label">Attack:</span>
<span class="stat-value" id="currentShipAttack">10</span>
</div>
<div class="ship-stat">
<span class="stat-label">Defense:</span>
<span class="stat-value" id="currentShipDefense">5</span>
</div>
<div class="ship-stat">
<span class="stat-label">Speed:</span>
<span class="stat-value" id="currentShipSpeed">15</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Ship Grid -->
<div class="ship-grid-section">
<h4>Ships Collected</h4>
<div class="ship-grid" id="shipGrid">
<!-- Ships will be displayed here as cards -->
</div>
</div>
</div>
</div>
</div>
<!-- Starbases -->
<div class="base-view-content hidden" id="base-starbases">
<div class="starbases-container">
<div class="starbase-section">
<h3>Starbase Management</h3>
<div class="starbase-list" id="starbaseList">
<!-- Starbases will be displayed here -->
</div>
</div>
<div class="starbase-section">
<h3>Available Starbases</h3>
<div class="starbase-shop" id="starbasePurchaseList">
<div class="starbase-purchase-list" id="starbasePurchaseItems">
<!-- Available starbases for purchase -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quests Tab -->
<div class="tab-content" id="quests-tab">
<div class="quests-container">
<div class="quest-tabs">
<button class="quest-tab-btn active" data-type="main">Main Story</button>
<button class="quest-tab-btn" data-type="daily">Daily</button>
<button class="quest-tab-btn" data-type="weekly">Weekly</button>
<button class="quest-tab-btn" data-type="completed">Completed</button>
<button class="quest-tab-btn" data-type="failed">Failed Quests</button>
</div>
<div class="daily-countdown" id="dailyCountdown">Daily quests reset in: 00:00:00</div>
<div class="weekly-countdown" id="weeklyCountdown">Weekly quests reset in: 0d 00:00</div>
<div class="quest-list" id="questList">
<!-- Quests will be generated here -->
</div>
</div>
</div>
<!-- Inventory Tab -->
<div class="tab-content" id="inventory-tab">
<div class="inventory-container">
<div class="equipment-section">
<h3>Equipment</h3>
<div class="equipment-slots">
<div class="equipment-slot">
<div class="slot-label">Weapon</div>
<div class="slot-container" id="equip-weapon">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Armor</div>
<div class="slot-container" id="equip-armor">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Engine</div>
<div class="slot-container" id="equip-engine">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Shield</div>
<div class="slot-container" id="equip-shield">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Accessory</div>
<div class="slot-container" id="equip-accessory">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
</div>
</div>
<div class="inventory-main">
<div class="inventory-section">
<h3>Inventory</h3>
<div class="inventory-grid" id="inventoryGrid">
<!-- Inventory items will be generated here -->
</div>
</div>
<div class="item-details" id="itemDetails">
<p>Select an item to view details</p>
</div>
</div>
</div>
</div>
<!-- Crafting Tab -->
<div class="tab-content" id="crafting-tab">
<div class="crafting-container">
<div class="crafting-header">
<h2><i class="fas fa-hammer"></i> Crafting Station</h2>
<div class="crafting-info">
<div class="crafting-level">
<i class="fas fa-level-up-alt"></i>
<span>Crafting Level: </span>
<span id="craftingLevel">1</span>
</div>
<div class="crafting-experience">
<i class="fas fa-star"></i>
<span>Experience: </span>
<span id="craftingExp">0/100</span>
</div>
</div>
</div>
<div class="crafting-categories">
<button class="crafting-cat-btn active" data-category="weapons">Weapons</button>
<button class="crafting-cat-btn" data-category="armor">Armor</button>
<button class="crafting-cat-btn" data-category="items">Items</button>
<button class="crafting-cat-btn" data-category="ships">Ships</button>
</div>
<div class="crafting-grid" id="recipeList">
<!-- Recipes will be generated here -->
</div>
</div>
</div>
<!-- Shop Tab -->
<div class="tab-content" id="shop-tab">
<div class="shop-container">
<div class="shop-header">
<div class="shop-categories">
<button class="shop-cat-btn active" data-category="ships">Ships</button>
<button class="shop-cat-btn" data-category="weapons">Weapons</button>
<button class="shop-cat-btn" data-category="armors">Armors</button>
<!-- <button class="shop-cat-btn" data-category="upgrades">Upgrades</button> -->
<button class="shop-cat-btn" data-category="cosmetics">Cosmetics</button>
<button class="shop-cat-btn" data-category="consumables">Consumables</button>
<button class="shop-cat-btn" data-category="materials">Materials</button>
</div>
</div>
<div class="shop-content">
<div class="shop-items-container">
<div class="shop-items" id="shopItems">
<!-- Shop items will be generated here -->
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Modals -->
<div class="modal-overlay hidden" id="modalOverlay">
<div class="modal" id="modal">
<div class="modal-header">
<h3 id="modalTitle">Modal Title</h3>
<button class="modal-close" id="modalClose">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Modal content will be inserted here -->
</div>
</div>
</div>
</div>
<!-- Loading Progress Indicator -->
<div class="loading-indicator" id="loadingIndicator"></div>
<div class="loading-status hidden" id="loadingStatus">Initializing...</div>
<!-- Scripts -->
<script src="../config/xp-progression.js"></script>
<script src="js/core/DebugLogger.js"></script>
<script src="js/core/Logger.js"></script>
<script src="js/core/TextureManager.js"></script>
<script src="js/core/GameEngine.js"></script>
<script src="js/core/Player.js"></script>
<script src="js/core/Inventory.js"></script>
<script src="js/core/Economy.js"></script>
<script src="js/systems/DungeonSystem.js"></script>
<script src="js/systems/SkillSystem.js"></script>
<script src="js/systems/BaseSystem.js"></script>
<script src="js/systems/QuestSystem.js"></script>
<script src="js/systems/ShipSystem.js"></script>
<script src="js/systems/IdleSystem.js"></script>
<script src="js/systems/CraftingSystem.js"></script>
<script src="js/data/GameData.js"></script>
<script src="js/ui/UIManager.js"></script>
<script src="js/SmartSaveManager.js"></script>
<script src="js/SaveSystemIntegration.js"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="js/GameInitializer.js"></script>
<script src="js/ui/LiveMainMenu.js"></script>
<script src="js/main.js"></script>
<!-- Hidden Console Window -->
<div id="consoleWindow" class="console-window">
<div class="console-header">
<span>Developer Console</span>
<button class="console-close" onclick="toggleConsole()">×</button>
</div>
<div class="console-content">
<div id="consoleOutput" class="console-output"></div>
<div class="console-input-container">
<input type="text" id="consoleInput" class="console-input" placeholder="Type command here..." onkeypress="handleConsoleInput(event)">
</div>
</div>
</div>
</body>
</html>

View File

@ -1,418 +0,0 @@
/**
* Local Server for Singleplayer Mode
* A simplified server that runs within the Electron client for offline/singleplayer functionality
* NOTE: This version requires express, socket.io, and cors dependencies to be installed
*/
class LocalServer {
constructor() {
this.app = null;
this.server = null;
this.io = null;
this.port = null;
this.isRunning = false;
this.connectedClients = new Map();
console.log('[LOCAL SERVER] LocalServer initialized');
}
async initialize() {
try {
// Try to require dependencies
if (typeof require !== 'undefined') {
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
this.setupExpress(express, cors);
this.createServer = createServer;
this.ServerClass = Server;
console.log('[LOCAL SERVER] Dependencies loaded successfully');
return true;
} else {
console.warn('[LOCAL SERVER] require() not available, running in browser context');
return false;
}
} catch (error) {
console.error('[LOCAL SERVER] Failed to load dependencies:', error.message);
console.log('[LOCAL SERVER] Please install dependencies: npm install express socket.io cors');
return false;
}
}
setupExpress(express, cors) {
// Initialize Express app
this.app = express();
// Middleware
this.app.use(cors({
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
credentials: true
}));
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true }));
// Health check endpoint
this.app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
mode: 'local'
});
});
// API version endpoint
this.app.get('/api/ssc/version', (req, res) => {
res.status(200).json({
version: '1.0.0',
service: 'galaxystrikeonline-local-server',
timestamp: new Date().toISOString(),
mode: 'local'
});
});
// Mock authentication endpoints for singleplayer
this.app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
// Auto-authenticate for singleplayer mode
const mockUser = {
id: 'local-user',
email: email || 'local@player.com',
username: 'Local Player',
token: 'local-token-' + Date.now(),
createdAt: new Date().toISOString()
};
res.status(200).json({
success: true,
user: mockUser,
token: mockUser.token,
message: 'Logged in to local mode'
});
});
this.app.post('/api/auth/register', (req, res) => {
const { email, password, username } = req.body;
// Auto-register for singleplayer mode
const mockUser = {
id: 'local-user',
email: email || 'local@player.com',
username: username || 'Local Player',
token: 'local-token-' + Date.now(),
createdAt: new Date().toISOString()
};
res.status(201).json({
success: true,
user: mockUser,
token: mockUser.token,
message: 'Registered in local mode'
});
});
// Mock server browser endpoints
this.app.get('/api/servers', (req, res) => {
// Return a single local server
const localServer = {
id: 'local-server',
name: 'Local Singleplayer',
description: 'Your personal local server for singleplayer gaming',
type: 'private',
region: 'local',
maxPlayers: 1,
currentPlayers: 0,
owner: 'Local Player',
address: 'localhost',
port: this.port,
status: 'online',
createdAt: new Date().toISOString(),
ping: 0
};
res.status(200).json({
success: true,
servers: [localServer]
});
});
// Mock game data endpoints
this.app.get('/api/game/player/:id', (req, res) => {
// Return player data from local storage if available
const playerId = req.params.id;
let saveData;
try {
// In Electron context, access localStorage differently
if (typeof localStorage !== 'undefined') {
saveData = localStorage.getItem(`gso_save_slot_1`);
}
} catch (error) {
console.warn('[LOCAL SERVER] Could not access localStorage:', error);
}
if (saveData) {
try {
const parsedData = JSON.parse(saveData);
res.status(200).json({
success: true,
player: parsedData.player
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to parse save data'
});
}
} else {
res.status(404).json({
success: false,
error: 'No save data found'
});
}
});
this.app.post('/api/game/player/:id/save', (req, res) => {
// Save player data to local storage
const playerId = req.params.id;
const playerData = req.body;
try {
let existingSaveData = '{}';
// In Electron context, access localStorage differently
if (typeof localStorage !== 'undefined') {
existingSaveData = localStorage.getItem(`gso_save_slot_1`) || '{}';
}
const parsedExisting = JSON.parse(existingSaveData);
// Merge player data
parsedExisting.player = playerData;
parsedExisting.lastSave = Date.now();
if (typeof localStorage !== 'undefined') {
localStorage.setItem(`gso_save_slot_1`, JSON.stringify(parsedExisting));
}
res.status(200).json({
success: true,
message: 'Player data saved locally'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to save player data'
});
}
});
}
async start(port = 3004) {
if (this.isRunning) {
console.log('[LOCAL SERVER] Server is already running on port', this.port);
return { success: false, error: 'Server already running' };
}
try {
// Initialize dependencies if not already done
if (!this.app) {
const initialized = await this.initialize();
if (!initialized) {
return { success: false, error: 'Failed to initialize server dependencies' };
}
}
this.port = port;
this.server = this.createServer(this.app);
this.io = new this.ServerClass(this.server, {
cors: {
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
methods: ["GET", "POST"]
}
});
// Setup Socket.IO handlers
this.setupSocketHandlers();
// Start the server
await new Promise((resolve, reject) => {
this.server.listen(port, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
this.isRunning = true;
console.log(`[LOCAL SERVER] Local server started on port ${port}`);
return {
success: true,
port: port,
url: `http://localhost:${port}`
};
} catch (error) {
console.error('[LOCAL SERVER] Failed to start server:', error);
return { success: false, error: error.message };
}
}
setupSocketHandlers() {
this.io.on('connection', (socket) => {
console.log('[LOCAL SERVER] Client connected:', socket.id);
this.connectedClients.set(socket.id, {
connectedAt: Date.now(),
playerData: null
});
// Handle authentication
socket.on('authenticate', (data) => {
console.log('[LOCAL SERVER] Authenticating client:', socket.id, data);
// Auto-authenticate for local mode
socket.emit('authenticated', {
success: true,
user: {
id: 'local-user',
username: 'Local Player',
token: 'local-token-' + Date.now()
}
});
// Update client info
const clientInfo = this.connectedClients.get(socket.id);
if (clientInfo) {
clientInfo.playerData = {
id: 'local-user',
username: 'Local Player'
};
}
});
// Handle game data sync
socket.on('saveGameData', (data) => {
console.log('[LOCAL SERVER] Saving game data for:', socket.id);
// Save to localStorage (this will be handled by the client-side save system)
socket.emit('gameDataSaved', {
success: true,
timestamp: Date.now()
});
});
socket.on('loadGameData', (data) => {
console.log('[LOCAL SERVER] Loading game data for:', socket.id);
// Load from localStorage (this will be handled by the client-side load system)
let saveData;
try {
if (typeof localStorage !== 'undefined') {
saveData = localStorage.getItem(`gso_save_slot_1`);
}
} catch (error) {
console.warn('[LOCAL SERVER] Could not access localStorage:', error);
}
if (saveData) {
try {
const parsedData = JSON.parse(saveData);
socket.emit('gameDataLoaded', {
success: true,
data: parsedData
});
} catch (error) {
socket.emit('gameDataLoaded', {
success: false,
error: 'Failed to parse save data'
});
}
} else {
socket.emit('gameDataLoaded', {
success: false,
error: 'No save data found'
});
}
});
// Handle disconnection
socket.on('disconnect', () => {
console.log('[LOCAL SERVER] Client disconnected:', socket.id);
this.connectedClients.delete(socket.id);
});
// Send welcome message
socket.emit('welcome', {
message: 'Connected to local server',
serverInfo: {
mode: 'local',
port: this.port,
timestamp: new Date().toISOString()
}
});
});
}
async stop() {
if (!this.isRunning) {
console.log('[LOCAL SERVER] Server is not running');
return { success: false, error: 'Server is not running' };
}
try {
// Disconnect all clients
if (this.io) {
this.io.disconnectSockets();
}
// Close the server
if (this.server) {
await new Promise((resolve) => {
this.server.close(resolve);
});
}
this.isRunning = false;
this.port = null;
this.connectedClients.clear();
console.log('[LOCAL SERVER] Local server stopped');
return { success: true };
} catch (error) {
console.error('[LOCAL SERVER] Failed to stop server:', error);
return { success: false, error: error.message };
}
}
getStatus() {
return {
isRunning: this.isRunning,
port: this.port,
connectedClients: this.connectedClients.size,
uptime: this.isRunning ? process.uptime() : 0
};
}
getUrl() {
return this.isRunning ? `http://localhost:${this.port}` : null;
}
}
// Export for use in Node.js environment
if (typeof module !== 'undefined' && module.exports) {
module.exports = LocalServer;
}
// Export for use in browser environment
if (typeof window !== 'undefined') {
window.LocalServer = LocalServer;
}

View File

@ -1,224 +0,0 @@
/**
* Local Server Manager
* Manages the local server for singleplayer mode within the Electron client
*/
class LocalServerManager {
constructor() {
this.localServer = null;
this.isRunning = false;
this.port = 3004;
this.startupAttempts = 0;
this.maxStartupAttempts = 3;
console.log('[LOCAL SERVER MANAGER] LocalServerManager initialized');
}
async initialize() {
console.log('[LOCAL SERVER MANAGER] Initializing local server...');
try {
// In Electron renderer context, use SimpleLocalServer which doesn't require Node.js modules
if (typeof window !== 'undefined' && window.SimpleLocalServer) {
this.localServer = new window.SimpleLocalServer();
console.log('[LOCAL SERVER MANAGER] SimpleLocalServer class loaded from window');
return true;
} else if (typeof window !== 'undefined' && window.LocalServer) {
// Fallback to original LocalServer if available
this.localServer = new window.LocalServer();
console.log('[LOCAL SERVER MANAGER] LocalServer class loaded from window');
return true;
} else {
console.warn('[LOCAL SERVER MANAGER] No local server class available');
return false;
}
} catch (error) {
console.error('[LOCAL SERVER MANAGER] Failed to initialize local server:', error);
console.log('[LOCAL SERVER MANAGER] Please ensure SimpleLocalServer.js is loaded properly');
return false;
}
}
async startServer() {
if (this.isRunning) {
console.log('[LOCAL SERVER MANAGER] Server is already running');
return { success: true, port: this.port };
}
if (!this.localServer) {
const initialized = await this.initialize();
if (!initialized) {
return { success: false, error: 'Failed to initialize server' };
}
}
console.log(`[LOCAL SERVER MANAGER] Attempting to start server on port ${this.port}`);
try {
const result = await this.localServer.start(this.port);
if (result.success) {
this.isRunning = true;
this.port = result.port;
this.startupAttempts = 0;
console.log(`[LOCAL SERVER MANAGER] Server started successfully on port ${this.port}`);
console.log(`[LOCAL SERVER MANAGER] Server URL: ${result.url}`);
// Update LiveMainMenu to use local server
this.updateMainMenuForLocalMode();
return result;
} else {
console.error('[LOCAL SERVER MANAGER] Failed to start server:', result.error);
this.startupAttempts++;
// Try alternative ports if first attempt fails
if (this.startupAttempts < this.maxStartupAttempts) {
this.port = 3004 + this.startupAttempts;
console.log(`[LOCAL SERVER MANAGER] Retrying with port ${this.port}`);
return await this.startServer();
}
return result;
}
} catch (error) {
console.error('[LOCAL SERVER MANAGER] Exception starting server:', error);
return { success: false, error: error.message };
}
}
async stopServer() {
if (!this.isRunning || !this.localServer) {
console.log('[LOCAL SERVER MANAGER] Server is not running');
return { success: true };
}
console.log('[LOCAL SERVER MANAGER] Stopping local server...');
try {
const result = await this.localServer.stop();
if (result.success) {
this.isRunning = false;
this.port = 3004;
console.log('[LOCAL SERVER MANAGER] Server stopped successfully');
} else {
console.error('[LOCAL SERVER MANAGER] Failed to stop server:', result.error);
}
return result;
} catch (error) {
console.error('[LOCAL SERVER MANAGER] Exception stopping server:', error);
return { success: false, error: error.message };
}
}
getStatus() {
if (!this.localServer) {
return {
isRunning: false,
port: null,
connectedClients: 0,
uptime: 0
};
}
const status = this.localServer.getStatus();
return {
...status,
url: this.isRunning ? `http://localhost:${status.port}` : null
};
}
getServerUrl() {
return this.isRunning ? `http://localhost:${this.port}` : null;
}
updateMainMenuForLocalMode() {
// Update LiveMainMenu to use local server URLs
if (window.liveMainMenu) {
console.log('[LOCAL SERVER MANAGER] Updating LiveMainMenu for local mode');
window.liveMainMenu.apiBaseUrl = `http://localhost:${this.port}/api`;
window.liveMainMenu.gameServerUrl = `http://localhost:${this.port}`;
window.liveMainMenu.isLocalMode = true;
console.log(`[LOCAL SERVER MANAGER] Updated API URL to: ${window.liveMainMenu.apiBaseUrl}`);
console.log(`[LOCAL SERVER MANAGER] Updated Game Server URL to: ${window.liveMainMenu.gameServerUrl}`);
// Also update GameInitializer URLs
if (window.gameInitializer) {
window.gameInitializer.updateServerUrls(
`http://localhost:${this.port}/api`,
`http://localhost:${this.port}`
);
console.log('[LOCAL SERVER MANAGER] Updated GameInitializer URLs for local mode');
} else {
console.warn('[LOCAL SERVER MANAGER] GameInitializer not available for URL update');
}
} else {
console.warn('[LOCAL SERVER MANAGER] LiveMainMenu not available for update');
}
}
// Auto-start server when in singleplayer mode
async autoStartIfSingleplayer() {
// Check if we should auto-start (no external server available)
try {
// Try to connect to external server first
const response = await fetch('https://api.korvarix.com/health', {
method: 'GET',
timeout: 3000
});
if (response.ok) {
console.log('[LOCAL SERVER MANAGER] External server available, not starting local server');
return { success: false, reason: 'External server available' };
}
} catch (error) {
console.log('[LOCAL SERVER MANAGER] External server not available, starting local server');
}
// Start local server
return await this.startServer();
}
// Handle server errors and restart if needed
handleServerError(error) {
console.error('[LOCAL SERVER MANAGER] Server error:', error);
// Try to restart server if it crashes
if (this.isRunning) {
console.log('[LOCAL SERVER MANAGER] Attempting to restart server...');
this.stopServer().then(() => {
setTimeout(() => {
this.startServer();
}, 2000); // Wait 2 seconds before restarting
});
}
}
// Get local server info for UI display
getServerInfo() {
return {
isRunning: this.isRunning,
port: this.port,
url: this.getServerUrl(),
status: this.isRunning ? 'Online' : 'Offline',
mode: 'Local Singleplayer',
connectedClients: this.localServer ? this.localServer.connectedClients.size : 0,
uptime: this.localServer ? this.localServer.uptime : 0
};
}
}
// Create global instance
window.localServerManager = new LocalServerManager();
// Auto-export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = LocalServerManager;
}
console.log('[LOCAL SERVER MANAGER] LocalServerManager loaded and global instance created');

View File

@ -1,535 +0,0 @@
/**
* Simple Local Server for Singleplayer Mode
* A mock server that simulates server responses without requiring Node.js dependencies
* This runs entirely in the browser/renderer context
*/
class SimpleLocalServer {
constructor() {
this.isRunning = false;
this.port = 3004;
this.connectedClients = new Map();
this.existingSaveData = null;
// Check for existing save data on initialization
this.loadExistingSaveData();
this.mockData = {
servers: [{
id: 'local-server',
name: 'Local Singleplayer',
description: 'Your personal local server for singleplayer gaming',
type: 'private',
region: 'local',
maxPlayers: 1,
currentPlayers: 0,
owner: 'Local Player',
address: 'localhost',
port: this.port,
status: 'online',
createdAt: new Date().toISOString(),
ping: 0
}],
user: {
id: 'local-user',
email: 'local@player.com',
username: 'Local Player',
token: 'local-token-' + Date.now(),
createdAt: new Date().toISOString()
}
};
console.log('[SIMPLE LOCAL SERVER] SimpleLocalServer initialized');
console.log('[SIMPLE LOCAL SERVER] Existing save data found:', !!this.existingSaveData);
}
// Mock Socket.IO server for local mode
createMockSocket() {
console.log('[SIMPLE LOCAL SERVER] Creating mock Socket.IO connection');
const mockSocket = {
connected: false,
eventHandlers: {},
on: function(event, handler) {
console.log(`[MOCKET SOCKET] Registering event handler for: ${event}`);
if (!this.eventHandlers[event]) {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(handler);
},
emit: function(event, data) {
console.log(`[MOCKET SOCKET] Emitting event: ${event}`, data);
},
connect: function() {
console.log('[MOCKET SOCKET] Connecting...');
this.connected = true;
// Simulate successful connection
setTimeout(() => {
if (this.eventHandlers['connect']) {
this.eventHandlers['connect'].forEach(handler => handler());
}
}, 100);
},
disconnect: function() {
console.log('[MOCKET SOCKET] Disconnecting...');
this.connected = false;
if (this.eventHandlers['disconnect']) {
this.eventHandlers['disconnect'].forEach(handler => handler());
}
}
};
// Auto-connect
mockSocket.connect();
return mockSocket;
}
loadExistingSaveData() {
try {
const saveData = localStorage.getItem(`gso_save_slot_1`);
if (saveData) {
this.existingSaveData = JSON.parse(saveData);
console.log('[SIMPLE LOCAL SERVER] Loaded existing save data:', {
hasPlayerData: !!this.existingSaveData.player,
playerLevel: this.existingSaveData.player?.stats?.level,
lastSave: this.existingSaveData.lastSave,
gameTime: this.existingSaveData.gameTime
});
} else {
console.log('[SIMPLE LOCAL SERVER] No existing save data found');
}
} catch (error) {
console.warn('[SIMPLE LOCAL SERVER] Error loading existing save data:', error);
this.existingSaveData = null;
}
}
applyExistingSaveDataToGame() {
if (!this.existingSaveData || !window.game) {
console.log('[SIMPLE LOCAL SERVER] No existing save data or game not available');
return false;
}
try {
console.log('[SIMPLE LOCAL SERVER] Applying existing save data to game...');
// Apply save data to game systems
if (this.existingSaveData.player && window.game.systems.player) {
window.game.systems.player.load(this.existingSaveData.player);
console.log('[SIMPLE LOCAL SERVER] Player data applied');
}
if (this.existingSaveData.inventory && window.game.systems.inventory) {
window.game.systems.inventory.load(this.existingSaveData.inventory);
console.log('[SIMPLE LOCAL SERVER] Inventory data applied');
}
if (this.existingSaveData.economy && window.game.systems.economy) {
window.game.systems.economy.load(this.existingSaveData.economy);
console.log('[SIMPLE LOCAL SERVER] Economy data applied');
}
if (this.existingSaveData.idleSystem && window.game.systems.idleSystem) {
window.game.systems.idleSystem.load(this.existingSaveData.idleSystem);
console.log('[SIMPLE LOCAL SERVER] Idle system data applied');
}
if (this.existingSaveData.dungeonSystem && window.game.systems.dungeonSystem) {
window.game.systems.dungeonSystem.load(this.existingSaveData.dungeonSystem);
console.log('[SIMPLE LOCAL SERVER] Dungeon system data applied');
}
if (this.existingSaveData.skillSystem && window.game.systems.skillSystem) {
window.game.systems.skillSystem.load(this.existingSaveData.skillSystem);
console.log('[SIMPLE LOCAL SERVER] Skill system data applied');
}
if (this.existingSaveData.baseSystem && window.game.systems.baseSystem) {
window.game.systems.baseSystem.load(this.existingSaveData.baseSystem);
console.log('[SIMPLE LOCAL SERVER] Base system data applied');
}
if (this.existingSaveData.questSystem && window.game.systems.questSystem) {
window.game.systems.questSystem.load(this.existingSaveData.questSystem);
console.log('[SIMPLE LOCAL SERVER] Quest system data applied');
}
if (this.existingSaveData.gameTime && window.game) {
window.game.gameTime = this.existingSaveData.gameTime;
console.log('[SIMPLE LOCAL SERVER] Game time applied:', this.existingSaveData.gameTime);
}
console.log('[SIMPLE LOCAL SERVER] All save data applied successfully');
return true;
} catch (error) {
console.error('[SIMPLE LOCAL SERVER] Error applying save data to game:', error);
return false;
}
}
async start(port = 3004) {
if (this.isRunning) {
console.log('[SIMPLE LOCAL SERVER] Server is already running on port', this.port);
return { success: false, error: 'Server already running' };
}
try {
this.port = port;
this.isRunning = true;
// Update mock server data with actual port
this.mockData.servers[0].port = port;
console.log(`[SIMPLE LOCAL SERVER] Mock local server started on port ${port}`);
return {
success: true,
port: port,
url: `http://localhost:${port}`
};
} catch (error) {
console.error('[SIMPLE LOCAL SERVER] Failed to start server:', error);
return { success: false, error: error.message };
}
}
async stop() {
if (!this.isRunning) {
console.log('[SIMPLE LOCAL SERVER] Server is not running');
return { success: false, error: 'Server is not running' };
}
try {
this.isRunning = false;
this.connectedClients.clear();
console.log('[SIMPLE LOCAL SERVER] Mock local server stopped');
return { success: true };
} catch (error) {
console.error('[SIMPLE LOCAL SERVER] Failed to stop server:', error);
return { success: false, error: error.message };
}
}
getStatus() {
return {
isRunning: this.isRunning,
port: this.port,
connectedClients: this.connectedClients.size,
uptime: this.isRunning ? 0 : 0 // Mock uptime
};
}
getUrl() {
return this.isRunning ? `http://localhost:${this.port}` : null;
}
// Mock API methods that simulate server responses
async mockRequest(method, url, data = null) {
console.log(`[SIMPLE LOCAL SERVER] Mock ${method} ${url}`, data);
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100));
try {
if (url === '/health') {
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: 0,
mode: 'local'
}),
text: () => Promise.resolve(JSON.stringify({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: 0,
mode: 'local'
}))
};
}
if (url === '/api/ssc/version') {
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
version: '1.0.0',
service: 'galaxystrikeonline-local-server',
timestamp: new Date().toISOString(),
mode: 'local'
}),
text: () => Promise.resolve(JSON.stringify({
version: '1.0.0',
service: 'galaxystrikeonline-local-server',
timestamp: new Date().toISOString(),
mode: 'local'
}))
};
}
if (url === '/api/auth/login' && method === 'POST') {
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
user: this.mockData.user,
token: this.mockData.user.token,
message: 'Logged in to local mode'
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
user: this.mockData.user,
token: this.mockData.user.token,
message: 'Logged in to local mode'
}))
};
}
if (url === '/api/auth/register' && method === 'POST') {
return {
status: 201,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
user: this.mockData.user,
token: this.mockData.user.token,
message: 'Registered in local mode'
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
user: this.mockData.user,
token: this.mockData.user.token,
message: 'Registered in local mode'
}))
};
}
if (url === '/api/servers') {
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
servers: this.mockData.servers
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
servers: this.mockData.servers
}))
};
}
if (url.startsWith('/servers/') && url.endsWith('/join') && method === 'POST') {
// Mock server join response
const serverId = url.split('/')[2];
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
server: {
id: serverId,
name: 'Local Singleplayer',
address: 'localhost',
port: this.port,
gamePort: this.port + 1,
maxPlayers: 1,
currentPlayers: 1,
status: 'online'
},
message: 'Joined server successfully'
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
server: {
id: serverId,
name: 'Local Singleplayer',
address: 'localhost',
port: this.port,
gamePort: this.port + 1,
maxPlayers: 1,
currentPlayers: 1,
status: 'online'
},
message: 'Joined server successfully'
}))
};
}
if (url.startsWith('/api/game/player/') && method === 'GET') {
// Return player data from existing save data or localStorage
let saveData = this.existingSaveData;
// If no existing save data, try localStorage
if (!saveData) {
try {
const localStorageData = localStorage.getItem(`gso_save_slot_1`);
if (localStorageData) {
saveData = JSON.parse(localStorageData);
}
} catch (error) {
console.warn('[SIMPLE LOCAL SERVER] Could not access localStorage:', error);
}
}
if (saveData) {
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
player: saveData.player
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
player: saveData.player
}))
};
} else {
return {
status: 404,
ok: false,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: false,
error: 'No save data found'
}),
text: () => Promise.resolve(JSON.stringify({
success: false,
error: 'No save data found'
}))
};
}
}
if (url.startsWith('/api/game/player/') && method === 'POST') {
// Save player data to localStorage
try {
let existingSaveData = '{}';
if (typeof localStorage !== 'undefined') {
existingSaveData = localStorage.getItem(`gso_save_slot_1`) || '{}';
}
const parsedExisting = JSON.parse(existingSaveData);
parsedExisting.player = data;
parsedExisting.lastSave = Date.now();
if (typeof localStorage !== 'undefined') {
localStorage.setItem(`gso_save_slot_1`, JSON.stringify(parsedExisting));
}
return {
status: 200,
ok: true,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: true,
message: 'Player data saved locally'
}),
text: () => Promise.resolve(JSON.stringify({
success: true,
message: 'Player data saved locally'
}))
};
} catch (error) {
return {
status: 500,
ok: false,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: false,
error: 'Failed to save player data'
}),
text: () => Promise.resolve(JSON.stringify({
success: false,
error: 'Failed to save player data'
}))
};
}
}
// Default response for unknown endpoints
return {
status: 404,
ok: false,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: false,
error: 'Endpoint not found'
}),
text: () => Promise.resolve(JSON.stringify({
success: false,
error: 'Endpoint not found'
}))
};
} catch (error) {
console.error('[SIMPLE LOCAL SERVER] Mock request error:', error);
return {
status: 500,
ok: false,
headers: {
get: (name) => name === 'content-type' ? 'application/json' : null
},
json: () => Promise.resolve({
success: false,
error: 'Internal server error'
}),
text: () => Promise.resolve(JSON.stringify({
success: false,
error: 'Internal server error'
}))
};
}
}
}
// Export for use in browser environment
if (typeof window !== 'undefined') {
window.SimpleLocalServer = SimpleLocalServer;
}
console.log('[SIMPLE LOCAL SERVER] SimpleLocalServer loaded and exported to window');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,570 +0,0 @@
/**
* Galaxy Strike Online - Game Data
* Static game data, constants, and configuration
*/
// Game configuration
const GAME_CONFIG = {
version: '1.0.0',
name: 'Galaxy Strike Online',
maxLevel: 100,
saveInterval: 30000, // 30 seconds
notificationDuration: 3000,
maxNotifications: 5
};
// Player defaults
const PLAYER_DEFAULTS = {
level: 1,
experience: 0,
skillPoints: 0,
credits: 1000,
gems: 10,
health: 100,
maxHealth: 100,
energy: 100,
maxEnergy: 100,
attack: 10,
defense: 5,
speed: 10,
criticalChance: 0.05,
criticalDamage: 1.5
};
// Experience requirements
const EXPERIENCE_TABLE = [];
for (let i = 1; i <= 100; i++) {
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
}
// Item rarities with colors and multipliers
const ITEM_RARITIES = {
common: {
name: 'Common',
color: '#888888',
multiplier: 1.0,
dropChance: 0.60
},
uncommon: {
name: 'Uncommon',
color: '#00ff00',
multiplier: 1.2,
dropChance: 0.25
},
rare: {
name: 'Rare',
color: '#0088ff',
multiplier: 1.5,
dropChance: 0.10
},
epic: {
name: 'Epic',
color: '#8833ff',
multiplier: 2.0,
dropChance: 0.04
},
legendary: {
name: 'Legendary',
color: '#ff8800',
multiplier: 3.0,
dropChance: 0.01
}
};
// Enemy types and stats
const ENEMY_TEMPLATES = {
space_pirate: {
name: 'Space Pirate',
health: 25,
attack: 10,
defense: 3,
speed: 8,
experience: 15,
credits: 12,
rarity: 'common'
},
alien_guardian: {
name: 'Alien Guardian',
health: 50,
attack: 8,
defense: 5,
speed: 6,
experience: 25,
credits: 15,
rarity: 'common'
},
mining_drone: {
name: 'Mining Drone',
health: 20,
attack: 8,
defense: 3,
speed: 5,
experience: 12,
credits: 8,
rarity: 'common'
},
security_drone: {
name: 'Security Drone',
health: 35,
attack: 14,
defense: 4,
speed: 10,
experience: 22,
credits: 15,
rarity: 'uncommon'
},
pirate_captain: {
name: 'Pirate Captain',
health: 40,
attack: 15,
defense: 6,
speed: 12,
experience: 30,
credits: 20,
rarity: 'uncommon'
},
crystal_golem: {
name: 'Crystal Golem',
health: 80,
attack: 6,
defense: 10,
speed: 4,
experience: 35,
credits: 25,
rarity: 'rare'
},
corrupted_ai: {
name: 'Corrupted AI',
health: 60,
attack: 20,
defense: 2,
speed: 15,
experience: 40,
credits: 30,
rarity: 'rare'
},
energy_being: {
name: 'Energy Being',
health: 55,
attack: 22,
defense: 3,
speed: 18,
experience: 45,
credits: 35,
rarity: 'epic'
},
quantum_entity: {
name: 'Quantum Entity',
health: 70,
attack: 35,
defense: 5,
speed: 20,
experience: 60,
credits: 50,
rarity: 'legendary'
}
};
// Dungeon configurations
const DUNGEON_CONFIGS = {
alien_ruins: {
name: 'Alien Ruins',
description: 'Ancient alien structures filled with mysterious technology',
difficulty: 'medium',
minLevel: 3,
roomCount: [5, 8],
enemyTypes: ['alien_guardian', 'ancient_drone', 'crystal_golem'],
rewardMultiplier: 1.2,
energyCost: 20
},
pirate_lair: {
name: 'Pirate Lair',
description: 'Dangerous pirate hideouts with valuable loot',
difficulty: 'easy',
minLevel: 1,
roomCount: [4, 6],
enemyTypes: ['space_pirate', 'pirate_captain', 'defense_turret'],
rewardMultiplier: 1.0,
energyCost: 15
},
corrupted_vault: {
name: 'Corrupted AI Vault',
description: 'Malfunctioning AI facilities with corrupted security',
difficulty: 'hard',
minLevel: 5,
roomCount: [6, 9],
enemyTypes: ['security_drone', 'corrupted_ai', 'virus_program'],
rewardMultiplier: 1.5,
energyCost: 25
},
asteroid_mine: {
name: 'Asteroid Mine',
description: 'Abandoned mining facilities in asteroid fields',
difficulty: 'easy',
minLevel: 2,
roomCount: [4, 7],
enemyTypes: ['mining_drone', 'rock_creature', 'explosive_asteroid'],
rewardMultiplier: 0.8,
energyCost: 10
},
nebula_anomaly: {
name: 'Nebula Anomaly',
description: 'Strange energy anomalies in deep space',
difficulty: 'extreme',
minLevel: 8,
roomCount: [7, 10],
enemyTypes: ['energy_being', 'phase_shifter', 'quantum_entity'],
rewardMultiplier: 2.0,
energyCost: 30
}
};
// Skill definitions
const SKILL_DEFINITIONS = {
combat: {
weapons_mastery: {
name: 'Weapons Mastery',
description: 'Increases weapon damage and critical chance',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
attack: 2,
criticalChance: 0.01
},
icon: 'fa-sword'
},
shield_techniques: {
name: 'Shield Techniques',
description: 'Improves defense and energy efficiency',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
defense: 2,
maxEnergy: 5
},
icon: 'fa-shield-alt'
},
piloting: {
name: 'Piloting',
description: 'Enhances speed and evasion',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
speed: 2,
criticalChance: 0.005
},
icon: 'fa-rocket'
}
},
science: {
energy_manipulation: {
name: 'Energy Manipulation',
description: 'Better energy control and regeneration',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
maxEnergy: 10,
energyRegeneration: 0.1
},
icon: 'fa-bolt'
},
alien_technology: {
name: 'Alien Technology',
description: 'Understanding and using alien artifacts',
maxLevel: 10,
experiencePerLevel: 150,
effects: {
findRarity: 0.05,
itemValue: 0.1
},
icon: 'fa-atom'
}
},
crafting: {
weapon_crafting: {
name: 'Weapon Crafting',
description: 'Create and upgrade weapons',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
craftingBonus: 0.1,
weaponStats: 0.05
},
icon: 'fa-hammer'
},
armor_forging: {
name: 'Armor Forging',
description: 'Forge protective armor and shields',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
craftingBonus: 0.1,
armorStats: 0.05
},
icon: 'fa-anvil'
}
}
};
// Shop items
const SHOP_ITEMS = {
ships: [
{
id: 'fighter_mk1',
name: 'Fighter Mk. I',
type: 'ship',
rarity: 'common',
price: 5000,
currency: 'credits',
description: 'Fast and agile fighter ship',
stats: { attack: 15, speed: 20, defense: 8 }
},
{
id: 'cruiser_mk1',
name: 'Cruiser Mk. I',
type: 'ship',
rarity: 'uncommon',
price: 15000,
currency: 'credits',
description: 'Well-balanced cruiser for combat',
stats: { attack: 20, speed: 10, defense: 15 }
}
],
upgrades: [
{
id: 'weapon_upgrade_1',
name: 'Weapon Upgrade I',
type: 'upgrade',
rarity: 'common',
price: 500,
currency: 'credits',
description: 'Increases weapon damage by 10%',
effect: { attackMultiplier: 1.1 }
},
{
id: 'shield_upgrade_1',
name: 'Shield Upgrade I',
type: 'upgrade',
rarity: 'common',
price: 400,
currency: 'credits',
description: 'Increases defense by 5 points',
effect: { defense: 5 }
}
],
cosmetics: [
{
id: 'blue_paint',
name: 'Blue Paint Job',
type: 'cosmetic',
rarity: 'common',
price: 100,
currency: 'gems',
description: 'Custom blue paint for your ship'
},
{
id: 'golden_trim',
name: 'Golden Trim',
type: 'cosmetic',
rarity: 'rare',
price: 500,
currency: 'gems',
description: 'Luxurious golden trim for your ship'
}
],
consumables: [
{
id: 'mega_health_kit',
name: 'Mega Health Kit',
type: 'consumable',
rarity: 'uncommon',
price: 50,
currency: 'credits',
description: 'Restores full health',
effect: { heal: 999 }
},
{
id: 'energy_boost',
name: 'Energy Boost',
type: 'consumable',
rarity: 'common',
price: 25,
currency: 'credits',
description: 'Restores 50 energy',
effect: { energy: 50 }
}
]
};
// Starter equipment for new players
const STARTER_EQUIPMENT = {
starter_blaster: {
id: 'starter_blaster',
name: 'Common Blaster',
type: 'weapon',
rarity: 'common',
description: 'A reliable basic blaster for new pilots',
stats: { attack: 5, criticalChance: 0.02 },
equipable: true,
slot: 'weapon',
value: 100,
stackable: false
},
basic_armor: {
id: 'basic_armor',
name: 'Basic Armor',
type: 'armor',
rarity: 'common',
description: 'Standard issue armor for basic protection',
stats: { defense: 3, health: 10 },
equipable: true,
slot: 'armor',
value: 150,
stackable: false
}
};
// Achievement definitions
const ACHIEVEMENTS = {
first_victory: {
name: 'First Victory',
description: 'Win your first dungeon',
requirement: { dungeonsCompleted: 1 },
reward: { gems: 10, experience: 100 },
icon: 'fa-trophy'
},
dungeon_master: {
name: 'Dungeon Master',
description: 'Complete 50 dungeons',
requirement: { dungeonsCompleted: 50 },
reward: { gems: 100, experience: 1000 },
icon: 'fa-dungeon'
},
level_master: {
name: 'Level Master',
description: 'Reach level 50',
requirement: { level: 50 },
reward: { gems: 200, experience: 5000 },
icon: 'fa-level-up-alt'
},
wealthy_commander: {
name: 'Wealthy Commander',
description: 'Accumulate 1,000,000 credits',
requirement: { credits: 1000000 },
reward: { gems: 150, experience: 2000 },
icon: 'fa-coins'
},
skill_expert: {
name: 'Skill Expert',
description: 'Max out any skill',
requirement: { maxSkillLevel: 10 },
reward: { gems: 75, experience: 1500 },
icon: 'fa-graduation-cap'
}
};
// Game messages and notifications
const GAME_MESSAGES = {
welcome: 'Welcome to Galaxy Strike Online, Commander!',
levelUp: 'Level Up! You are now level {level}!',
questCompleted: 'Quest completed: {questName}!',
dungeonCompleted: 'Dungeon completed! Time: {time}',
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
insufficientCredits: 'Not enough credits!',
insufficientGems: 'Not enough gems!',
insufficientEnergy: 'Not enough energy!',
inventoryFull: 'Inventory is full!',
skillPointsAvailable: 'You have {points} skill points available!',
dailyReward: 'Daily reward claimed! Day {day}',
offlineRewards: 'Welcome back! You were offline for {time}'
};
// Utility functions
const GameUtils = {
// Get random item from array
getRandomItem(array) {
return array[Math.floor(Math.random() * array.length)];
},
// Get random integer between min and max (inclusive)
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
// Get random float between min and max
getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
},
// Check if chance succeeds
checkChance(chance) {
return Math.random() < chance;
},
// Format large numbers with suffixes
formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return Math.floor(num).toString();
},
// Format time in milliseconds to readable string
formatTime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
},
// Calculate experience needed for level
getExperienceForLevel(level) {
return EXPERIENCE_TABLE[level] || 0;
},
// Get item rarity by chance
getItemRarity() {
const roll = Math.random();
let cumulative = 0;
for (const [rarity, data] of Object.entries(ITEM_RARITIES)) {
cumulative += data.dropChance;
if (roll <= cumulative) {
return rarity;
}
}
return 'common';
},
// Deep clone object
deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
},
// Generate unique ID
generateId() {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
}
};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
GAME_CONFIG,
PLAYER_DEFAULTS,
EXPERIENCE_TABLE,
ITEM_RARITIES,
ENEMY_TEMPLATES,
DUNGEON_CONFIGS,
SKILL_DEFINITIONS,
SHOP_ITEMS,
ACHIEVEMENTS,
GAME_MESSAGES,
GameUtils
};
}

View File

@ -1,657 +0,0 @@
/**
* Galaxy Strike Online - Crafting System
* Handles item crafting, recipes, and crafting skill progression
*/
class CraftingSystem extends BaseSystem {
constructor(gameEngine) {
super(gameEngine);
this.recipes = new Map();
this.currentCategory = 'weapons';
this.selectedRecipe = null;
this.initializeRecipes();
}
initializeRecipes() {
// Weapon Recipes
this.addRecipe('basic_blaster', {
name: 'Basic Blaster',
category: 'weapons',
description: 'A simple energy blaster for beginners',
requirements: {
weapon_crafting: 1,
crafting: 1
},
materials: [
{ id: 'iron_ore', quantity: 5 },
{ id: 'energy_crystal', quantity: 2 }
],
results: [
{ id: 'basic_blaster', quantity: 1 }
],
experience: 10,
craftingTime: 3000 // 3 seconds
});
this.addRecipe('enhanced_blaster', {
name: 'Enhanced Blaster',
category: 'weapons',
description: 'An improved blaster with better damage output',
requirements: {
weapon_crafting: 3,
crafting: 5
},
materials: [
{ id: 'iron_ore', quantity: 10 },
{ id: 'energy_crystal', quantity: 5 },
{ id: 'copper_wire', quantity: 3 }
],
results: [
{ id: 'enhanced_blaster', quantity: 1 }
],
experience: 25,
craftingTime: 5000 // 5 seconds
});
this.addRecipe('laser_sniper_rifle', {
name: 'Laser Sniper Rifle',
category: 'weapons',
description: 'A long-range precision laser weapon',
requirements: {
weapon_crafting: 3,
crafting: 5
},
materials: [
{ id: 'advanced_circuitboard', quantity: 2 },
{ id: 'energy_crystal', quantity: 8 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'copper_wire', quantity: 4 }
],
results: [
{ id: 'laser_sniper_rifle', quantity: 1 }
],
experience: 40,
craftingTime: 8000 // 8 seconds
});
this.addRecipe('plasma_cannon', {
name: 'Plasma Cannon',
category: 'weapons',
description: 'A devastating plasma-based weapon',
requirements: {
weapon_crafting: 5,
crafting: 7
},
materials: [
{ id: 'advanced_components', quantity: 3 },
{ id: 'energy_crystal', quantity: 12 },
{ id: 'steel_plate', quantity: 8 },
{ id: 'battery', quantity: 3 }
],
results: [
{ id: 'plasma_cannon', quantity: 1 }
],
experience: 60,
craftingTime: 12000 // 12 seconds
});
// Armor Recipes
this.addRecipe('basic_armor', {
name: 'Basic Armor',
category: 'armor',
description: 'Light armor providing basic protection',
requirements: {
armor_forging: 1,
crafting: 1
},
materials: [
{ id: 'iron_ore', quantity: 8 },
{ id: 'leather', quantity: 3 }
],
results: [
{ id: 'basic_armor', quantity: 1 }
],
experience: 15,
craftingTime: 4000 // 4 seconds
});
this.addRecipe('reinforced_armor', {
name: 'Reinforced Armor',
category: 'armor',
description: 'Heavy armor with enhanced protection',
requirements: {
armor_forging: 4,
crafting: 6
},
materials: [
{ id: 'iron_ore', quantity: 15 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'leather', quantity: 5 }
],
results: [
{ id: 'reinforced_armor', quantity: 1 }
],
experience: 35,
craftingTime: 6000 // 6 seconds
});
// Item Recipes
this.addRecipe('health_kit', {
name: 'Health Kit',
category: 'items',
description: 'A medical kit that restores health',
requirements: {
crafting: 2
},
materials: [
{ id: 'herbs', quantity: 3 },
{ id: 'bandages', quantity: 2 }
],
results: [
{ id: 'health_kit', quantity: 3 }
],
experience: 5,
craftingTime: 2000 // 2 seconds
});
this.addRecipe('basic_circuit', {
name: 'Basic Circuit',
category: 'items',
description: 'Create a basic electronic circuit',
requirements: {
crafting: 3
},
materials: [
{ id: 'basic_circuitboard', quantity: 1 },
{ id: 'copper_wire', quantity: 2 }
],
results: [
{ id: 'basic_circuit', quantity: 1 }
],
experience: 8,
craftingTime: 2500 // 2.5 seconds
});
this.addRecipe('advanced_circuit', {
name: 'Advanced Circuit',
category: 'items',
description: 'Create an advanced electronic circuit',
requirements: {
crafting: 5
},
materials: [
{ id: 'advanced_circuitboard', quantity: 1 },
{ id: 'energy_crystal', quantity: 2 },
{ id: 'copper_wire', quantity: 3 }
],
results: [
{ id: 'advanced_circuit', quantity: 1 }
],
experience: 15,
craftingTime: 4000 // 4 seconds
});
this.addRecipe('electronic_device', {
name: 'Electronic Device',
category: 'items',
description: 'Create a complex electronic device',
requirements: {
crafting: 7
},
materials: [
{ id: 'advanced_components', quantity: 1 },
{ id: 'battery', quantity: 2 },
{ id: 'common_circuitboard', quantity: 1 }
],
results: [
{ id: 'electronic_device', quantity: 1 }
],
experience: 20,
craftingTime: 5000 // 5 seconds
});
// Ship Component Recipes
this.addRecipe('shield_generator', {
name: 'Shield Generator',
category: 'ships',
description: 'A basic shield generator for ship protection',
requirements: {
engineering: 2,
crafting: 4
},
materials: [
{ id: 'energy_crystal', quantity: 8 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'copper_wire', quantity: 4 }
],
results: [
{ id: 'shield_generator', quantity: 1 }
],
experience: 30,
craftingTime: 8000 // 8 seconds
});
this.addRecipe('engine_upgrade', {
name: 'Engine Upgrade',
category: 'ships',
description: 'An upgrade that improves ship engine performance',
requirements: {
engineering: 5,
crafting: 7
},
materials: [
{ id: 'steel_plate', quantity: 10 },
{ id: 'energy_crystal', quantity: 6 },
{ id: 'rare_metal', quantity: 2 }
],
results: [
{ id: 'engine_upgrade', quantity: 1 }
],
experience: 50,
craftingTime: 10000 // 10 seconds
});
this.addRecipe('quantum_computer', {
name: 'Quantum Computer',
category: 'ships',
description: 'Advanced quantum computer for high-end ship systems',
requirements: {
engineering: 8,
crafting: 10
},
materials: [
{ id: 'advanced_components', quantity: 3 },
{ id: 'energy_crystal', quantity: 8 },
{ id: 'rare_metal', quantity: 4 },
{ id: 'copper_wire', quantity: 6 }
],
results: [
{ id: 'quantum_computer', quantity: 1 }
],
experience: 100,
craftingTime: 15000 // 15 seconds
});
}
addRecipe(id, recipe) {
recipe.id = id;
recipe.unlocked = false;
this.recipes.set(id, recipe);
}
update(deltaTime) {
// Check for newly unlocked recipes
this.checkRecipeUnlocks();
}
checkRecipeUnlocks() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
for (const [id, recipe] of this.recipes) {
if (!recipe.unlocked) {
let canCraft = true;
// Check skill requirements
if (recipe.requirements) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
canCraft = false;
break;
}
}
}
if (canCraft) {
recipe.unlocked = true;
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
}
}
}
}
getRecipesByCategory(category) {
return Array.from(this.recipes.values())
.filter(recipe => recipe.category === category);
}
canCraftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe) return false;
// Check skill requirements
if (recipe.requirements) {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return false;
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
return false;
}
}
}
// Check materials
if (recipe.materials) {
for (const material of recipe.materials) {
const inventory = this.game.systems.inventory;
if (!inventory || !inventory.hasItem(material.id, material.quantity)) {
return false;
}
}
}
return true;
}
getMissingMaterials(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe || !recipe.materials) return [];
const missing = [];
const inventory = this.game.systems.inventory;
console.log(`[CRAFTING DEBUG] Checking materials for recipe: ${recipe.name}`);
console.log(`[CRAFTING DEBUG] Inventory system:`, inventory);
for (const material of recipe.materials) {
let currentCount = 0;
// Safely get current material count
if (inventory && typeof inventory.getItemCount === 'function') {
try {
currentCount = inventory.getItemCount(material.id);
// Ensure we have a valid number
currentCount = typeof currentCount === 'number' && !isNaN(currentCount) ? currentCount : 0;
} catch (error) {
console.log(`[CRAFTING DEBUG] Error getting count for ${material.id}:`, error);
currentCount = 0;
}
console.log(`[CRAFTING DEBUG] Material ${material.id}: current=${currentCount}, required=${material.quantity}`);
} else {
console.log(`[CRAFTING DEBUG] Inventory or getItemCount not available for ${material.id}`);
currentCount = 0;
}
// Ensure required quantity is also a valid number
const requiredQuantity = typeof material.quantity === 'number' && !isNaN(material.quantity) ? material.quantity : 0;
// Check if we have enough materials
if (currentCount < requiredQuantity) {
missing.push({
id: material.id,
required: requiredQuantity,
current: currentCount,
missing: Math.max(0, requiredQuantity - currentCount)
});
}
}
console.log(`[CRAFTING DEBUG] Missing materials:`, missing);
return missing;
}
async craftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe) {
console.error(`[CRAFTING] Recipe not found: ${recipeId}`);
return false;
}
if (!this.canCraftRecipe(recipeId)) {
console.log(`[CRAFTING] Cannot craft recipe: ${recipe.name}`);
return false;
}
console.log(`[CRAFTING] Starting to craft: ${recipe.name}`);
// Remove materials
if (recipe.materials) {
for (const material of recipe.materials) {
this.game.systems.inventory.removeItem(material.id, material.quantity);
}
}
// Add crafting experience
if (recipe.experience) {
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
}
// Wait for crafting time
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime));
// Add results to inventory
if (recipe.results) {
for (const result of recipe.results) {
this.game.systems.inventory.addItem(result.id, result.quantity);
}
}
// Update quest progress
if (this.game.systems.questSystem) {
this.game.systems.questSystem.onItemCrafted();
}
console.log(`[CRAFTING] Successfully crafted: ${recipe.name}`);
return true;
}
selectRecipe(recipeId) {
this.selectedRecipe = this.recipes.get(recipeId);
return this.selectedRecipe;
}
getSelectedRecipe() {
return this.selectedRecipe;
}
updateUI() {
this.updateRecipeList();
this.updateCraftingDetails();
this.updateCraftingInfo();
}
updateRecipeList() {
const recipeListElement = document.getElementById('recipeList');
if (!recipeListElement) return;
const recipes = this.getRecipesByCategory(this.currentCategory);
recipeListElement.innerHTML = '';
if (recipes.length === 0) {
recipeListElement.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
return;
}
recipes.forEach(recipe => {
const recipeElement = document.createElement('div');
recipeElement.className = 'recipe-item';
recipeElement.dataset.recipeId = recipe.id;
const canCraft = this.canCraftRecipe(recipe.id);
const missingMaterials = this.getMissingMaterials(recipe.id);
// Check if recipe is unlocked (skill requirements met)
const skillSystem = this.game.systems.skillSystem;
let skillRequirementsMet = true;
if (recipe.requirements && skillSystem) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
skillRequirementsMet = false;
break;
}
}
}
// Apply styling based on status
if (!skillRequirementsMet) {
recipeElement.classList.add('locked');
} else if (!canCraft) {
recipeElement.classList.add('missing-materials');
} else {
recipeElement.classList.add('can-craft');
}
// Generate requirements text
const requirementsText = recipe.requirements ?
Object.entries(recipe.requirements).map(([skill, level]) => `${skill}: ${level}`).join(', ') : 'None';
// Generate materials with missing status
const materialsHtml = recipe.materials ? recipe.materials.map(mat => {
const missing = missingMaterials.find(m => m.id === mat.id);
const currentCount = missing ? missing.current : 0;
const requiredCount = mat.quantity || 0;
if (missing) {
return `<div class="material-item missing">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${currentCount}/${requiredCount}</span>
</div>`;
} else {
return `<div class="material-item">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${currentCount}/${requiredCount}</span>
</div>`;
}
}).join('') : '';
recipeElement.innerHTML = `
<div class="recipe-header">
<h4>${recipe.name}</h4>
<span class="recipe-level">Level ${requirementsText}</span>
</div>
<div class="recipe-description">${recipe.description}</div>
<div class="recipe-materials">
${materialsHtml}
</div>
${missingMaterials.length > 0 ? `
<div class="missing-materials-text">
<i class="fas fa-exclamation-triangle"></i>
Missing: ${missingMaterials.map(m => `${m.missing}x ${m.id}`).join(', ')}
</div>
` : ''}
<div class="recipe-time">
<i class="fas fa-clock"></i>
<span>${recipe.craftingTime / 1000}s</span>
</div>
`;
recipeElement.addEventListener('click', () => {
this.selectRecipe(recipe.id);
this.updateCraftingDetails();
});
recipeListElement.appendChild(recipeElement);
});
}
updateCraftingDetails() {
const detailsElement = document.getElementById('craftingDetails');
if (!detailsElement) return;
if (!this.selectedRecipe) {
detailsElement.innerHTML = `
<div class="selected-recipe">
<h3>Select a Recipe</h3>
<p>Choose a recipe from the list to see details and craft items.</p>
</div>
`;
return;
}
const recipe = this.selectedRecipe;
const canCraft = this.canCraftRecipe(recipe.id);
detailsElement.innerHTML = `
<div class="selected-recipe">
<h3>${recipe.name}</h3>
<p class="recipe-description">${recipe.description}</p>
<div class="recipe-requirements">
<h4>Requirements:</h4>
${recipe.requirements ? Object.entries(recipe.requirements).map(([skill, level]) =>
`<div class="requirement-item">
<span class="skill-name">${skill}</span>
<span class="skill-level">Level ${level}</span>
</div>`
).join('') : '<p>No special requirements</p>'}
</div>
<div class="recipe-materials-needed">
<h4>Materials Needed:</h4>
${recipe.materials ? recipe.materials.map(mat =>
`<div class="material-needed">
<span class="material-name">${mat.id}</span>
<span class="material-needed">x${mat.quantity}</span>
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
</div>`
).join('') : '<p>No materials needed</p>'}
</div>
<div class="recipe-results">
<h4>Results:</h4>
${recipe.results ? recipe.results.map(result =>
`<div class="result-item">
<span class="result-name">${result.id}</span>
<span class="result-quantity">x${result.quantity}</span>
</div>`
).join('') : ''}
</div>
<div class="recipe-info">
<div class="experience-reward">
<i class="fas fa-star"></i>
<span>${recipe.experience} XP</span>
</div>
<div class="crafting-time">
<i class="fas fa-clock"></i>
<span>${recipe.craftingTime / 1000} seconds</span>
</div>
</div>
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
${canCraft ? 'Craft Item' : 'Cannot Craft'}
</button>
</div>
`;
}
updateCraftingInfo() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
const craftingLevel = skillSystem.getSkillLevel('crafting');
const craftingExp = skillSystem.getSkillExperience('crafting');
const expNeeded = skillSystem.getExperienceNeeded('crafting');
const levelElement = document.getElementById('craftingLevel');
const expElement = document.getElementById('craftingExp');
if (levelElement) levelElement.textContent = craftingLevel;
if (expElement) expElement.textContent = `${craftingExp}/${expNeeded}`;
}
switchCategory(category) {
this.currentCategory = category;
this.selectedRecipe = null;
// Update UI only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
this.updateUI();
}
}
}
// Export for use in GameEngine
if (typeof module !== 'undefined' && module.exports) {
module.exports = CraftingSystem;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,596 +0,0 @@
/**
* Galaxy Strike Online - Skill System
* Manages skills, progression, and specialization
*/
class SkillSystem {
constructor(gameEngine) {
this.game = gameEngine;
// Skill categories
this.categories = {
combat: 'Combat',
science: 'Science',
crafting: 'Crafting'
};
// Skill definitions
this.skills = {
combat: {
weapons_mastery: {
name: 'Weapons Mastery',
description: 'Increases weapon damage and critical chance',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
attack: 2,
criticalChance: 0.01
},
icon: 'fa-sword',
unlocked: true
},
shield_techniques: {
name: 'Shield Techniques',
description: 'Improves defense and energy efficiency',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
defense: 2,
maxEnergy: 5
},
icon: 'fa-shield-alt',
unlocked: true
},
piloting: {
name: 'Piloting',
description: 'Enhances speed and evasion',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
speed: 2,
criticalChance: 0.005
},
icon: 'fa-rocket',
unlocked: true
},
tactical_analysis: {
name: 'Tactical Analysis',
description: 'Reveals enemy weaknesses and improves accuracy',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
criticalDamage: 0.05,
attack: 1
},
icon: 'fa-brain',
unlocked: false,
requiredLevel: 5
}
},
science: {
engineering: {
name: 'Engineering',
description: 'Technical skills for ship components and machinery',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.08,
shipStats: 0.03
},
icon: 'fa-wrench',
unlocked: true
},
energy_manipulation: {
name: 'Energy Manipulation',
description: 'Better energy control and regeneration',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
maxEnergy: 10,
energyRegeneration: 0.1
},
icon: 'fa-bolt',
unlocked: true
},
alien_technology: {
name: 'Alien Technology',
description: 'Understanding and using alien artifacts',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
findRarity: 0.05,
itemValue: 0.1
},
icon: 'fa-atom',
unlocked: false,
requiredLevel: 3
},
quantum_physics: {
name: 'Quantum Physics',
description: 'Advanced quantum mechanics for better equipment',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
criticalDamage: 0.1,
attack: 3
},
icon: 'fa-microscope',
unlocked: false,
requiredLevel: 8
},
bio_engineering: {
name: 'Bio-Engineering',
description: 'Biological enhancements and healing',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
maxHealth: 15,
healthRegeneration: 0.05
},
icon: 'fa-dna',
unlocked: false,
requiredLevel: 6
}
},
crafting: {
crafting: {
name: 'General Crafting',
description: 'Basic crafting skills for all items',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.05
},
icon: 'fa-hammer',
unlocked: true
},
weapon_crafting: {
name: 'Weapon Crafting',
description: 'Create and upgrade weapons',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.1,
weaponStats: 0.05
},
icon: 'fa-hammer',
unlocked: true
},
armor_forging: {
name: 'Armor Forging',
description: 'Forge protective armor and shields',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.1,
armorStats: 0.05
},
icon: 'fa-anvil',
unlocked: true
},
resource_extraction: {
name: 'Resource Extraction',
description: 'Better resource gathering and efficiency',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
resourceBonus: 0.15,
findResources: 0.1
},
icon: 'fa-gem',
unlocked: false,
requiredLevel: 4
},
engineering: {
name: 'Engineering',
description: 'Advanced ship modifications and systems',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
shipUpgrades: 0.2,
systemEfficiency: 0.1
},
icon: 'fa-cogs',
unlocked: false,
requiredLevel: 7
}
}
};
// Skill experience rates
this.experienceRates = {
combat: 1.0,
science: 0.8,
crafting: 0.6
};
// Active buffs from skills
this.activeBuffs = {};
}
async initialize() {
}
// Skill management
addSkillExperience(category, skillId, amount) {
const skill = this.skills[category]?.[skillId];
if (!skill || skill.currentLevel >= skill.maxLevel) {
return false;
}
skill.experience += amount;
// Check for level up
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
this.levelUpSkill(category, skillId);
}
this.applySkillEffects();
return true;
}
levelUpSkill(category, skillId) {
const skill = this.skills[category][skillId];
// Handle excess experience
const excessExperience = skill.experience - skill.experienceToNext;
skill.currentLevel++;
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
// Set experience to excess (minimum 0)
skill.experience = Math.max(0, excessExperience);
// Apply skill effects
this.applySkillEffects();
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
this.game.showNotification('Skill effects applied!', 'info', 3000);
}
upgradeSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) {
this.game.showNotification('Skill not found', 'error', 3000);
return false;
}
if (!skill.unlocked) {
this.game.showNotification('Skill is locked', 'error', 3000);
return false;
}
if (skill.currentLevel >= skill.maxLevel) {
this.game.showNotification('Skill is at maximum level', 'warning', 3000);
return false;
}
if (player.stats.skillPoints < 1) {
this.game.showNotification('Not enough skill points', 'error', 3000);
return false;
}
// Use skill point and level up
player.stats.skillPoints--;
this.levelUpSkill(category, skillId);
// Update UI to refresh skill points display only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
this.updateUI();
}
return true;
}
unlockSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) {
this.game.showNotification('Skill not found', 'error', 3000);
return false;
}
if (skill.unlocked) {
this.game.showNotification('Skill is already unlocked', 'warning', 3000);
return false;
}
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
return false;
}
if (player.stats.skillPoints < 2) {
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
return false;
}
// Unlock skill
player.stats.skillPoints -= 2;
skill.unlocked = true;
skill.currentLevel = 1;
this.applySkillEffects();
// Update UI to refresh skill points display only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
this.updateUI();
}
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
return true;
}
applySkillEffects() {
const player = this.game.systems.player;
// Reset to base stats first
this.resetToBaseStats();
// Apply all skill effects
Object.values(this.skills).forEach(skill => {
if (skill.level > 0) {
const skillData = this.skillData[skill.id];
if (skillData && skillData.effects) {
Object.entries(skillData.effects).forEach(([effect, value]) => {
const totalEffect = value * skill.level;
switch (effect) {
case 'attack':
player.attributes.attack += totalEffect;
break;
case 'defense':
player.attributes.defense += totalEffect;
break;
case 'speed':
player.attributes.speed += totalEffect;
break;
case 'maxHealth':
player.attributes.maxHealth += totalEffect;
break;
case 'maxEnergy':
player.attributes.maxEnergy += totalEffect;
break;
case 'criticalChance':
player.attributes.criticalChance += totalEffect;
break;
case 'criticalDamage':
player.attributes.criticalDamage += totalEffect;
break;
case 'energyRegeneration':
case 'healthRegeneration':
case 'craftingBonus':
case 'weaponStats':
case 'armorStats':
case 'resourceBonus':
case 'shipUpgrades':
case 'systemEfficiency':
case 'findRarity':
case 'itemValue':
case 'findResources':
// Store these for other systems to use
if (!this.activeBuffs[effect]) {
this.activeBuffs[effect] = 0;
}
this.activeBuffs[effect] += totalEffect;
break;
}
});
}
}
});
player.updateUI();
}
resetToBaseStats() {
const player = this.game.systems.player;
// Reset to base values (would need to store base stats separately)
// For now, we'll use initial values
const baseStats = {
attack: 10 + (player.stats.level - 1) * 2,
defense: 5 + (player.stats.level - 1) * 1,
speed: 10,
maxHealth: 100 + (player.stats.level - 1) * 10,
maxEnergy: 100 + (player.stats.level - 1) * 5,
criticalChance: 0.05,
criticalDamage: 1.5
};
Object.assign(player.attributes, baseStats);
this.activeBuffs = {};
}
// Skill experience from actions
awardCombatExperience(amount) {
this.addSkillExperience('combat', 'weapons_mastery', amount);
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
}
awardScienceExperience(amount) {
this.addSkillExperience('science', 'energy_manipulation', amount);
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
}
awardCraftingExperience(amount) {
this.addSkillExperience('crafting', 'weapon_crafting', amount);
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
}
// Skill checks
getSkillLevel(category, skillId) {
return this.skills[category]?.[skillId]?.currentLevel || 0;
}
hasSkill(category, skillId, minimumLevel = 1) {
const skill = this.skills[category]?.[skillId];
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
}
getSkillBonus(effect) {
return this.activeBuffs[effect] || 0;
}
// UI updates
updateUI() {
this.updateSkillsGrid();
this.updateSkillPointsDisplay();
}
updateSkillsGrid() {
const skillsGridElement = document.getElementById('skillsGrid');
if (!skillsGridElement) return;
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
const skills = this.skills[activeCategory] || {};
skillsGridElement.innerHTML = '';
Object.entries(skills).forEach(([skillId, skill]) => {
const skillElement = document.createElement('div');
skillElement.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
const progressPercent = skill.currentLevel > 0 ?
(skill.experience / skill.experienceToNext) * 100 : 0;
// Use texture manager for icon fallback
const iconClass = this.game.systems.textureManager ?
this.game.systems.textureManager.getIcon(skill.icon) :
(skill.icon || 'fa-question');
skillElement.innerHTML = `
<div class="skill-header">
<div class="skill-icon">
<i class="fas ${iconClass}"></i>
</div>
<div class="skill-info">
<div class="skill-name">${skill.name}</div>
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
</div>
</div>
<div class="skill-description">${skill.description}</div>
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
<div class="skill-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
</div>
<span>${skill.experience}/${skill.experienceToNext} XP</span>
</div>
` : skill.currentLevel >= skill.maxLevel ? `
<div class="skill-max-level">
<span>MAX LEVEL</span>
</div>
` : ''}
<div class="skill-actions">
${!skill.unlocked ? `
<button class="btn btn-warning" onclick="if(window.game && window.game.systems) window.game.systems.skillSystem.unlockSkill('${activeCategory}', '${skillId}')">
Unlock (2 Points)
</button>
` : skill.currentLevel < skill.maxLevel ? `
<button class="btn btn-primary" onclick="if(window.game && window.game.systems) window.game.systems.skillSystem.upgradeSkill('${activeCategory}', '${skillId}')">
Upgrade (1 Point)
</button>
` : `
<span class="max-level">MAX LEVEL</span>
`}
</div>
${skill.requiredLevel && !skill.unlocked ? `
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
` : ''}
`;
skillsGridElement.appendChild(skillElement);
});
}
updateSkillPointsDisplay() {
const player = this.game.systems.player;
// Update skill points display if element exists
const skillPointsElements = document.querySelectorAll('.skill-points');
skillPointsElements.forEach(element => {
element.textContent = `Skill Points: ${player.stats.skillPoints}`;
});
}
// Save/Load
save() {
return {
skills: this.skills,
activeBuffs: this.activeBuffs
};
}
load(data) {
if (data.skills) {
// Deep merge to preserve structure
for (const [category, skills] of Object.entries(data.skills)) {
if (this.skills[category]) {
for (const [skillId, skillData] of Object.entries(skills)) {
if (this.skills[category][skillId]) {
Object.assign(this.skills[category][skillId], skillData);
}
}
}
}
}
if (data.activeBuffs) {
this.activeBuffs = data.activeBuffs;
}
this.applySkillEffects();
}
reset() {
this.skillPoints = 0;
this.unlockedSkills = [];
this.activeBuffs = [];
// Skills are already defined in constructor, just reset levels
Object.values(this.skills).forEach(category => {
Object.values(category).forEach(skill => {
skill.currentLevel = 0;
skill.experience = 0;
});
});
}
clear() {
this.reset();
}
}

View File

@ -0,0 +1,120 @@
{
"_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 }
}

File diff suppressed because it is too large Load Diff

View File

@ -70,13 +70,13 @@ class GameInitializer {
return;
}
// FORCE THE URL - Override any undefined issues
const FORCED_URL = 'https://dev.gameserver.galaxystrike.online';
console.log('[GAME INITIALIZER] FORCING URL to:', FORCED_URL);
console.log('[GAME INITIALIZER] Original this.gameServerUrl:', this.gameServerUrl);
console.log('[GAME INITIALIZER] Using remote development server');
// Resolve server URL: use serverData.url if set, otherwise gameServerUrl, then dev default
const FORCED_URL = (this.serverData && this.serverData.url)
? this.serverData.url
: (this.gameServerUrl || 'https://dev.gameserver.galaxystrike.online');
console.log('[GAME INITIALIZER] Connecting to:', FORCED_URL);
// Connect to the game server with FORCED URL
// Connect to the game server
this.socket = io(FORCED_URL, {
auth: {
token: this.authToken,
@ -84,7 +84,7 @@ class GameInitializer {
}
});
console.log('[GAME INITIALIZER] Socket.IO connection initiated to FORCED URL:', FORCED_URL);
console.log('[GAME INITIALIZER] Socket.IO connection initiated to:', FORCED_URL);
// Socket event handlers
this.socket.on('connect', () => {

View File

@ -636,24 +636,11 @@ class Economy {
console.log('[ECONOMY] updateShopUI called');
if (this.game.multiplayerMode && this.game.itemSystem && this.game.itemSystem.catalog) {
console.log('[ECONOMY] Multiplayer mode:', true);
console.log('[ECONOMY] ItemSystem available:', !!this.game.itemSystem);
console.log('[ECONOMY] ItemSystem catalog:', !!this.game.itemSystem.catalog);
const shopItems = this.game.itemSystem.catalog;
console.log('[ECONOMY] Got categorized shop items:', Object.keys(shopItems));
// Get current active category
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';
console.log('[ECONOMY] Active shop category:', activeCategory);
// Filter items for active category
const categoryItems = shopItems[activeCategory] || [];
console.log('[ECONOMY] Using new shop structure - found', categoryItems.length, 'categories');
console.log('[ECONOMY] Filtered items for category', activeCategory, ':', categoryItems.length, 'items');
console.log('[ECONOMY] Item types in category:', categoryItems.map(item => item.type));
this.renderShopItems(categoryItems);
} else {
// Singleplayer mode - use local shop data

View File

@ -1,6 +1,7 @@
/**
* Galaxy Strike Online - Game Data
* Static game data, constants, and configuration
* UI constants and configuration only.
* All game content (items, skills, recipes, dungeons, enemies) is loaded from the server.
*/
// Game configuration
@ -13,7 +14,7 @@ const GAME_CONFIG = {
maxNotifications: 5
};
// Player defaults
// Player defaults (used only for initial UI state before server data arrives)
const PLAYER_DEFAULTS = {
level: 1,
experience: 0,
@ -31,522 +32,81 @@ const PLAYER_DEFAULTS = {
criticalDamage: 1.5
};
// Experience requirements
// 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 rarities with colors and multipliers
// Item rarity display properties (colours/labels only - drop rates are server-side)
const ITEM_RARITIES = {
common: {
name: 'Common',
color: '#888888',
multiplier: 1.0,
dropChance: 0.60
},
uncommon: {
name: 'Uncommon',
color: '#00ff00',
multiplier: 1.2,
dropChance: 0.25
},
rare: {
name: 'Rare',
color: '#0088ff',
multiplier: 1.5,
dropChance: 0.10
},
epic: {
name: 'Epic',
color: '#8833ff',
multiplier: 2.0,
dropChance: 0.04
},
legendary: {
name: 'Legendary',
color: '#ff8800',
multiplier: 3.0,
dropChance: 0.01
}
};
// Enemy types and stats
const ENEMY_TEMPLATES = {
space_pirate: {
name: 'Space Pirate',
health: 25,
attack: 10,
defense: 3,
speed: 8,
experience: 15,
credits: 12,
rarity: 'common'
},
alien_guardian: {
name: 'Alien Guardian',
health: 50,
attack: 8,
defense: 5,
speed: 6,
experience: 25,
credits: 15,
rarity: 'common'
},
mining_drone: {
name: 'Mining Drone',
health: 20,
attack: 8,
defense: 3,
speed: 5,
experience: 12,
credits: 8,
rarity: 'common'
},
security_drone: {
name: 'Security Drone',
health: 35,
attack: 14,
defense: 4,
speed: 10,
experience: 22,
credits: 15,
rarity: 'uncommon'
},
pirate_captain: {
name: 'Pirate Captain',
health: 40,
attack: 15,
defense: 6,
speed: 12,
experience: 30,
credits: 20,
rarity: 'uncommon'
},
crystal_golem: {
name: 'Crystal Golem',
health: 80,
attack: 6,
defense: 10,
speed: 4,
experience: 35,
credits: 25,
rarity: 'rare'
},
corrupted_ai: {
name: 'Corrupted AI',
health: 60,
attack: 20,
defense: 2,
speed: 15,
experience: 40,
credits: 30,
rarity: 'rare'
},
energy_being: {
name: 'Energy Being',
health: 55,
attack: 22,
defense: 3,
speed: 18,
experience: 45,
credits: 35,
rarity: 'epic'
},
quantum_entity: {
name: 'Quantum Entity',
health: 70,
attack: 35,
defense: 5,
speed: 20,
experience: 60,
credits: 50,
rarity: 'legendary'
}
};
// Dungeon configurations
const DUNGEON_CONFIGS = {
alien_ruins: {
name: 'Alien Ruins',
description: 'Ancient alien structures filled with mysterious technology',
difficulty: 'medium',
minLevel: 3,
roomCount: [5, 8],
enemyTypes: ['alien_guardian', 'ancient_drone', 'crystal_golem'],
rewardMultiplier: 1.2,
energyCost: 20
},
pirate_lair: {
name: 'Pirate Lair',
description: 'Dangerous pirate hideouts with valuable loot',
difficulty: 'easy',
minLevel: 1,
roomCount: [4, 6],
enemyTypes: ['space_pirate', 'pirate_captain', 'defense_turret'],
rewardMultiplier: 1.0,
energyCost: 15
},
corrupted_vault: {
name: 'Corrupted AI Vault',
description: 'Malfunctioning AI facilities with corrupted security',
difficulty: 'hard',
minLevel: 5,
roomCount: [6, 9],
enemyTypes: ['security_drone', 'corrupted_ai', 'virus_program'],
rewardMultiplier: 1.5,
energyCost: 25
},
asteroid_mine: {
name: 'Asteroid Mine',
description: 'Abandoned mining facilities in asteroid fields',
difficulty: 'easy',
minLevel: 2,
roomCount: [4, 7],
enemyTypes: ['mining_drone', 'rock_creature', 'explosive_asteroid'],
rewardMultiplier: 0.8,
energyCost: 10
},
nebula_anomaly: {
name: 'Nebula Anomaly',
description: 'Strange energy anomalies in deep space',
difficulty: 'extreme',
minLevel: 8,
roomCount: [7, 10],
enemyTypes: ['energy_being', 'phase_shifter', 'quantum_entity'],
rewardMultiplier: 2.0,
energyCost: 30
}
};
// Skill definitions
const SKILL_DEFINITIONS = {
combat: {
weapons_mastery: {
name: 'Weapons Mastery',
description: 'Increases weapon damage and critical chance',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
attack: 2,
criticalChance: 0.01
},
icon: 'fa-sword'
},
shield_techniques: {
name: 'Shield Techniques',
description: 'Improves defense and energy efficiency',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
defense: 2,
maxEnergy: 5
},
icon: 'fa-shield-alt'
},
piloting: {
name: 'Piloting',
description: 'Enhances speed and evasion',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
speed: 2,
criticalChance: 0.005
},
icon: 'fa-rocket'
}
},
science: {
energy_manipulation: {
name: 'Energy Manipulation',
description: 'Better energy control and regeneration',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
maxEnergy: 10,
energyRegeneration: 0.1
},
icon: 'fa-bolt'
},
alien_technology: {
name: 'Alien Technology',
description: 'Understanding and using alien artifacts',
maxLevel: 10,
experiencePerLevel: 150,
effects: {
findRarity: 0.05,
itemValue: 0.1
},
icon: 'fa-atom'
}
},
crafting: {
weapon_crafting: {
name: 'Weapon Crafting',
description: 'Create and upgrade weapons',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
craftingBonus: 0.1,
weaponStats: 0.05
},
icon: 'fa-hammer'
},
armor_forging: {
name: 'Armor Forging',
description: 'Forge protective armor and shields',
maxLevel: 10,
experiencePerLevel: 100,
effects: {
craftingBonus: 0.1,
armorStats: 0.05
},
icon: 'fa-anvil'
}
}
};
// Shop items
const SHOP_ITEMS = {
ships: [
{
id: 'fighter_mk1',
name: 'Fighter Mk. I',
type: 'ship',
rarity: 'common',
price: 5000,
currency: 'credits',
description: 'Fast and agile fighter ship',
stats: { attack: 15, speed: 20, defense: 8 }
},
{
id: 'cruiser_mk1',
name: 'Cruiser Mk. I',
type: 'ship',
rarity: 'uncommon',
price: 15000,
currency: 'credits',
description: 'Well-balanced cruiser for combat',
stats: { attack: 20, speed: 10, defense: 15 }
}
],
upgrades: [
{
id: 'weapon_upgrade_1',
name: 'Weapon Upgrade I',
type: 'upgrade',
rarity: 'common',
price: 500,
currency: 'credits',
description: 'Increases weapon damage by 10%',
effect: { attackMultiplier: 1.1 }
},
{
id: 'shield_upgrade_1',
name: 'Shield Upgrade I',
type: 'upgrade',
rarity: 'common',
price: 400,
currency: 'credits',
description: 'Increases defense by 5 points',
effect: { defense: 5 }
}
],
cosmetics: [
{
id: 'blue_paint',
name: 'Blue Paint Job',
type: 'cosmetic',
rarity: 'common',
price: 100,
currency: 'gems',
description: 'Custom blue paint for your ship'
},
{
id: 'golden_trim',
name: 'Golden Trim',
type: 'cosmetic',
rarity: 'rare',
price: 500,
currency: 'gems',
description: 'Luxurious golden trim for your ship'
}
],
consumables: [
{
id: 'mega_health_kit',
name: 'Mega Health Kit',
type: 'consumable',
rarity: 'uncommon',
price: 50,
currency: 'credits',
description: 'Restores full health',
effect: { heal: 999 }
},
{
id: 'energy_boost',
name: 'Energy Boost',
type: 'consumable',
rarity: 'common',
price: 25,
currency: 'credits',
description: 'Restores 50 energy',
effect: { energy: 50 }
}
]
};
// Starter equipment for new players
const STARTER_EQUIPMENT = {
starter_blaster: {
id: 'starter_blaster',
name: 'Common Blaster',
type: 'weapon',
rarity: 'common',
description: 'A reliable basic blaster for new pilots',
stats: { attack: 5, criticalChance: 0.02 },
equipable: true,
slot: 'weapon',
value: 100,
stackable: false
},
basic_armor: {
id: 'basic_armor',
name: 'Basic Armor',
type: 'armor',
rarity: 'common',
description: 'Standard issue armor for basic protection',
stats: { defense: 3, health: 10 },
equipable: true,
slot: 'armor',
value: 150,
stackable: false
}
};
// Achievement definitions
const ACHIEVEMENTS = {
first_victory: {
name: 'First Victory',
description: 'Win your first dungeon',
requirement: { dungeonsCompleted: 1 },
reward: { gems: 10, experience: 100 },
icon: 'fa-trophy'
},
dungeon_master: {
name: 'Dungeon Master',
description: 'Complete 50 dungeons',
requirement: { dungeonsCompleted: 50 },
reward: { gems: 100, experience: 1000 },
icon: 'fa-dungeon'
},
level_master: {
name: 'Level Master',
description: 'Reach level 50',
requirement: { level: 50 },
reward: { gems: 200, experience: 5000 },
icon: 'fa-level-up-alt'
},
wealthy_commander: {
name: 'Wealthy Commander',
description: 'Accumulate 1,000,000 credits',
requirement: { credits: 1000000 },
reward: { gems: 150, experience: 2000 },
icon: 'fa-coins'
},
skill_expert: {
name: 'Skill Expert',
description: 'Max out any skill',
requirement: { maxSkillLevel: 10 },
reward: { gems: 75, experience: 1500 },
icon: 'fa-graduation-cap'
}
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!',
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}'
dailyReward: 'Daily reward claimed! Day {day}',
offlineRewards: 'Welcome back! You were offline for {time}'
};
// Utility functions
const GameUtils = {
// Get random item from array
getRandomItem(array) {
return array[Math.floor(Math.random() * array.length)];
},
// Get random integer between min and max (inclusive)
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
// Get random float between min and max
getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
},
// Check if chance succeeds
checkChance(chance) {
return Math.random() < chance;
},
// Format large numbers with suffixes
formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return Math.floor(num).toString();
},
// Format time in milliseconds to readable string
formatTime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
},
// Calculate experience needed for level
getExperienceForLevel(level) {
return EXPERIENCE_TABLE[level] || 0;
},
// Get item rarity by chance
getItemRarity() {
const roll = Math.random();
let cumulative = 0;
for (const [rarity, data] of Object.entries(ITEM_RARITIES)) {
cumulative += data.dropChance;
if (roll <= cumulative) {
return rarity;
}
}
return 'common';
},
// Deep clone object
deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
},
// Generate unique ID
generateId() {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
}
@ -559,11 +119,6 @@ if (typeof module !== 'undefined' && module.exports) {
PLAYER_DEFAULTS,
EXPERIENCE_TABLE,
ITEM_RARITIES,
ENEMY_TEMPLATES,
DUNGEON_CONFIGS,
SKILL_DEFINITIONS,
SHOP_ITEMS,
ACHIEVEMENTS,
GAME_MESSAGES,
GameUtils
};

View File

@ -1026,8 +1026,15 @@ class BaseSystem {
} else if (view === 'ships') {
this.updateShipGallery();
} else if (view === 'starbases') {
this.updateStarbaseList();
this.updateStarbasePurchaseList();
// Boot the isometric starbase world instead of the old list UI
if (typeof _startStarbaseWorld === 'function') {
_startStarbaseWorld();
}
} else {
// Leaving starbases — stop the world loop
if (typeof _stopStarbaseWorld === 'function') {
_stopStarbaseWorld();
}
}
}

View File

@ -1,651 +1,397 @@
/**
* Galaxy Strike Online - Crafting System
* Handles item crafting, recipes, and crafting skill progression
* 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();
this.recipes = new Map(); // recipeId -> recipe object
this.currentCategory = 'weapons';
this.selectedRecipe = null;
this.initializeRecipes();
this.selectedRecipe = null;
this._loaded = false;
this._loading = false;
}
initializeRecipes() {
// Weapon Recipes
this.addRecipe('basic_blaster', {
name: 'Basic Blaster',
category: 'weapons',
description: 'A simple energy blaster for beginners',
requirements: {
weapon_crafting: 1,
crafting: 1
},
materials: [
{ id: 'iron_ore', quantity: 5 },
{ id: 'energy_crystal', quantity: 2 }
],
results: [
{ id: 'basic_blaster', quantity: 1 }
],
experience: 10,
craftingTime: 3000 // 3 seconds
});
this.addRecipe('enhanced_blaster', {
name: 'Enhanced Blaster',
category: 'weapons',
description: 'An improved blaster with better damage output',
requirements: {
weapon_crafting: 3,
crafting: 5
},
materials: [
{ id: 'iron_ore', quantity: 10 },
{ id: 'energy_crystal', quantity: 5 },
{ id: 'copper_wire', quantity: 3 }
],
results: [
{ id: 'enhanced_blaster', quantity: 1 }
],
experience: 25,
craftingTime: 5000 // 5 seconds
});
this.addRecipe('laser_sniper_rifle', {
name: 'Laser Sniper Rifle',
category: 'weapons',
description: 'A long-range precision laser weapon',
requirements: {
weapon_crafting: 3,
crafting: 5
},
materials: [
{ id: 'advanced_circuitboard', quantity: 2 },
{ id: 'energy_crystal', quantity: 8 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'copper_wire', quantity: 4 }
],
results: [
{ id: 'laser_sniper_rifle', quantity: 1 }
],
experience: 40,
craftingTime: 8000 // 8 seconds
});
this.addRecipe('plasma_cannon', {
name: 'Plasma Cannon',
category: 'weapons',
description: 'A devastating plasma-based weapon',
requirements: {
weapon_crafting: 5,
crafting: 7
},
materials: [
{ id: 'advanced_components', quantity: 3 },
{ id: 'energy_crystal', quantity: 12 },
{ id: 'steel_plate', quantity: 8 },
{ id: 'battery', quantity: 3 }
],
results: [
{ id: 'plasma_cannon', quantity: 1 }
],
experience: 60,
craftingTime: 12000 // 12 seconds
});
// Armor Recipes
this.addRecipe('basic_armor', {
name: 'Basic Armor',
category: 'armor',
description: 'Light armor providing basic protection',
requirements: {
armor_forging: 1,
crafting: 1
},
materials: [
{ id: 'iron_ore', quantity: 8 },
{ id: 'leather', quantity: 3 }
],
results: [
{ id: 'basic_armor', quantity: 1 }
],
experience: 15,
craftingTime: 4000 // 4 seconds
});
this.addRecipe('reinforced_armor', {
name: 'Reinforced Armor',
category: 'armor',
description: 'Heavy armor with enhanced protection',
requirements: {
armor_forging: 4,
crafting: 6
},
materials: [
{ id: 'iron_ore', quantity: 15 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'leather', quantity: 5 }
],
results: [
{ id: 'reinforced_armor', quantity: 1 }
],
experience: 35,
craftingTime: 6000 // 6 seconds
});
// Item Recipes
this.addRecipe('health_kit', {
name: 'Health Kit',
category: 'items',
description: 'A medical kit that restores health',
requirements: {
crafting: 2
},
materials: [
{ id: 'herbs', quantity: 3 },
{ id: 'bandages', quantity: 2 }
],
results: [
{ id: 'health_kit', quantity: 3 }
],
experience: 5,
craftingTime: 2000 // 2 seconds
});
this.addRecipe('basic_circuit', {
name: 'Basic Circuit',
category: 'items',
description: 'Create a basic electronic circuit',
requirements: {
crafting: 3
},
materials: [
{ id: 'basic_circuitboard', quantity: 1 },
{ id: 'copper_wire', quantity: 2 }
],
results: [
{ id: 'basic_circuit', quantity: 1 }
],
experience: 8,
craftingTime: 2500 // 2.5 seconds
});
this.addRecipe('advanced_circuit', {
name: 'Advanced Circuit',
category: 'items',
description: 'Create an advanced electronic circuit',
requirements: {
crafting: 5
},
materials: [
{ id: 'advanced_circuitboard', quantity: 1 },
{ id: 'energy_crystal', quantity: 2 },
{ id: 'copper_wire', quantity: 3 }
],
results: [
{ id: 'advanced_circuit', quantity: 1 }
],
experience: 15,
craftingTime: 4000 // 4 seconds
});
this.addRecipe('electronic_device', {
name: 'Electronic Device',
category: 'items',
description: 'Create a complex electronic device',
requirements: {
crafting: 7
},
materials: [
{ id: 'advanced_components', quantity: 1 },
{ id: 'battery', quantity: 2 },
{ id: 'common_circuitboard', quantity: 1 }
],
results: [
{ id: 'electronic_device', quantity: 1 }
],
experience: 20,
craftingTime: 5000 // 5 seconds
});
// Ship Component Recipes
this.addRecipe('shield_generator', {
name: 'Shield Generator',
category: 'ships',
description: 'A basic shield generator for ship protection',
requirements: {
engineering: 2,
crafting: 4
},
materials: [
{ id: 'energy_crystal', quantity: 8 },
{ id: 'steel_plate', quantity: 5 },
{ id: 'copper_wire', quantity: 4 }
],
results: [
{ id: 'shield_generator', quantity: 1 }
],
experience: 30,
craftingTime: 8000 // 8 seconds
});
this.addRecipe('engine_upgrade', {
name: 'Engine Upgrade',
category: 'ships',
description: 'An upgrade that improves ship engine performance',
requirements: {
engineering: 5,
crafting: 7
},
materials: [
{ id: 'steel_plate', quantity: 10 },
{ id: 'energy_crystal', quantity: 6 },
{ id: 'rare_metal', quantity: 2 }
],
results: [
{ id: 'engine_upgrade', quantity: 1 }
],
experience: 50,
craftingTime: 10000 // 10 seconds
});
this.addRecipe('quantum_computer', {
name: 'Quantum Computer',
category: 'ships',
description: 'Advanced quantum computer for high-end ship systems',
requirements: {
engineering: 8,
crafting: 10
},
materials: [
{ id: 'advanced_components', quantity: 3 },
{ id: 'energy_crystal', quantity: 8 },
{ id: 'rare_metal', quantity: 4 },
{ id: 'copper_wire', quantity: 6 }
],
results: [
{ id: 'quantum_computer', quantity: 1 }
],
experience: 100,
craftingTime: 15000 // 15 seconds
// ------------------------------------------------------------------ //
// 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) {
// Check for newly unlocked recipes
this.checkRecipeUnlocks();
}
checkRecipeUnlocks() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
for (const [id, recipe] of this.recipes) {
if (!recipe.unlocked) {
let canCraft = true;
// Check skill requirements
if (recipe.requirements) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
canCraft = false;
break;
}
if (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 (canCraft) {
recipe.unlocked = true;
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
}
}
if (canUnlock) {
recipe.unlocked = true;
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
}
}
}
getRecipesByCategory(category) {
return Array.from(this.recipes.values())
.filter(recipe => recipe.category === category);
.filter(r => r.category === category || r.type === category);
}
canCraftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
const recipe = this.recipes.get(recipeId);
const skillSystem = this.game.systems.skillSystem;
const inventory = this.game.systems.inventory;
if (!recipe) return false;
// Check skill requirements
if (recipe.requirements) {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return false;
if (recipe.requirements && skillSystem) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
return false;
}
if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false;
}
}
// Check materials
if (recipe.materials) {
for (const material of recipe.materials) {
const inventory = this.game.systems.inventory;
if (!inventory || !inventory.hasItem(material.id, material.quantity)) {
return false;
}
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);
if (!recipe || !recipe.materials) return [];
const missing = [];
const recipe = this.recipes.get(recipeId);
const inventory = this.game.systems.inventory;
console.log(`[CRAFTING DEBUG] Checking materials for recipe: ${recipe.name}`);
console.log(`[CRAFTING DEBUG] Inventory system:`, inventory);
for (const material of recipe.materials) {
let currentCount = 0;
// Safely get current material count
if (inventory && typeof inventory.getItemCount === 'function') {
try {
currentCount = inventory.getItemCount(material.id);
// Ensure we have a valid number
currentCount = typeof currentCount === 'number' && !isNaN(currentCount) ? currentCount : 0;
} catch (error) {
console.log(`[CRAFTING DEBUG] Error getting count for ${material.id}:`, error);
currentCount = 0;
}
console.log(`[CRAFTING DEBUG] Material ${material.id}: current=${currentCount}, required=${material.quantity}`);
} else {
console.log(`[CRAFTING DEBUG] Inventory or getItemCount not available for ${material.id}`);
currentCount = 0;
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 (_) {}
}
// Ensure required quantity is also a valid number
const requiredQuantity = typeof material.quantity === 'number' && !isNaN(material.quantity) ? material.quantity : 0;
// Check if we have enough materials
if (currentCount < requiredQuantity) {
missing.push({
id: material.id,
required: requiredQuantity,
current: currentCount,
missing: Math.max(0, requiredQuantity - currentCount)
});
const required = mat.quantity || 0;
if (current < required) {
missing.push({ id: mat.id, required, current, missing: required - current });
}
}
console.log(`[CRAFTING DEBUG] Missing materials:`, missing);
return missing;
}
async craftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe) {
console.error(`[CRAFTING] Recipe not found: ${recipeId}`);
return false;
}
if (!this.canCraftRecipe(recipeId)) {
console.log(`[CRAFTING] Cannot craft recipe: ${recipe.name}`);
return false;
}
console.log(`[CRAFTING] Starting to craft: ${recipe.name}`);
// Remove materials
if (!recipe || !this.canCraftRecipe(recipeId)) return false;
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
if (recipe.materials) {
for (const material of recipe.materials) {
this.game.systems.inventory.removeItem(material.id, material.quantity);
for (const mat of recipe.materials) {
this.game.systems.inventory.removeItem(mat.id, mat.quantity);
}
}
// Add crafting experience
if (recipe.experience) {
if (recipe.experience && this.game.systems.skillSystem) {
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
}
// Wait for crafting time
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime));
// Add results to inventory
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);
}
}
// Update quest progress
if (this.game.systems.questSystem) {
this.game.systems.questSystem.onItemCrafted();
this.game.systems.questSystem.onItemCrafted?.();
}
console.log(`[CRAFTING] Successfully crafted: ${recipe.name}`);
console.log(`[CRAFTING] Done: ${recipe.name}`);
return true;
}
selectRecipe(recipeId) {
this.selectedRecipe = this.recipes.get(recipeId);
return this.selectedRecipe;
}
getSelectedRecipe() {
return this.selectedRecipe;
}
getSelectedRecipe() { return this.selectedRecipe; }
// ------------------------------------------------------------------ //
// UI
// ------------------------------------------------------------------ //
updateUI() {
this.updateRecipeList();
this.updateCraftingDetails();
this.updateCraftingInfo();
}
updateRecipeList() {
const recipeListElement = document.getElementById('recipeList');
if (!recipeListElement) return;
const recipes = this.getRecipesByCategory(this.currentCategory);
recipeListElement.innerHTML = '';
if (recipes.length === 0) {
recipeListElement.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
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 recipeElement = document.createElement('div');
recipeElement.className = 'recipe-item';
recipeElement.dataset.recipeId = recipe.id;
const canCraft = this.canCraftRecipe(recipe.id);
const missingMaterials = this.getMissingMaterials(recipe.id);
// Check if recipe is unlocked (skill requirements met)
const skillSystem = this.game.systems.skillSystem;
let skillRequirementsMet = true;
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 [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
const skillLevel = skillSystem.getSkillLevel(skillName);
if (skillLevel < requiredLevel) {
skillRequirementsMet = false;
break;
}
for (const [skill, level] of Object.entries(recipe.requirements)) {
if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; }
}
}
// Apply styling based on status
if (!skillRequirementsMet) {
recipeElement.classList.add('locked');
} else if (!canCraft) {
recipeElement.classList.add('missing-materials');
} else {
recipeElement.classList.add('can-craft');
}
// Generate requirements text
const requirementsText = recipe.requirements ?
Object.entries(recipe.requirements).map(([skill, level]) => `${skill}: ${level}`).join(', ') : 'None';
// Generate materials with missing status
const materialsHtml = recipe.materials ? recipe.materials.map(mat => {
const missing = missingMaterials.find(m => m.id === mat.id);
const currentCount = missing ? missing.current : 0;
const requiredCount = mat.quantity || 0;
if (missing) {
return `<div class="material-item missing">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${currentCount}/${requiredCount}</span>
</div>`;
} else {
return `<div class="material-item">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${currentCount}/${requiredCount}</span>
</div>`;
}
}).join('') : '';
recipeElement.innerHTML = `
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 ${requirementsText}</span>
<span class="recipe-level">Level ${reqText}</span>
</div>
<div class="recipe-description">${recipe.description}</div>
<div class="recipe-materials">
${materialsHtml}
</div>
${missingMaterials.length > 0 ? `
<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: ${missingMaterials.map(m => `${m.missing}x ${m.id}`).join(', ')}
</div>
` : ''}
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
</div>` : ''}
<div class="recipe-time">
<i class="fas fa-clock"></i>
<span>${recipe.craftingTime / 1000}s</span>
<span>${(recipe.craftingTime || 0) / 1000}s</span>
</div>
`;
recipeElement.addEventListener('click', () => {
el.addEventListener('click', () => {
this.selectRecipe(recipe.id);
this.updateCraftingDetails();
});
recipeListElement.appendChild(recipeElement);
listEl.appendChild(el);
});
}
updateCraftingDetails() {
const detailsElement = document.getElementById('craftingDetails');
if (!detailsElement) return;
const detailsEl = document.getElementById('craftingDetails');
if (!detailsEl) return;
if (!this.selectedRecipe) {
detailsElement.innerHTML = `
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>
`;
</div>`;
return;
}
const recipe = this.selectedRecipe;
const recipe = this.selectedRecipe;
const canCraft = this.canCraftRecipe(recipe.id);
detailsElement.innerHTML = `
detailsEl.innerHTML = `
<div class="selected-recipe">
<h3>${recipe.name}</h3>
<p class="recipe-description">${recipe.description}</p>
<p class="recipe-description">${recipe.description || ''}</p>
<div class="recipe-requirements">
<h4>Requirements:</h4>
${recipe.requirements ? Object.entries(recipe.requirements).map(([skill, level]) =>
`<div class="requirement-item">
<span class="skill-name">${skill}</span>
<span class="skill-level">Level ${level}</span>
</div>`
).join('') : '<p>No special requirements</p>'}
${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 ? recipe.materials.map(mat =>
${recipe.materials.map(mat =>
`<div class="material-needed">
<span class="material-name">${mat.id}</span>
<span class="material-needed">x${mat.quantity}</span>
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
</div>`
).join('') : '<p>No materials needed</p>'}
</div>`).join('')}
</div>
<div class="recipe-results">
<h4>Results:</h4>
${recipe.results ? recipe.results.map(result =>
${recipe.results.map(r =>
`<div class="result-item">
<span class="result-name">${result.id}</span>
<span class="result-quantity">x${result.quantity}</span>
</div>`
).join('') : ''}
<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} XP</span>
<span>${recipe.experience || 0} XP</span>
</div>
<div class="crafting-time">
<i class="fas fa-clock"></i>
<span>${recipe.craftingTime / 1000} seconds</span>
<span>${(recipe.craftingTime || 0) / 1000} seconds</span>
</div>
</div>
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
<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>
`;
</div>`;
}
updateCraftingInfo() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
const craftingLevel = skillSystem.getSkillLevel('crafting');
const craftingExp = skillSystem.getSkillExperience('crafting');
const expNeeded = skillSystem.getExperienceNeeded('crafting');
const levelElement = document.getElementById('craftingLevel');
const expElement = document.getElementById('craftingExp');
if (levelElement) levelElement.textContent = craftingLevel;
if (expElement) expElement.textContent = `${craftingExp}/${expNeeded}`;
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;
// Update UI only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
this.selectedRecipe = null;
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
}

View File

@ -430,6 +430,13 @@ class ItemSystem {
}
}
/**
* catalog getter alias for shopItemsByCategory, used by Economy.updateShopUI
*/
get catalog() {
return this.shopItemsByCategory;
}
/**
* Get system statistics
*/

View File

@ -1,504 +1,330 @@
/**
* Galaxy Strike Online - Skill System
* Manages skills, progression, and specialization
* 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;
// Skill categories
// Populated after server responds to 'get_skills'
this.skills = { combat: {}, science: {}, crafting: {} };
this.categories = {
combat: 'Combat',
science: 'Science',
combat: 'Combat',
science: 'Science',
crafting: 'Crafting'
};
// Skill definitions
this.skills = {
combat: {
weapons_mastery: {
name: 'Weapons Mastery',
description: 'Increases weapon damage and critical chance',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
attack: 2,
criticalChance: 0.01
},
icon: 'fa-sword',
unlocked: true
},
shield_techniques: {
name: 'Shield Techniques',
description: 'Improves defense and energy efficiency',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
defense: 2,
maxEnergy: 5
},
icon: 'fa-shield-alt',
unlocked: true
},
piloting: {
name: 'Piloting',
description: 'Enhances speed and evasion',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
speed: 2,
criticalChance: 0.005
},
icon: 'fa-rocket',
unlocked: true
},
tactical_analysis: {
name: 'Tactical Analysis',
description: 'Reveals enemy weaknesses and improves accuracy',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
criticalDamage: 0.05,
attack: 1
},
icon: 'fa-brain',
unlocked: false,
requiredLevel: 5
}
},
science: {
engineering: {
name: 'Engineering',
description: 'Technical skills for ship components and machinery',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.08,
shipStats: 0.03
},
icon: 'fa-wrench',
unlocked: true
},
energy_manipulation: {
name: 'Energy Manipulation',
description: 'Better energy control and regeneration',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
maxEnergy: 10,
energyRegeneration: 0.1
},
icon: 'fa-bolt',
unlocked: true
},
alien_technology: {
name: 'Alien Technology',
description: 'Understanding and using alien artifacts',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
findRarity: 0.05,
itemValue: 0.1
},
icon: 'fa-atom',
unlocked: false,
requiredLevel: 3
},
quantum_physics: {
name: 'Quantum Physics',
description: 'Advanced quantum mechanics for better equipment',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
criticalDamage: 0.1,
attack: 3
},
icon: 'fa-microscope',
unlocked: false,
requiredLevel: 8
},
bio_engineering: {
name: 'Bio-Engineering',
description: 'Biological enhancements and healing',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
maxHealth: 15,
healthRegeneration: 0.05
},
icon: 'fa-dna',
unlocked: false,
requiredLevel: 6
}
},
crafting: {
crafting: {
name: 'General Crafting',
description: 'Basic crafting skills for all items',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.05
},
icon: 'fa-hammer',
unlocked: true
},
weapon_crafting: {
name: 'Weapon Crafting',
description: 'Create and upgrade weapons',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.1,
weaponStats: 0.05
},
icon: 'fa-hammer',
unlocked: true
},
armor_forging: {
name: 'Armor Forging',
description: 'Forge protective armor and shields',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
craftingBonus: 0.1,
armorStats: 0.05
},
icon: 'fa-anvil',
unlocked: true
},
resource_extraction: {
name: 'Resource Extraction',
description: 'Better resource gathering and efficiency',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
resourceBonus: 0.15,
findResources: 0.1
},
icon: 'fa-gem',
unlocked: false,
requiredLevel: 4
},
engineering: {
name: 'Engineering',
description: 'Advanced ship modifications and systems',
maxLevel: 10,
currentLevel: 0,
experience: 0,
experienceToNext: 100,
effects: {
shipUpgrades: 0.2,
systemEfficiency: 0.1
},
icon: 'fa-cogs',
unlocked: false,
requiredLevel: 7
}
}
};
// Skill experience rates
this.experienceRates = {
combat: 1.0,
science: 0.8,
crafting: 0.6
};
// Active buffs from skills
this.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() {
}
// Skill management
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;
}
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
skill.experience += amount;
// Check for level up
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
this.levelUpSkill(category, skillId);
}
this.applySkillEffects();
return true;
}
levelUpSkill(category, skillId) {
const skill = this.skills[category][skillId];
// Handle excess experience
const excessExperience = skill.experience - skill.experienceToNext;
skill.currentLevel++;
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
// Set experience to excess (minimum 0)
skill.experience = Math.max(0, excessExperience);
// Apply skill effects
this.applySkillEffects();
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
this.game.showNotification('Skill effects applied!', 'info', 3000);
}
upgradeSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) {
this.game.showNotification('Skill not found', 'error', 3000);
return false;
}
if (!skill.unlocked) {
this.game.showNotification('Skill is locked', 'error', 3000);
return false;
}
if (skill.currentLevel >= skill.maxLevel) {
this.game.showNotification('Skill is at maximum level', 'warning', 3000);
return false;
}
if (player.stats.skillPoints < 1) {
this.game.showNotification('Not enough skill points', 'error', 3000);
return false;
}
// Use skill point and level up
player.stats.skillPoints--;
this.levelUpSkill(category, skillId);
// Update UI to refresh skill points display only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
this.updateUI();
}
return true;
}
unlockSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
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) { 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();
}
if (skill.unlocked) {
this.game.showNotification('Skill is already unlocked', 'warning', 3000);
return false;
}
return true;
}
unlockSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
if (skill.unlocked) { this.game.showNotification('Skill is already unlocked', 'warning', 3000); return false; }
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
return false;
}
if (player.stats.skillPoints < 2) {
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
return false;
}
// Unlock skill
player.stats.skillPoints -= 2;
skill.unlocked = true;
skill.currentLevel = 1;
this.applySkillEffects();
// Update UI to refresh skill points display only if in multiplayer mode or game is actively running
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
if (shouldUpdateUI) {
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;
// Reset to base stats first
this.resetToBaseStats();
// Apply all skill effects
Object.values(this.skills).forEach(skill => {
if (skill.level > 0) {
const skillData = this.skillData[skill.id];
if (skillData && skillData.effects) {
Object.entries(skillData.effects).forEach(([effect, value]) => {
const totalEffect = value * skill.level;
switch (effect) {
case 'attack':
player.attributes.attack += totalEffect;
break;
case 'defense':
player.attributes.defense += totalEffect;
break;
case 'speed':
player.attributes.speed += totalEffect;
break;
case 'maxHealth':
player.attributes.maxHealth += totalEffect;
break;
case 'maxEnergy':
player.attributes.maxEnergy += totalEffect;
break;
case 'criticalChance':
player.attributes.criticalChance += totalEffect;
break;
case 'criticalDamage':
player.attributes.criticalDamage += totalEffect;
break;
case 'energyRegeneration':
case 'healthRegeneration':
case 'craftingBonus':
case 'weaponStats':
case 'armorStats':
case 'resourceBonus':
case 'shipUpgrades':
case 'systemEfficiency':
case 'findRarity':
case 'itemValue':
case 'findResources':
// Store these for other systems to use
if (!this.activeBuffs[effect]) {
this.activeBuffs[effect] = 0;
}
this.activeBuffs[effect] += totalEffect;
break;
}
});
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;
// Reset to base values (would need to store base stats separately)
// For now, we'll use initial values
const baseStats = {
attack: 10 + (player.stats.level - 1) * 2,
defense: 5 + (player.stats.level - 1) * 1,
speed: 10,
maxHealth: 100 + (player.stats.level - 1) * 10,
maxEnergy: 100 + (player.stats.level - 1) * 5,
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
};
Object.assign(player.attributes, baseStats);
});
this.activeBuffs = {};
}
// Skill experience from actions
// ------------------------------------------------------------------ //
// Combat / science / crafting XP helpers
// ------------------------------------------------------------------ //
awardCombatExperience(amount) {
this.addSkillExperience('combat', 'weapons_mastery', 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);
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
}
awardCraftingExperience(amount) {
this.addSkillExperience('crafting', 'weapon_crafting', amount);
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
this.addSkillExperience('crafting', 'weapons_crafting', amount);
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
}
// Skill checks
// ------------------------------------------------------------------ //
// 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 updates
// ------------------------------------------------------------------ //
// UI
// ------------------------------------------------------------------ //
updateUI() {
this.updateSkillsGrid();
this.updateSkillPointsDisplay();
}
updateSkillsGrid() {
const skillsGridElement = document.getElementById('skillsGrid');
if (!skillsGridElement) return;
const grid = document.getElementById('skillsGrid');
if (!grid) return;
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
const skills = this.skills[activeCategory] || {};
skillsGridElement.innerHTML = '';
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 skillElement = document.createElement('div');
skillElement.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
const progressPercent = skill.currentLevel > 0 ?
(skill.experience / skill.experienceToNext) * 100 : 0;
// Use texture manager for icon fallback
const iconClass = this.game.systems.textureManager ?
this.game.systems.textureManager.getIcon(skill.icon) :
(skill.icon || 'fa-question');
skillElement.innerHTML = `
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-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>
@ -508,89 +334,74 @@ return true;
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
<div class="skill-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
<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-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}')">
<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}')">
<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>
`}
` : `<span class="max-level">MAX LEVEL</span>`}
</div>
${skill.requiredLevel && !skill.unlocked ? `
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
` : ''}
`;
skillsGridElement.appendChild(skillElement);
grid.appendChild(el);
});
}
updateSkillPointsDisplay() {
const player = this.game.systems.player;
// Update skill points display if element exists
const skillPointsElements = document.querySelectorAll('.skill-points');
skillPointsElements.forEach(element => {
element.textContent = `Skill Points: ${player.stats.skillPoints}`;
document.querySelectorAll('.skill-points').forEach(el => {
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
});
}
// Save/Load
// ------------------------------------------------------------------ //
// Save / Load
// ------------------------------------------------------------------ //
save() {
return {
skills: this.skills,
activeBuffs: this.activeBuffs
};
return { skills: this.skills, activeBuffs: this.activeBuffs };
}
load(data) {
if (data.skills) {
// Deep merge to preserve structure
for (const [category, skills] of Object.entries(data.skills)) {
if (this.skills[category]) {
for (const [skillId, skillData] of Object.entries(skills)) {
if (this.skills[category][skillId]) {
Object.assign(this.skills[category][skillId], skillData);
}
if (!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;
}
if (data.activeBuffs) this.activeBuffs = data.activeBuffs;
this.applySkillEffects();
}
}
reset() {
this.skillPoints = 0;
this.unlockedSkills = [];
this.activeBuffs = [];
// Skills are already defined in constructor, just reset levels
Object.values(this.skills).forEach(category => {
Object.values(category).forEach(skill => {
skill.currentLevel = 0;
skill.experience = 0;
});
});
}
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();
}
clear() { this.reset(); }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
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 Normal file
View File

@ -0,0 +1,139 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@ -0,0 +1,19 @@
const mongoose = require('mongoose');
const logger = require('../utils/logger');
const connectDB = async () => {
try {
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline';
const conn = await mongoose.connect(mongoUri, {
// Remove deprecated options for newer MongoDB versions
});
logger.info(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
logger.error('Database connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;

View File

@ -0,0 +1,131 @@
const logger = require('../utils/logger');
const productionConfig = {
// Server settings
port: process.env.PORT || 3001,
// Database settings
database: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline',
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
}
},
// JWT settings
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '24h',
refreshExpiresIn: '7d'
},
// Redis settings (for sessions and caching)
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
options: {
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
}
},
// CORS settings
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
},
// Rate limiting
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
},
// Socket.IO settings
socketio: {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
methods: ['GET', 'POST']
},
pingTimeout: 60000,
pingInterval: 25000,
maxHttpBufferSize: 1e8, // 100 MB
},
// Logging settings
logging: {
level: process.env.LOG_LEVEL || 'info',
format: process.env.NODE_ENV === 'production' ? 'json' : 'simple',
file: {
enabled: true,
filename: 'logs/app.log',
maxsize: 10485760, // 10MB
maxFiles: 5,
}
},
// Security settings
security: {
helmet: {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
},
compression: {
level: 6,
threshold: 1024,
}
},
// Game settings
game: {
maxPlayersPerServer: 50,
serverCleanupInterval: 300000, // 5 minutes
inactivePlayerTimeout: 1800000, // 30 minutes
autoSaveInterval: 60000, // 1 minute
}
};
// Validate required environment variables
const validateConfig = () => {
const required = ['JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
logger.error(`Missing required environment variables: ${missing.join(', ')}`);
process.exit(1);
}
if (process.env.NODE_ENV === 'production') {
const prodRequired = ['MONGODB_URI', 'CLIENT_URL'];
const prodMissing = prodRequired.filter(key => !process.env[key]);
if (prodMissing.length > 0) {
logger.error(`Missing required production environment variables: ${prodMissing.join(', ')}`);
process.exit(1);
}
}
};
module.exports = {
...productionConfig,
validateConfig
};

View File

@ -0,0 +1,134 @@
const logger = require('../utils/logger');
// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class AuthenticationError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message = 'Access denied') {
super(message, 403);
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
}
}
class DatabaseError extends AppError {
constructor(message = 'Database operation failed') {
super(message, 500);
}
}
// Error handling middleware
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
logger.error({
error: err,
request: {
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.userId
}
});
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message).join(', ');
error = new ValidationError(message);
}
// Mongoose duplicate key error
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
const value = err.keyValue[field];
error = new ConflictError(`${field} '${value}' already exists`);
}
// Mongoose cast error
if (err.name === 'CastError') {
error = new ValidationError(`Invalid ${err.path}: ${err.value}`);
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
error = new AuthenticationError('Invalid token');
}
if (err.name === 'TokenExpiredError') {
error = new AuthenticationError('Token expired');
}
// Default error
if (!error.isOperational) {
error = new AppError('Something went wrong', 500);
}
res.status(error.statusCode || 500).json({
status: error.status || 'error',
message: error.message,
...(process.env.NODE_ENV === 'development' && {
stack: error.stack,
error: err
})
});
};
// Async error wrapper
const catchAsync = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// 404 handler
const notFound = (req, res, next) => {
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
next(error);
};
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
DatabaseError,
errorHandler,
catchAsync,
notFound
};

View File

@ -0,0 +1,134 @@
const mongoose = require('mongoose');
const gameServerSchema = new mongoose.Schema({
serverId: {
type: String,
required: true,
unique: true
},
name: {
type: String,
required: true
},
type: {
type: String,
enum: ['public', 'private'],
default: 'public'
},
region: {
type: String,
default: 'us-east'
},
maxPlayers: {
type: Number,
default: 10,
min: 1,
max: 100
},
currentPlayers: {
type: Number,
default: 0
},
owner: {
userId: { type: String, required: true },
username: { type: String, required: true }
},
settings: {
password: { type: String, default: null },
description: { type: String, default: '' },
tags: [{ type: String }]
},
status: {
type: String,
enum: ['waiting', 'active', 'full', 'offline'],
default: 'waiting'
},
gameServerUrl: {
type: String,
default: null
},
createdAt: {
type: Date,
default: Date.now
},
lastActivity: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Indexes for performance (only for non-unique fields)
gameServerSchema.index({ type: 1 });
gameServerSchema.index({ region: 1 });
gameServerSchema.index({ status: 1 });
gameServerSchema.index({ 'owner.userId': 1 });
// Methods
gameServerSchema.methods.addPlayer = function() {
if (this.currentPlayers < this.maxPlayers) {
this.currentPlayers += 1;
this.lastActivity = new Date();
if (this.currentPlayers >= this.maxPlayers) {
this.status = 'full';
} else if (this.currentPlayers > 0) {
this.status = 'active';
}
return true;
}
return false;
};
gameServerSchema.methods.removePlayer = function() {
if (this.currentPlayers > 0) {
this.currentPlayers -= 1;
this.lastActivity = new Date();
if (this.currentPlayers === 0) {
this.status = 'waiting';
} else if (this.currentPlayers < this.maxPlayers) {
this.status = 'active';
}
return true;
}
return false;
};
gameServerSchema.methods.isFull = function() {
return this.currentPlayers >= this.maxPlayers;
};
gameServerSchema.methods.canJoin = function() {
return this.status !== 'offline' && !this.isFull();
};
// Static methods
gameServerSchema.statics.findAvailableServers = function(filters = {}) {
const query = { status: { $ne: 'offline' } };
if (filters.type) {
query.type = filters.type;
}
if (filters.region) {
query.region = filters.region;
}
return this.find(query).sort({ lastActivity: -1 });
};
gameServerSchema.statics.cleanupOldServers = function(maxAge = 24 * 60 * 60 * 1000) { // 24 hours
const cutoffTime = new Date(Date.now() - maxAge);
return this.deleteMany({
$or: [
{ lastActivity: { $lt: cutoffTime }, currentPlayers: 0 },
{ status: 'offline', lastActivity: { $lt: cutoffTime } }
]
});
};
module.exports = mongoose.model('GameServer', gameServerSchema);

View File

@ -0,0 +1,306 @@
const mongoose = require('mongoose');
const inventorySchema = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'Player'
},
// Inventory settings
maxSlots: {
type: Number,
default: 50
},
// Items array
items: [{
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
type: {
type: String,
required: true,
enum: ['weapon', 'armor', 'material', 'consumable']
},
rarity: {
type: String,
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
default: 'common'
},
quantity: {
type: Number,
default: 1,
min: 1
},
// Item stats (for weapons/armor)
stats: {
attack: { type: Number, default: 0 },
defense: { type: Number, default: 0 },
speed: { type: Number, default: 0 },
criticalChance: { type: Number, default: 0 },
criticalDamage: { type: Number, default: 1.5 },
damage: { type: Number, default: 0 },
fireRate: { type: Number, default: 0 },
range: { type: Number, default: 0 },
energy: { type: Number, default: 0 },
health: { type: Number, default: 0 },
maxHealth: { type: Number, default: 0 },
durability: { type: Number, default: 0 },
weight: { type: Number, default: 0 },
energyShield: { type: Number, default: 0 }
},
// Item properties
description: {
type: String,
default: ''
},
// Equipment properties
equipable: {
type: Boolean,
default: false
},
slot: {
type: String,
enum: ['weapon', 'armor', 'engine', 'shield', 'special'],
default: null
},
isEquipped: {
type: Boolean,
default: false
},
// Consumable properties
consumable: {
type: Boolean,
default: false
},
effect: {
health: { type: Number, default: 0 },
energy: { type: Number, default: 0 },
attack: { type: Number, default: 0 },
defense: { type: Number, default: 0 },
speed: { type: Number, default: 0 },
duration: { type: Number, default: 0 }
},
// Stackable items
stackable: {
type: Boolean,
default: false
},
// Timestamps
acquiredAt: {
type: Date,
default: Date.now
},
lastUsed: {
type: Date,
default: null
}
}],
// Equipped items
equippedItems: {
weapon: { type: String, default: null },
armor: { type: String, default: null },
engine: { type: String, default: null },
shield: { type: String, default: null },
special: { type: String, default: null }
},
// Timestamps
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Indexes for performance
inventorySchema.index({ userId: 1 });
inventorySchema.index({ 'items.id': 1 });
inventorySchema.index({ 'items.type': 1 });
// Methods
inventorySchema.methods.addItem = function(itemData) {
// Check if item already exists and is stackable
const existingItem = this.items.find(item =>
item.id === itemData.id &&
item.type === itemData.type &&
item.stackable
);
if (existingItem) {
existingItem.quantity += itemData.quantity || 1;
} else {
// Add new item
const newItem = {
...itemData,
quantity: itemData.quantity || 1,
acquiredAt: new Date()
};
this.items.push(newItem);
}
this.updatedAt = new Date();
return this.save();
};
inventorySchema.methods.removeItem = function(itemId, quantity = 1) {
const itemIndex = this.items.findIndex(item => item.id === itemId);
if (itemIndex === -1) {
throw new Error('Item not found in inventory');
}
const item = this.items[itemIndex];
if (item.quantity > quantity) {
item.quantity -= quantity;
} else {
// Remove item completely
this.items.splice(itemIndex, 1);
// Unequip if it was equipped
Object.keys(this.equippedItems).forEach(slot => {
if (this.equippedItems[slot] === itemId) {
this.equippedItems[slot] = null;
}
});
}
this.updatedAt = new Date();
return this.save();
};
inventorySchema.methods.hasItem = function(itemId, quantity = 1) {
const item = this.items.find(item => item.id === itemId);
return item && item.quantity >= quantity;
};
inventorySchema.methods.getItemCount = function(itemId) {
const item = this.items.find(item => item.id === itemId);
return item ? item.quantity : 0;
};
inventorySchema.methods.equipItem = function(itemId, slot) {
const item = this.items.find(item => item.id === itemId);
if (!item) {
throw new Error('Item not found in inventory');
}
if (!item.equipable) {
throw new Error('Item is not equipable');
}
if (item.slot !== slot) {
throw new Error('Item cannot be equipped in this slot');
}
// Unequip current item in slot
if (this.equippedItems[slot]) {
const currentItem = this.items.find(item => item.id === this.equippedItems[slot]);
if (currentItem) {
currentItem.isEquipped = false;
}
}
// Equip new item
this.equippedItems[slot] = itemId;
item.isEquipped = true;
item.lastUsed = new Date();
this.updatedAt = new Date();
return this.save();
};
inventorySchema.methods.unequipItem = function(slot) {
const itemId = this.equippedItems[slot];
if (!itemId) {
throw new Error('No item equipped in this slot');
}
const item = this.items.find(item => item.id === itemId);
if (item) {
item.isEquipped = false;
}
this.equippedItems[slot] = null;
this.updatedAt = new Date();
return this.save();
};
inventorySchema.methods.useConsumable = function(itemId) {
const item = this.items.find(item => item.id === itemId);
if (!item) {
throw new Error('Item not found in inventory');
}
if (!item.consumable) {
throw new Error('Item is not consumable');
}
if (item.quantity <= 0) {
throw new Error('No quantity left');
}
// Apply effects
const effects = { ...item.effect };
// Remove one from quantity
item.quantity -= 1;
item.lastUsed = new Date();
// Remove item if quantity is 0
if (item.quantity === 0) {
const itemIndex = this.items.findIndex(item => item.id === itemId);
this.items.splice(itemIndex, 1);
}
this.updatedAt = new Date();
this.save();
return effects;
};
inventorySchema.methods.getInventorySummary = function() {
const summary = {
totalItems: this.items.length,
usedSlots: this.items.length,
maxSlots: this.maxSlots,
itemsByType: {},
equippedItems: this.equippedItems
};
// Count items by type
this.items.forEach(item => {
summary.itemsByType[item.type] = (summary.itemsByType[item.type] || 0) + item.quantity;
});
return summary;
};
inventorySchema.methods.getItemsByType = function(type) {
return this.items.filter(item => item.type === type);
};
inventorySchema.methods.getItemsByRarity = function(rarity) {
return this.items.filter(item => item.rarity === rarity);
};
module.exports = mongoose.model('Inventory', inventorySchema);

View File

@ -0,0 +1,155 @@
const mongoose = require('mongoose');
const playerSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
unique: true
},
username: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
// Authentication
password: {
type: String,
required: true,
select: false // Don't include password in queries by default
},
// Player stats (simplified for API server)
stats: {
level: { type: Number, default: 1 },
experience: { type: Number, default: 0 },
credits: { type: Number, default: 1000 },
dungeonsCleared: { type: Number, default: 0 },
playTime: { type: Number, default: 0 },
lastLogin: { type: Date, default: Date.now }
},
// Base attributes
attributes: {
health: { type: Number, default: 100 },
maxHealth: { type: Number, default: 100 },
energy: { type: Number, default: 100 },
maxEnergy: { type: Number, default: 100 },
attack: { type: Number, default: 10 },
defense: { type: Number, default: 5 },
speed: { type: Number, default: 10 },
criticalChance: { type: Number, default: 0.05 },
criticalDamage: { type: Number, default: 1.5 }
},
// Player info
info: {
name: { type: String, default: 'Commander' },
title: { type: String, default: 'Rookie Pilot' },
guild: { type: String, default: null },
rank: { type: String, default: 'Cadet' }
},
// Current ship
currentShip: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Ship'
},
// Settings
settings: {
autoSave: { type: Boolean, default: true },
notifications: { type: Boolean, default: true },
soundEffects: { type: Boolean, default: true },
music: { type: Boolean, default: false },
discordIntegration: { type: Boolean, default: false }
},
// Daily rewards
dailyRewards: {
lastClaim: { type: Date, default: null },
consecutiveDays: { type: Number, default: 0 }
},
// Server info
currentServer: { type: String, default: null },
// Timestamps
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}, {
timestamps: true
});
// Indexes for performance (only for non-unique fields)
playerSchema.index({ 'stats.level': 1 });
playerSchema.index({ currentServer: 1 });
// Methods (simplified for API server)
playerSchema.methods.addExperience = function(amount) {
this.stats.experience += amount;
return this.stats.experience;
};
playerSchema.methods.addCredits = function(amount) {
this.stats.credits += amount;
return this.stats.credits;
};
playerSchema.methods.canAfford = function(cost) {
return this.stats.credits >= cost;
};
playerSchema.methods.spendCredits = function(cost) {
if (this.canAfford(cost)) {
this.stats.credits -= cost;
return true;
}
return false;
};
playerSchema.methods.updatePlayTime = function(sessionTime) {
this.stats.playTime += sessionTime;
this.stats.lastLogin = new Date();
};
playerSchema.methods.claimDailyReward = function() {
const today = new Date();
const lastClaim = this.dailyRewards.lastClaim;
// Check if already claimed today
if (lastClaim && lastClaim.toDateString() === today.toDateString()) {
return { success: false, message: 'Daily reward already claimed today' };
}
// Check consecutive days
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (lastClaim && lastClaim.toDateString() === yesterday.toDateString()) {
this.dailyRewards.consecutiveDays += 1;
} else {
this.dailyRewards.consecutiveDays = 1;
}
this.dailyRewards.lastClaim = today;
// Calculate reward based on consecutive days
const baseReward = 100;
const consecutiveBonus = (this.dailyRewards.consecutiveDays - 1) * 50;
const totalReward = baseReward + consecutiveBonus;
this.addCredits(totalReward);
return {
success: true,
reward: totalReward,
consecutiveDays: this.dailyRewards.consecutiveDays
};
};
module.exports = mongoose.model('Player', playerSchema);

View File

@ -0,0 +1,189 @@
const mongoose = require('mongoose');
const shipSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'Player'
},
// Ship identification
id: {
type: String,
required: true,
unique: true
},
name: {
type: String,
required: true
},
class: {
type: String,
required: true,
enum: ['Fighter', 'Cruiser', 'Battleship', 'Carrier', 'Explorer']
},
level: {
type: Number,
default: 1
},
// Ship stats
stats: {
health: { type: Number, required: true },
maxHealth: { type: Number, required: true },
attack: { type: Number, required: true },
defense: { type: Number, required: true },
speed: { type: Number, required: true },
criticalChance: { type: Number, default: 0.05 },
criticalDamage: { type: Number, default: 1.5 },
hull: { type: Number, required: true }
},
// Ship appearance
texture: {
type: String,
required: true
},
// Ship progression
experience: {
type: Number,
default: 0
},
requiredExp: {
type: Number,
default: 100
},
upgrades: [{
type: String
}],
// Ship status
isEquipped: {
type: Boolean,
default: false
},
isCurrent: {
type: Boolean,
default: false
},
// Shop information (if purchased)
price: {
type: Number,
default: 0
},
rarity: {
type: String,
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
default: 'common'
},
description: {
type: String,
default: ''
},
// Timestamps
acquiredAt: {
type: Date,
default: Date.now
},
lastUsed: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Indexes for performance
shipSchema.index({ userId: 1 });
shipSchema.index({ id: 1 });
shipSchema.index({ isEquipped: 1 });
shipSchema.index({ isCurrent: 1 });
// Methods
shipSchema.methods.addExperience = function(amount) {
this.experience += amount;
// Level up logic
while (this.experience >= this.requiredExp) {
this.experience -= this.requiredExp;
this.level += 1;
this.requiredExp = this.level * 100;
// Increase stats on level up
this.stats.maxHealth += 10;
this.stats.health = this.stats.maxHealth;
this.stats.attack += 2;
this.stats.defense += 1;
this.stats.speed += 1;
}
return this.level;
};
shipSchema.methods.takeDamage = function(damage) {
const actualDamage = Math.max(0, damage - this.stats.defense);
this.stats.health = Math.max(0, this.stats.health - actualDamage);
if (this.stats.health === 0) {
this.isDestroyed = true;
}
return actualDamage;
};
shipSchema.methods.heal = function(amount) {
const healAmount = Math.min(amount, this.stats.maxHealth - this.stats.health);
this.stats.health += healAmount;
this.isDestroyed = false;
return healAmount;
};
shipSchema.methods.isAlive = function() {
return this.stats.health > 0;
};
shipSchema.methods.getStatSummary = function() {
return {
name: this.name,
class: this.class,
level: this.level,
health: `${this.stats.health}/${this.stats.maxHealth}`,
attack: this.stats.attack,
defense: this.stats.defense,
speed: this.stats.speed,
criticalChance: `${(this.stats.criticalChance * 100).toFixed(1)}%`,
criticalDamage: `${this.stats.criticalDamage}x`
};
};
shipSchema.methods.upgrade = function(upgradeType) {
switch (upgradeType) {
case 'health':
this.stats.maxHealth += 20;
this.stats.health = this.stats.maxHealth;
break;
case 'attack':
this.stats.attack += 5;
break;
case 'defense':
this.stats.defense += 3;
break;
case 'speed':
this.stats.speed += 2;
break;
default:
throw new Error('Unknown upgrade type');
}
if (!this.upgrades.includes(upgradeType)) {
this.upgrades.push(upgradeType);
}
this.lastUsed = new Date();
};
module.exports = mongoose.model('Ship', shipSchema);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "galaxystrikeonline-server",
"version": "1.0.0",
"description": "Galaxy Strike Online - Server Backend",
"license": "MIT",
"author": "Korvarix Studios",
"type": "commonjs",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"debug": "node --inspect server.js",
"test": "jest",
"migrate": "node scripts/migrate.js",
"seed": "node scripts/seed.js"
},
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"dotenv": "^16.3.1",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.3",
"winston": "^3.11.0",
"joi": "^17.11.0",
"rate-limiter-flexible": "^2.4.2",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.3"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,214 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const Joi = require('joi');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const Player = require('../models/Player');
const logger = require('../utils/logger');
const router = express.Router();
// Rate limiting for auth routes
const authLimiter = new RateLimiterMemory({
keyGenerator: (req) => req.ip,
points: 5, // Number of requests
duration: 900, // Per 15 minutes (900 seconds)
blockDuration: 900, // Block for 15 minutes
message: 'Too many authentication attempts, please try again later.'
});
// Validation schemas
const registerSchema = Joi.object({
username: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
// Register route
router.post('/register', async (req, res) => {
try {
// Rate limiting check
const resLimiter = await authLimiter.consume(req.ip);
if (!resLimiter.remainingPoints) {
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
}
const { error } = registerSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await Player.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
error: 'User with this email or username already exists'
});
}
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new player
const player = new Player({
userId: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
username,
email,
password: hashedPassword,
createdAt: new Date(),
lastLogin: new Date()
});
await player.save();
// Create JWT token
const token = jwt.sign(
{ userId: player.userId, email: player.email },
process.env.JWT_SECRET || 'fallback_secret',
{ expiresIn: '24h' }
);
logger.info(`New user registered: ${email}`);
res.status(201).json({
message: 'User registered successfully',
token,
user: {
userId: player.userId,
username: player.username,
email: player.email,
stats: player.stats
}
});
} catch (error) {
logger.error('Registration error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Login route
router.post('/login', async (req, res) => {
try {
// Rate limiting check
const resLimiter = await authLimiter.consume(req.ip);
if (!resLimiter.remainingPoints) {
return res.status(429).json({ error: 'Too many authentication attempts, please try again later.' });
}
const { error } = loginSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
const { email, password } = req.body;
// Find user
const player = await Player.findOne({ email }).select('+password');
if (!player) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if password exists (for backward compatibility with existing users)
if (!player.password) {
logger.error('Player password field is missing for user:', email);
return res.status(401).json({
error: 'Account migration required. Please re-register your account.',
requiresMigration: true
});
}
// Check password
const isPasswordValid = await bcrypt.compare(password, player.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Update last login
player.stats.lastLogin = new Date();
await player.save();
// Create JWT token
const token = jwt.sign(
{ userId: player.userId, email: player.email },
process.env.JWT_SECRET || 'fallback_secret',
{ expiresIn: '24h' }
);
logger.info(`User logged in: ${email}`);
res.json({
message: 'Login successful',
token,
user: {
userId: player.userId,
username: player.username,
email: player.email,
stats: player.stats,
info: player.info
}
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Verify token route
router.get('/verify', async (req, res) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
const player = await Player.findOne({ userId: decoded.userId });
if (!player) {
return res.status(401).json({ error: 'Invalid token' });
}
res.json({
valid: true,
user: {
userId: player.userId,
username: player.username,
email: player.email,
stats: player.stats,
info: player.info
}
});
} catch (error) {
logger.error('Token verification error:', error);
res.status(401).json({ error: 'Invalid token' });
}
});
// Logout route
router.post('/logout', async (req, res) => {
try {
// In a real implementation, you might want to blacklist the token
// For now, we'll just return success
res.json({ message: 'Logout successful' });
} catch (error) {
logger.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@ -0,0 +1,419 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const GameServer = require('../models/GameServer');
const logger = require('../utils/logger');
const router = express.Router();
// Middleware to authenticate JWT token
const authenticateToken = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
req.userId = decoded.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Register new game server (for GameServer instances to register themselves)
router.post('/register', async (req, res) => {
try {
const { serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner } = req.body;
logger.info(`[API SERVER] Game server registration request:`, {
serverId, name, type, region, maxPlayers, currentPlayers, gameServerUrl, owner
});
// Check if server already exists
const existingServer = await GameServer.findOne({ serverId });
if (existingServer) {
// Update existing server
existingServer.name = name || existingServer.name;
existingServer.type = type || existingServer.type;
existingServer.region = region || existingServer.region;
existingServer.maxPlayers = maxPlayers || existingServer.maxPlayers;
existingServer.currentPlayers = currentPlayers !== undefined ? currentPlayers : existingServer.currentPlayers;
existingServer.gameServerUrl = gameServerUrl || existingServer.gameServerUrl;
existingServer.status = 'waiting';
existingServer.lastActivity = new Date();
await existingServer.save();
logger.info(`[API SERVER] Updated existing server: ${serverId} with ${existingServer.currentPlayers} players`);
return res.json({
success: true,
message: 'Server updated successfully',
server: existingServer
});
}
// Create new server
const newServer = new GameServer({
serverId,
name: name || `Game Server ${serverId}`,
type: type || 'public',
region: region || 'us-east',
maxPlayers: maxPlayers || 10,
currentPlayers: currentPlayers !== undefined ? currentPlayers : 0,
owner: owner || {
userId: 'system',
username: 'System'
},
status: 'waiting',
gameServerUrl,
createdAt: new Date(),
lastActivity: new Date()
});
await newServer.save();
logger.info(`[API SERVER] Registered new server: ${serverId}`);
res.status(201).json({
success: true,
message: 'Server registered successfully',
server: newServer
});
} catch (error) {
logger.error('[API SERVER] Error registering server:', error);
res.status(500).json({
success: false,
error: 'Failed to register server'
});
}
});
// Update server status (for GameServer instances to update their status)
router.post('/update-status/:serverId', async (req, res) => {
try {
const { serverId } = req.params;
const { currentPlayers, status } = req.body;
const server = await GameServer.findOne({ serverId });
if (!server) {
return res.status(404).json({
success: false,
error: 'Server not found'
});
}
if (currentPlayers !== undefined) {
server.currentPlayers = currentPlayers;
}
if (status) {
server.status = status;
}
server.lastActivity = new Date();
await server.save();
logger.info(`[API SERVER] Updated server ${serverId} status:`, {
currentPlayers: server.currentPlayers,
status: server.status
});
res.json({
success: true,
message: 'Server status updated successfully'
});
} catch (error) {
logger.error('[API SERVER] Error updating server status:', error);
res.status(500).json({
success: false,
error: 'Failed to update server status'
});
}
});
// Unregister game server (for GameServer instances to unregister themselves)
router.delete('/unregister/:serverId', async (req, res) => {
try {
const { serverId } = req.params;
logger.info(`[API SERVER] Game server unregistration request:`, { serverId });
// Find and remove server
const server = await GameServer.findOneAndDelete({ serverId });
if (!server) {
return res.status(404).json({
success: false,
error: 'Server not found'
});
}
logger.info(`[API SERVER] Unregistered server: ${serverId}`);
res.json({
success: true,
message: 'Server unregistered successfully'
});
} catch (error) {
logger.error('[API SERVER] Error unregistering server:', error);
res.status(500).json({
success: false,
error: 'Failed to unregister server'
});
}
});
// Get server list
router.get('/', authenticateToken, async (req, res) => {
try {
const { type, region } = req.query;
// Build filters
const filters = {};
if (type) filters.type = type;
if (region) filters.region = region;
logger.info(`[API SERVER] Fetching servers for user ${req.userId} with filters:`, filters);
// Get available servers from database
const servers = await GameServer.findAvailableServers(filters);
logger.info(`[API SERVER] Found ${servers.length} servers in database`);
// Format server list for client
const serverList = servers.map(server => ({
id: server.serverId,
name: server.name,
type: server.type,
region: server.region,
currentPlayers: server.currentPlayers,
maxPlayers: server.maxPlayers,
status: server.status,
ownerName: server.owner.username,
createdAt: server.createdAt,
lastActivity: server.lastActivity
}));
logger.info(`[API SERVER] Returning ${serverList.length} servers to client`);
res.json({
success: true,
servers: serverList,
totalServers: serverList.length
});
} catch (error) {
logger.error('Error getting server list:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Create new server
router.post('/create', authenticateToken, async (req, res) => {
try {
const { name, type = 'public', maxPlayers = 10, region = 'us-east', settings = {} } = req.body;
if (!name) {
return res.status(400).json({ error: 'Server name required' });
}
// Generate unique server ID
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Get user info from token (you might want to fetch full user data)
const ownerUsername = req.body.username || 'Unknown'; // This should come from user data
// Create new server in database
const newServer = new GameServer({
serverId,
name,
type,
region,
maxPlayers,
owner: {
userId: req.userId,
username: ownerUsername
},
settings,
gameServerUrl: process.env.GAME_SERVER_URL || 'https://api.korvarix.com'
});
await newServer.save();
logger.info(`Server created: ${serverId} by user ${req.userId}`);
res.status(201).json({
message: 'Server created successfully',
server: {
id: newServer.serverId,
name: newServer.name,
type: newServer.type,
region: newServer.region,
currentPlayers: newServer.currentPlayers,
maxPlayers: newServer.maxPlayers,
status: newServer.status,
ownerName: newServer.owner.username,
createdAt: newServer.createdAt
}
});
} catch (error) {
logger.error('Error creating server:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Join server
router.post('/:serverId/join', authenticateToken, async (req, res) => {
try {
const { serverId } = req.params;
// Find server in database
const server = await GameServer.findOne({ serverId });
if (!server) {
return res.status(404).json({ error: 'Server not found' });
}
// Check if server can be joined
if (!server.canJoin()) {
return res.status(400).json({ error: 'Server is full or offline' });
}
// Add player to server
const playerAdded = server.addPlayer();
if (!playerAdded) {
return res.status(400).json({ error: 'Server is full' });
}
await server.save();
logger.info(`User ${req.userId} joined server ${serverId}`);
res.json({
message: 'Joined server successfully',
server: {
id: server.serverId,
name: server.name,
currentPlayers: server.currentPlayers,
maxPlayers: server.maxPlayers,
gameServerUrl: server.gameServerUrl
}
});
} catch (error) {
logger.error('Error joining server:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Leave server
router.post('/:serverId/leave', authenticateToken, async (req, res) => {
try {
const { serverId } = req.params;
// Find server in database
const server = await GameServer.findOne({ serverId });
if (!server) {
return res.status(404).json({ error: 'Server not found' });
}
// Remove player from server
const playerRemoved = server.removePlayer();
if (playerRemoved) {
await server.save();
logger.info(`User ${req.userId} left server ${serverId}`);
}
// Update player's current server
const Player = require('../models/Player');
await Player.findOneAndUpdate(
{ userId: req.userId },
{ currentServer: null }
);
res.json({
message: 'Left server successfully'
});
} catch (error) {
logger.error('Error leaving server:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get server details
router.get('/:serverId', authenticateToken, async (req, res) => {
try {
const { serverId } = req.params;
const server = await GameServer.findOne({ serverId });
if (!server) {
return res.status(404).json({ error: 'Server not found' });
}
res.json({
server: {
id: server.serverId,
name: server.name,
type: server.type,
region: server.region,
currentPlayers: server.currentPlayers,
maxPlayers: server.maxPlayers,
status: server.status,
ownerName: server.owner.username,
settings: server.settings,
createdAt: server.createdAt,
lastActivity: server.lastActivity
}
});
} catch (error) {
logger.error('Error getting server details:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get user's current server
router.get('/user/current', authenticateToken, async (req, res) => {
try {
const Player = require('../models/Player');
const player = await Player.findOne({ userId: req.userId });
if (!player || !player.currentServer) {
return res.json({ currentServer: null });
}
const server = await GameServer.findOne({ serverId: player.currentServer });
if (!server) {
// Clear invalid server reference
await Player.findOneAndUpdate(
{ userId: req.userId },
{ currentServer: null }
);
return res.json({ currentServer: null });
}
res.json({
currentServer: {
id: server.serverId,
name: server.name,
currentPlayers: server.currentPlayers,
maxPlayers: server.maxPlayers
}
});
} catch (error) {
logger.error('Error getting current server:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@ -0,0 +1,67 @@
/**
* Create Test Server Script
* Adds a test server to the database for testing the server browser
*/
const mongoose = require('mongoose');
const GameServer = require('../models/GameServer');
require('dotenv').config();
async function createTestServer() {
try {
// Connect to database
await mongoose.connect(process.env.MONGODB_URI);
console.log('Connected to database');
// Check if test server already exists
const existingServer = await GameServer.findOne({ serverId: 'test_server_001' });
if (existingServer) {
console.log('Test server already exists, deleting it first...');
await GameServer.deleteOne({ serverId: 'test_server_001' });
}
// Create test server
const testServer = new GameServer({
serverId: 'test_server_001',
name: 'Test Server - Galaxy Strike',
type: 'public',
region: 'us-east',
maxPlayers: 10,
currentPlayers: 2,
owner: {
userId: 'test_user_001',
username: 'TestAdmin'
},
settings: {
description: 'A test server for Galaxy Strike Online',
tags: ['test', 'beginner', 'pve']
},
status: 'active',
gameServerUrl: 'https://api.korvarix.com'
});
await testServer.save();
console.log('Test server created successfully!');
console.log('Server details:', {
id: testServer.serverId,
name: testServer.name,
type: testServer.type,
region: testServer.region,
currentPlayers: testServer.currentPlayers,
maxPlayers: testServer.maxPlayers,
status: testServer.status
});
} catch (error) {
console.error('Error creating test server:', error);
} finally {
await mongoose.disconnect();
}
}
// Run the script
if (require.main === module) {
createTestServer();
}
module.exports = createTestServer;

View File

@ -0,0 +1,50 @@
const mongoose = require('mongoose');
const logger = require('../utils/logger');
require('dotenv').config();
async function migrate() {
try {
logger.info('Starting database migration...');
// Connect to database
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
logger.info('Connected to database');
// Create indexes for performance
const db = mongoose.connection.db;
// Player indexes
await db.collection('players').createIndex({ userId: 1 }, { unique: true });
await db.collection('players').createIndex({ email: 1 }, { unique: true });
await db.collection('players').createIndex({ 'stats.level': 1 });
await db.collection('players').createIndex({ currentServer: 1 });
// Ship indexes
await db.collection('ships').createIndex({ userId: 1 });
await db.collection('ships').createIndex({ id: 1 }, { unique: true });
await db.collection('ships').createIndex({ isEquipped: 1 });
await db.collection('ships').createIndex({ isCurrent: 1 });
// Inventory indexes
await db.collection('inventories').createIndex({ userId: 1 }, { unique: true });
await db.collection('inventories').createIndex({ 'items.id': 1 });
await db.collection('inventories').createIndex({ 'items.type': 1 });
logger.info('Database migration completed successfully');
// Close connection
await mongoose.connection.close();
logger.info('Database connection closed');
} catch (error) {
logger.error('Migration failed:', error);
process.exit(1);
}
}
if (require.main === module) {
migrate();
}
module.exports = migrate;

View File

@ -0,0 +1,71 @@
/**
* Password Migration Script
* Updates existing users to have password fields
*/
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Player = require('../models/Player');
const logger = require('../utils/logger');
require('dotenv').config();
async function migratePasswords() {
try {
// Connect to database
await mongoose.connect(process.env.MONGODB_URI);
logger.info('Connected to database for password migration');
// Find all users without passwords
const usersWithoutPasswords = await Player.find({
password: { $exists: false }
});
logger.info(`Found ${usersWithoutPasswords.length} users without passwords`);
if (usersWithoutPasswords.length === 0) {
logger.info('No users need password migration');
return;
}
// Update each user with a default password
for (const user of usersWithoutPasswords) {
// Generate a default password (you might want to use a different approach)
const defaultPassword = 'tempPassword123!';
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(defaultPassword, salt);
await Player.updateOne(
{ _id: user._id },
{
$set: {
password: hashedPassword,
'stats.lastLogin': new Date()
}
}
);
logger.info(`Migrated user: ${user.email} with default password`);
}
logger.info('Password migration completed successfully');
// Output the default password for users to change
console.log('\n=== MIGRATION COMPLETE ===');
console.log(`Updated ${usersWithoutPasswords.length} users`);
console.log('Default password for all migrated users: tempPassword123!');
console.log('Users should change their password after first login\n');
} catch (error) {
logger.error('Password migration error:', error);
console.error('Migration failed:', error);
} finally {
await mongoose.disconnect();
}
}
// Run the migration
if (require.main === module) {
migratePasswords();
}
module.exports = migratePasswords;

View File

@ -0,0 +1,196 @@
const mongoose = require('mongoose');
const logger = require('../utils/logger');
const Player = require('../models/Player');
const Ship = require('../models/Ship');
const Inventory = require('../models/Inventory');
require('dotenv').config();
async function seed() {
try {
logger.info('Starting database seeding...');
// Connect to database
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline');
logger.info('Connected to database');
// Clear existing data (optional - remove if you want to preserve data)
logger.info('Clearing existing data...');
await Player.deleteMany({});
await Ship.deleteMany({});
await Inventory.deleteMany({});
// Create a test user
const testUser = new Player({
userId: 'test_user_001',
username: 'TestPlayer',
email: 'test@example.com',
password: '$2a$10$example_hashed_password_here',
stats: {
level: 1,
experience: 0,
credits: 5000,
dungeonsCleared: 0,
playTime: 0,
lastLogin: new Date()
},
attributes: {
health: 100,
maxHealth: 100,
energy: 100,
maxEnergy: 100,
attack: 10,
defense: 5,
speed: 10,
criticalChance: 0.05,
criticalDamage: 1.5
},
info: {
name: 'Commander',
title: 'Rookie Pilot',
guild: null,
rank: 'Cadet'
},
settings: {
autoSave: true,
notifications: true,
soundEffects: true,
music: false,
discordIntegration: false
},
dailyRewards: {
lastClaim: null,
consecutiveDays: 0
}
});
await testUser.save();
logger.info('Created test user');
// Create starter ship for test user
const starterShip = new Ship({
userId: testUser.userId,
id: 'starter_cruiser_001',
name: 'Starter Cruiser',
class: 'Cruiser',
level: 1,
stats: {
health: 100,
maxHealth: 100,
attack: 15,
defense: 12,
speed: 10,
criticalChance: 0.05,
criticalDamage: 1.5,
hull: 100
},
texture: 'assets/textures/ships/starter_cruiser.png',
experience: 0,
requiredExp: 100,
upgrades: [],
isEquipped: true,
isCurrent: true,
price: 5000,
rarity: 'common',
description: 'Reliable starter cruiser for new pilots',
acquiredAt: new Date(),
lastUsed: new Date()
});
await starterShip.save();
logger.info('Created starter ship');
// Update player with current ship
testUser.currentShip = starterShip._id;
await testUser.save();
// Create inventory for test user
const inventory = new Inventory({
userId: testUser.userId,
maxSlots: 50,
items: [
{
id: 'starter_blaster_common',
name: 'Common Blaster',
type: 'weapon',
rarity: 'common',
quantity: 1,
stats: {
attack: 5,
criticalChance: 0.02,
damage: 10,
fireRate: 2,
range: 5,
energy: 5
},
description: 'A reliable basic blaster for new pilots',
equipable: true,
slot: 'weapon',
isEquipped: false,
stackable: false,
acquiredAt: new Date()
},
{
id: 'basic_armor_common',
name: 'Basic Armor',
type: 'armor',
rarity: 'common',
quantity: 1,
stats: {
defense: 3,
durability: 20,
weight: 2,
energyShield: 0
},
description: 'Light armor providing basic protection',
equipable: true,
slot: 'armor',
isEquipped: false,
stackable: false,
acquiredAt: new Date()
},
{
id: 'health_kit',
name: 'Health Kit',
type: 'consumable',
rarity: 'common',
quantity: 3,
stats: {},
description: 'A medical kit that restores health',
consumable: true,
effect: {
health: 50
},
stackable: true,
acquiredAt: new Date()
}
],
equippedItems: {
weapon: null,
armor: null,
engine: null,
shield: null,
special: null
}
});
await inventory.save();
logger.info('Created inventory with starter items');
logger.info('Database seeding completed successfully');
// Close connection
await mongoose.connection.close();
logger.info('Database connection closed');
} catch (error) {
logger.error('Seeding failed:', error);
process.exit(1);
}
}
if (require.main === module) {
seed();
}
module.exports = seed;

View File

@ -0,0 +1,234 @@
const express = require('express');
const http = require('http');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('rate-limiter-flexible');
require('dotenv').config();
const logger = require('./utils/logger');
const connectDB = require('./config/database');
const authRoutes = require('./routes/auth');
const serverRoutes = require('./routes/servers');
const { errorHandler, notFound } = require('./middleware/errorHandler');
const GameServer = require('./models/GameServer');
// Override console.error to properly log error objects
const originalConsoleError = console.error;
console.error = (...args) => {
args.forEach(arg => {
if (arg instanceof Error) {
logger.error('Console Error:', {
message: arg.message,
stack: arg.stack,
name: arg.name
});
} else if (typeof arg === 'object' && arg !== null) {
logger.error('Console Error Object:', arg);
} else {
logger.error('Console Error:', arg);
}
});
};
const app = express();
const server = http.createServer(app);
// Middleware
app.use(helmet());
app.use(compression());
app.use(cors({
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://api.korvarix.com:3001", "https://dev.gameserver.galaxystrike.online"],
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Static file serving
app.use(express.static('../Website/dist'));
// Rate limiting (more lenient for development)
const { RateLimiterMemory } = require('rate-limiter-flexible');
const limiter = new RateLimiterMemory({
keyGenerator: (req) => req.ip,
points: 1000, // limit each IP to 1000 requests per windowMs (increased from 100)
duration: 60, // 1 minute window (reduced from 15 minutes)
blockDuration: 60, // Block for 1 minute (reduced from 15 minutes)
});
app.use('/api/', async (req, res, next) => {
try {
// Skip rate limiting for localhost in development
const isLocalhost = req.ip === '127.0.0.1' || req.ip === '::1' || req.hostname === 'localhost';
if (!isLocalhost) {
const resLimiter = await limiter.consume(req.ip);
if (!resLimiter.remainingPoints) {
return res.status(429).json({ error: 'Too many requests, please try again later.' });
}
}
next();
} catch (rejRes) {
// Handle rate limit exceeded
const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;
res.set('Retry-After', String(secs));
res.status(429).json({ error: 'Too many requests, please try again later.' });
}
});
// Routes - API Server Only (Auth + Server Browser)
app.use('/api/auth', authRoutes);
app.use('/api/servers', serverRoutes);
// Manual cleanup endpoint (for testing)
app.post('/api/admin/cleanup-dead-servers', async (req, res) => {
try {
await cleanupDeadServers();
res.json({ success: true, message: 'Dead server cleanup completed' });
} catch (error) {
logger.error('Manual cleanup error:', error);
res.status(500).json({ success: false, error: 'Cleanup failed' });
}
});
// Health check
app.get('/health', (req, res) => {
res.status(200).json({
status: 'API Server OK',
service: 'galaxystrikeonline-api',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// API version endpoint
app.get('/api/ssc/version', (req, res) => {
res.status(200).json({
version: '1.0.0',
service: 'galaxystrikeonline-api',
timestamp: new Date().toISOString()
});
});
// Fallback route for SPA - only serve index.html for non-API routes
app.get('*', (req, res) => {
// Don't try to serve index.html for API routes
if (req.path.startsWith('/api/')) {
return res.status(404).json({ error: 'API endpoint not found' });
}
// Try dist first (for built files), fallback to public (for development)
const distPath = require('path').resolve(__dirname, '../dist/index.html');
const publicPath = require('path').resolve(__dirname, '../public/index.html');
const fs = require('fs');
if (fs.existsSync(distPath)) {
res.sendFile(distPath);
} else if (fs.existsSync(publicPath)) {
res.sendFile(publicPath);
} else {
res.status(404).json({ error: 'Frontend not found' });
}
});
// Error handling
app.use(notFound);
app.use(errorHandler);
// Clean up dead servers
async function cleanupDeadServers() {
try {
logger.info('[API SERVER] Starting dead server cleanup...');
// Find servers that haven't been updated in the last 5 minutes
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const deadServers = await GameServer.find({
lastActivity: { $lt: fiveMinutesAgo }
});
if (deadServers.length > 0) {
logger.info(`[API SERVER] Found ${deadServers.length} potentially dead servers, checking...`);
for (const server of deadServers) {
const isAlive = await checkServerHealth(server.gameServerUrl);
if (!isAlive) {
logger.info(`[API SERVER] Removing dead server: ${server.name} (${server.serverId})`);
await GameServer.deleteOne({ _id: server._id });
} else {
logger.info(`[API SERVER] Server ${server.name} is still alive, updating lastActivity`);
server.lastActivity = new Date();
await server.save();
}
}
} else {
logger.info('[API SERVER] No dead servers found');
}
} catch (error) {
logger.error('[API SERVER] Error during dead server cleanup:', error);
}
}
// Check if a server is healthy
async function checkServerHealth(serverUrl) {
try {
if (!serverUrl) {
return false;
}
// Add /health endpoint to the URL
const healthUrl = serverUrl.endsWith('/') ? `${serverUrl}health` : `${serverUrl}/health`;
const response = await fetch(healthUrl, {
method: 'GET',
timeout: 5000 // 5 second timeout
});
return response.ok;
} catch (error) {
logger.warn(`[API SERVER] Health check failed for ${serverUrl}:`, error.message);
return false;
}
}
// Initialize database only (no game systems for API server)
async function startServer() {
try {
// Connect to database
await connectDB();
logger.info('Database connected successfully');
// Start API server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
logger.info(`API Server running on port ${PORT}`);
logger.info('API Server handles: Authentication, Server Browser, User Data');
// Start dead server cleanup (every 2 minutes)
setInterval(cleanupDeadServers, 120000);
});
} catch (error) {
logger.error('Failed to start API server:', error);
process.exit(1);
}
}
// Handle uncaught errors
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// Handle HTTP server errors
server.on('error', (error) => {
logger.error('HTTP Server error:', error);
});
startServer();
module.exports = { app, server };

View File

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

View File

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

View File

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

View File

@ -0,0 +1,220 @@
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../server');
const Player = require('../models/Player');
describe('API Tests', () => {
let token;
let testUser;
beforeAll(async () => {
// Connect to test database
const mongoUri = process.env.MONGODB_TEST_URI || 'mongodb://localhost:27017/galaxystrikeonline_test';
await mongoose.connect(mongoUri);
});
afterAll(async () => {
// Clean up and close connection
await Player.deleteMany({});
await mongoose.connection.close();
});
beforeEach(async () => {
// Clean up before each test
await Player.deleteMany({});
});
describe('Authentication', () => {
test('POST /api/auth/register - should register new user', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('token');
expect(response.body.user).toHaveProperty('username', userData.username);
expect(response.body.user).toHaveProperty('email', userData.email);
});
test('POST /api/auth/login - should login existing user', async () => {
// First register a user
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
await request(app)
.post('/api/auth/register')
.send(userData);
// Then login
const loginData = {
email: userData.email,
password: userData.password
};
const response = await request(app)
.post('/api/auth/login')
.send(loginData)
.expect(200);
expect(response.body).toHaveProperty('token');
token = response.body.token;
testUser = response.body.user;
});
test('GET /api/auth/verify - should verify token', async () => {
// First login to get token
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const registerResponse = await request(app)
.post('/api/auth/register')
.send(userData);
token = registerResponse.body.token;
const response = await request(app)
.get('/api/auth/verify')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('valid', true);
expect(response.body.user).toHaveProperty('username', userData.username);
});
});
describe('Game API', () => {
beforeEach(async () => {
// Create and login a user for game tests
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: userData.email,
password: userData.password
});
token = loginResponse.body.token;
testUser = loginResponse.body.user;
});
test('GET /api/game/player - should get player data', async () => {
const response = await request(app)
.get('/api/game/player')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('userId');
expect(response.body).toHaveProperty('stats');
expect(response.body).toHaveProperty('attributes');
});
test('GET /api/game/ships - should get player ships', async () => {
const response = await request(app)
.get('/api/game/ships')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('ships');
expect(Array.isArray(response.body.ships)).toBe(true);
});
test('GET /api/game/inventory - should get player inventory', async () => {
const response = await request(app)
.get('/api/game/inventory')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('items');
expect(response.body).toHaveProperty('summary');
});
test('POST /api/game/daily-reward - should claim daily reward', async () => {
const response = await request(app)
.post('/api/game/daily-reward')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('success');
});
});
describe('Server API', () => {
beforeEach(async () => {
// Create and login a user for server tests
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: userData.email,
password: userData.password
});
token = loginResponse.body.token;
testUser = loginResponse.body.user;
});
test('GET /api/servers - should get server list', async () => {
const response = await request(app)
.get('/api/servers')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('servers');
expect(response.body).toHaveProperty('totalServers');
expect(Array.isArray(response.body.servers)).toBe(true);
});
test('POST /api/servers/create - should create new server', async () => {
const serverData = {
name: 'Test Server',
type: 'public',
maxPlayers: 10,
region: 'us-east'
};
const response = await request(app)
.post('/api/servers/create')
.set('Authorization', `Bearer ${token}`)
.send(serverData)
.expect(201);
expect(response.body).toHaveProperty('message');
expect(response.body.server).toHaveProperty('name', serverData.name);
expect(response.body.server).toHaveProperty('type', serverData.type);
});
});
describe('Health Check', () => {
test('GET /health - should return health status', async () => {
const response = await request(app)
.get('/health')
.expect(200);
expect(response.body).toHaveProperty('status', 'OK');
expect(response.body).toHaveProperty('timestamp');
expect(response.body).toHaveProperty('uptime');
});
});
});

View File

@ -0,0 +1,27 @@
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'galaxystrikeonline-server' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Add console transport for development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;

View File

@ -0,0 +1,120 @@
{
"_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 }
}

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ class GameInitializer {
this.currentUser = null;
this.socket = null;
this.apiBaseUrl = 'https://api.korvarix.com/api'; // API Server
this.gameServerUrl = 'https://dev.gameserver.galaxystrike.online'; // Game Server for Socket.IO (local dev server)
this.gameServerUrl = 'https://dev.gameserver.galaxystrike.online'; // Game Server for Socket.IO
console.log('[GAME INITIALIZER] Constructor - gameServerUrl set to:', this.gameServerUrl);
}
@ -70,12 +70,13 @@ class GameInitializer {
return;
}
// FORCE THE URL - Override any undefined issues
const FORCED_URL = 'https://dev.gameserver.galaxystrike.online';
console.log('[GAME INITIALIZER] FORCING URL to:', FORCED_URL);
console.log('[GAME INITIALIZER] Original this.gameServerUrl:', this.gameServerUrl);
// Resolve server URL: use serverData.url if set, otherwise gameServerUrl, then dev default
const FORCED_URL = (this.serverData && this.serverData.url)
? this.serverData.url
: (this.gameServerUrl || 'https://dev.gameserver.galaxystrike.online');
console.log('[GAME INITIALIZER] Connecting to:', FORCED_URL);
// Connect to the game server with FORCED URL
// Connect to the game server
this.socket = io(FORCED_URL, {
auth: {
token: this.authToken,
@ -83,7 +84,7 @@ class GameInitializer {
}
});
console.log('[GAME INITIALIZER] Socket.IO connection initiated to FORCED URL:', FORCED_URL);
console.log('[GAME INITIALIZER] Socket.IO connection initiated to:', FORCED_URL);
// Socket event handlers
this.socket.on('connect', () => {
@ -146,9 +147,117 @@ class GameInitializer {
console.log('[GAME INITIALIZER] Game data saved:', data);
this.onGameDataSaved(data);
});
// Idle rewards events
this.socket.on('offlineRewardsClaimed', (data) => {
console.log('[GAME INITIALIZER] Offline rewards claimed:', data);
this.onOfflineRewardsClaimed(data);
});
this.socket.on('onlineIdleRewards', (data) => {
if (data.credits > 0 || data.experience > 0) {
console.log('[GAME INITIALIZER] Online idle rewards received:', data);
}
this.onOnlineIdleRewards(data);
});
// Quest completion events
this.socket.on('quest_completed', (data) => {
console.log('[GAME INITIALIZER] Quest completed:', data);
this.onQuestCompleted(data);
});
// 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);
});
// Quest data events
this.socket.on('quests_data', (data) => {
console.log('[GAME INITIALIZER] Quest data received:', data);
this.onQuestsData(data);
});
// PlayTime events
this.socket.on('playTimeUpdated', (data) => {
// Only log playtime updates every minute (600,000 ms) to reduce spam
if (!this.lastPlayTimeLog || Date.now() - this.lastPlayTimeLog > 60000) {
console.log('[GAME INITIALIZER] PlayTime updated:', `${Math.floor(data.playTime / 3600000)}h ${Math.floor((data.playTime % 3600000) / 60000)}m`);
this.lastPlayTimeLog = Date.now();
}
this.onPlayTimeUpdated(data);
});
// Shop purchase events
this.socket.on('purchaseCompleted', (data) => {
console.log('[GAME INITIALIZER] Purchase completed:', data);
this.onPurchaseCompleted(data);
});
// Item system events
this.socket.on('shopItemsReceived', (data) => {
console.log('[GAME INITIALIZER] Shop items received:', data);
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);
});
}
onSocketConnected() {
// Expose socket globally for systems that need it
if (window.game) {
window.game.socket = this.socket;
}
// Join the server room
this.socket.emit('joinServer', {
serverId: this.serverData.id,
@ -232,6 +341,17 @@ class GameInitializer {
if (data.success && data.playerData) {
// Store server player data from authentication (this is our primary source)
this.serverPlayerData = data.playerData;
this.currentUser = data.user;
// CRITICAL: Force multiplayer mode and prevent fallback
if (window.smartSaveManager) {
console.log('[GAME INITIALIZER] FORCING multiplayer mode after authentication');
window.smartSaveManager.setMultiplayerMode(true, this);
}
// ItemSystem is now initialized by GameEngine - no need to initialize here
console.log('[GAME INITIALIZER] ItemSystem initialization handled by GameEngine');
console.log('[GAME INITIALIZER] Using authentication data as primary source:', this.serverPlayerData);
// NOW create GameEngine AFTER authentication is successful
@ -245,22 +365,12 @@ class GameInitializer {
window.smartSaveManager.setMultiplayerMode(true, this);
console.log('[GAME INITIALIZER] SmartSaveManager set to multiplayer mode');
// Apply authentication data immediately
// Apply authentication data immediately (this will be stored for later)
window.smartSaveManager.applyServerDataToGame(data.playerData);
}
// Fallback: Apply authentication data to game if game is running
if (window.game && window.game.loadServerPlayerData) {
console.log('[GAME INITIALIZER] Applying authentication data to GameEngine:', data.playerData);
window.game.loadServerPlayerData(data.playerData);
console.log('[GAME INITIALIZER] Authentication data applied to GameEngine');
// Force UI refresh when authentication data is applied
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
console.log('[GAME INITIALIZER] Forcing UI refresh after authentication data application');
window.game.systems.ui.forceRefreshAllUI();
}
}
// NOTE: Don't apply to GameEngine here - it doesn't exist yet!
// The data will be applied in createGameEngineForMultiplayer() after the game is created.
this.showNotification(`Welcome back! Level ${data.playerData.stats?.level || 1}`, 'success');
} else {
@ -276,11 +386,22 @@ class GameInitializer {
// Create GameEngine instance
window.game = new GameEngine();
// CRITICAL: Set multiplayer mode BEFORE initializing systems
console.log('[GAME INITIALIZER] Setting multiplayer mode BEFORE initialization');
window.game.setMultiplayerMode(true, this.socket, this.serverData, this.currentUser);
// NOTE: Don't apply server data immediately - wait for full initialization
console.log('[GAME INITIALIZER] Server data ready, will apply after GameEngine initialization');
console.log('[GAME INITIALIZER] - this.serverPlayerData:', !!this.serverPlayerData);
console.log('[GAME INITIALIZER] - window.game:', !!window.game);
console.log('[GAME INITIALIZER] - window.game.loadServerPlayerData:', !!window.game?.loadServerPlayerData);
// Initialize the game engine
console.log('[GAME INITIALIZER] About to call window.game.init()');
const initPromise = window.game.init();
console.log('[GAME INITIALIZER] GameEngine.init() returned:', typeof initPromise, initPromise);
// Apply server data and refresh UI after initialization is complete
initPromise.then(() => {
console.log('[GAME INITIALIZER] GameEngine initialized successfully for multiplayer');
@ -290,13 +411,23 @@ class GameInitializer {
window.game.loadServerPlayerData(this.serverPlayerData);
console.log('[GAME INITIALIZER] Server player data applied to GameEngine');
// CRITICAL: Force immediate economy sync
if (window.game.systems && window.game.systems.economy && window.game.systems.economy.syncWithServerData) {
console.log('[GAME INITIALIZER] Forcing immediate economy sync with server data');
window.game.systems.economy.syncWithServerData(this.serverPlayerData);
}
// Force UI refresh
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
console.log('[GAME INITIALIZER] Forcing UI refresh after data application');
window.game.systems.ui.forceRefreshAllUI();
} else {
console.warn('[GAME INITIALIZER] UI refresh not available - systems:', !!window.game.systems, 'ui:', !!window.game.systems?.ui, 'forceRefreshAllUI:', !!window.game.systems?.ui?.forceRefreshAllUI);
}
} else {
console.warn('[GAME INITIALIZER] No server player data or loadServerPlayerData method available');
console.log('[GAME INITIALIZER] - this.serverPlayerData:', !!this.serverPlayerData);
console.log('[GAME INITIALIZER] - window.game.loadServerPlayerData:', !!window.game?.loadServerPlayerData);
}
// Start the game
@ -322,11 +453,11 @@ class GameInitializer {
console.log('[GAME INITIALIZER] Data content:', data.data);
console.log('[GAME INITIALIZER] Data keys:', data.data ? Object.keys(data.data) : 'No data object');
if (data.success && data.data && Object.keys(data.data).length > 0) {
// Store server player data
// Only process if we don't already have good data from authentication
if (data.success && data.data && Object.keys(data.data).length > 0 && !this.serverPlayerData) {
console.log('[GAME INITIALIZER] Using gameDataLoaded as primary source (no auth data available)');
this.serverPlayerData = data.data;
console.log('[GAME INITIALIZER] Applying server data to game systems');
// Apply server data to game if game is running
if (window.game && window.game.loadServerPlayerData) {
window.game.loadServerPlayerData(data.data);
@ -335,17 +466,9 @@ class GameInitializer {
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
window.game.systems.ui.forceRefreshAllUI();
}
} else {
console.warn('[GAME INITIALIZER] No game or loadServerPlayerData method available');
}
} else {
console.log('[GAME INITIALIZER] Ignoring empty game data - no player data to load');
console.log('[GAME INITIALIZER] Data analysis:', {
success: data.success,
hasData: !!data.data,
dataKeys: data.data ? Object.keys(data.data) : [],
dataLength: data.data ? JSON.stringify(data.data).length : 0
});
console.log('[GAME INITIALIZER] Ignoring gameDataLoaded - already have data from authentication or data is empty');
}
}
@ -414,7 +537,7 @@ class GameInitializer {
// Configure game for multiplayer mode
console.log('[GAME INITIALIZER] Configuring for multiplayer mode');
window.game.setMultiplayerMode(true, this.socket, this.serverData, this.currentUser);
// Note: setMultiplayerMode already called in createGameEngineForMultiplayer() before initialization
window.game.gameInitializer = this; // Store reference for server polling
// DISABLE game logic in multiplayer - server is authoritative
@ -482,8 +605,24 @@ class GameInitializer {
updateUIForMultiplayerMode() {
// Update UI elements to show multiplayer mode
const playerName = document.getElementById('playerName');
if (playerName && this.currentUser) {
playerName.textContent = this.currentUser.username;
const playerTitle = document.getElementById('playerTitle');
const playerUsername = document.getElementById('playerUsername');
if (this.currentUser) {
// Set the player name (rank/title)
if (playerName) {
playerName.textContent = 'Commander';
}
// Set the player title
if (playerTitle) {
playerTitle.textContent = '- Rookie Pilot';
}
// Set the username next to the level
if (playerUsername) {
playerUsername.textContent = this.currentUser.username + ' ';
}
}
// Show multiplayer-specific UI elements
@ -666,13 +805,223 @@ class GameInitializer {
username: this.currentUser.username
});
} else {
console.warn('[GAME INITIALIZER] Cannot authenticate - missing socket or user data');
if (!this.socket) {
console.warn('[GAME INITIALIZER] Socket is null/undefined');
// Try to get from localStorage as fallback
const storedUser = localStorage.getItem('currentUser');
if (storedUser) {
try {
const user = JSON.parse(storedUser);
const username = user.username || 'anonymous';
} catch (e) {
console.warn('[GAME INITIALIZER] Failed to parse stored user, using default');
}
} else {
window.game.showNotification('No offline rewards available', 'info', 3000);
}
if (!this.currentUser) {
console.warn('[GAME INITIALIZER] Current user is null/undefined');
}
}
onOnlineIdleRewards(data) {
if (window.game && window.game.systems) {
// Only log if there are actual rewards to process
if (data.credits > 0 || data.experience > 0 || data.energy > 0) {
console.log('[GAME INITIALIZER] Processing online idle rewards:', data);
}
// Update player balance with online idle rewards
if (data.credits > 0) {
// Update the Economy system's local credits
if (window.game.systems.economy) {
window.game.systems.economy.credits += data.credits;
// Update UI immediately
if (window.game.ui) {
window.game.ui.updatePlayerStats();
console.log('[GAME INITIALIZER] UI updated with new credits');
}
} else {
console.warn('[GAME INITIALIZER] Economy system not available for credit update');
}
// Show notification
window.game.showNotification(`+${data.credits} credits (online idle)`, 'success', 2000);
}
// Update experience if provided
if (data.experience > 0 && window.game.systems.player) {
window.game.systems.player.addExperience(data.experience);
console.log('[GAME INITIALIZER] Added idle experience:', data.experience);
}
} else {
console.warn('[GAME INITIALIZER] Game systems not available for idle rewards');
}
}
onPlayTimeUpdated(data) {
// PlayTime updates are handled in the socket listener with throttled logging
if (window.game && window.game.systems && window.game.systems.player) {
const player = window.game.systems.player;
// Update playTime from server
player.stats.playTime = data.playTime;
// Update UI
player.updateUI();
}
}
onPurchaseCompleted(data) {
if (data.success) {
// Update local player data with server response
if (window.game && window.game.systems && window.game.systems.economy) {
const economy = window.game.systems.economy;
// Update currency balance
if (data.currency === 'credits') {
economy.credits = data.newBalance;
} else if (data.currency === 'gems') {
economy.gems = data.newBalance;
}
// Add item to inventory if it's a consumable or material
if (data.item && (data.item.type === 'consumable' || data.item.type === 'material')) {
if (window.game.systems.inventory) {
console.log('[GAME INITIALIZER] Adding item to inventory:', data.item);
window.game.systems.inventory.addItem(data.item, data.quantity || 1);
window.game.showNotification(`${data.item.name} added to inventory!`, 'success', 3000);
}
}
// Update owned ships if it's a ship
if (data.item && data.item.type === 'ship') {
if (!economy.ownedShips) economy.ownedShips = [];
if (!economy.ownedShips.includes(data.item.id)) {
economy.ownedShips.push(data.item.id);
console.log('[GAME INITIALIZER] Ship added to owned ships:', data.item.id);
// Add ship to BaseSystem ship gallery
if (window.game.systems.baseSystem) {
console.log('[GAME INITIALIZER] BaseSystem available, adding ship to gallery');
const shipData = {
id: data.item.id,
name: data.item.name,
class: data.item.name.replace(/\s+/g, '_').toLowerCase(), // Generate class from name
level: 1,
stats: data.item.stats || {},
texture: data.item.texturePath || `assets/textures/ships/${data.item.id}.png`,
isCurrent: false,
rarity: data.item.rarity || 'common'
};
console.log('[GAME INITIALIZER] Ship data prepared:', shipData);
// Initialize ship gallery if needed
if (!window.game.systems.baseSystem.purchasedShips) {
console.log('[GAME INITIALIZER] Initializing ship gallery');
window.game.systems.baseSystem.initializeShipGallery();
}
// Check if ship already exists
const existingShip = window.game.systems.baseSystem.purchasedShips.find(s => s.id === shipData.id);
if (!existingShip) {
// Add ship to gallery
window.game.systems.baseSystem.purchasedShips.push(shipData);
console.log('[GAME INITIALIZER] Ship added to gallery. Total ships:', window.game.systems.baseSystem.purchasedShips.length);
// Update the ship gallery UI
window.game.systems.baseSystem.updateShipGallery();
console.log('[GAME INITIALIZER] Ship gallery UI updated');
} else {
console.log('[GAME INITIALIZER] Ship already exists in gallery:', shipData.id);
}
} else {
console.error('[GAME INITIALIZER] BaseSystem not available for ship gallery');
}
}
}
// Update owned cosmetics if it's a cosmetic
if (data.item && data.item.type === 'cosmetic') {
if (!economy.ownedCosmetics) economy.ownedCosmetics = [];
if (!economy.ownedCosmetics.includes(data.item.id)) {
economy.ownedCosmetics.push(data.item.id);
console.log('[GAME INITIALIZER] Cosmetic added to owned cosmetics:', data.item.id);
}
}
// Request fresh economy data from server to ensure sync
if (economy.requestEconomyData) {
setTimeout(() => {
economy.requestEconomyData();
}, 500);
}
// Also request economy data immediately to prevent reset
if (economy.requestEconomyData) {
economy.requestEconomyData();
}
// Update UI
economy.updateUI();
// Update inventory UI if item was added
if (data.item && (data.item.type === 'consumable' || data.item.type === 'material')) {
if (window.game.systems.inventory) {
window.game.systems.inventory.updateUI();
}
}
// Show success message
window.game.showNotification(`Purchased ${data.item.name}!`, 'success', 3000);
}
} else {
// Show error message
window.game.showNotification(`Purchase failed: ${data.error}`, 'error', 5000);
}
}
onShopItemsReceived(data) {
if (data.success && window.game && window.game.systems && window.game.systems.itemSystem) {
console.log('[GAME INITIALIZER] Processing shop items data structure:', Object.keys(data));
// Handle both old (data.items) and new (data.shopItems) structures
let itemsToProcess = null;
if (data.shopItems && typeof data.shopItems === 'object') {
// New structure: categorized items
console.log('[GAME INITIALIZER] Using new shop structure');
itemsToProcess = Object.values(data.shopItems).flat();
console.log('[GAME INITIALIZER] Flattened', itemsToProcess.length, 'items from categories');
} else if (data.items && Array.isArray(data.items)) {
// Old structure: flat array
console.log('[GAME INITIALIZER] Using old shop structure');
itemsToProcess = data.items;
console.log('[GAME INITIALIZER] Processing', itemsToProcess.length, 'items from flat array');
} else {
console.warn('[GAME INITIALIZER] Invalid shop items structure:', data);
return;
}
// Update ItemSystem with server data
window.game.systems.itemSystem.processServerItems(itemsToProcess);
console.log('[GAME INITIALIZER] ItemSystem updated with server shop items');
// Update Economy shop UI
if (window.game.systems.economy) {
window.game.systems.economy.updateShopUI();
console.log('[GAME INITIALIZER] Economy shop UI updated');
}
} else {
console.warn('[GAME INITIALIZER] Failed to receive shop items:', data);
}
}
onItemDetailsReceived(data) {
// This is handled by the ItemSystem directly
// Just log for debugging
if (data.success) {
console.log('[GAME INITIALIZER] Item details received for:', data.item.name);
} else {
console.warn('[GAME INITIALIZER] Failed to receive item details:', data);
}
}
@ -696,6 +1045,101 @@ class GameInitializer {
this.currentUser = null;
this.serverPlayerData = null;
}
onQuestCompleted(data) {
console.log('[GAME INITIALIZER] Processing quest completion:', data);
// Show quest completion notification
if (window.game && window.game.showNotification) {
const questName = data.questId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
window.game.showNotification(`Quest Completed: ${questName}! 🎯`, 'success', 5000);
}
// Award quest rewards
if (data.rewards) {
if (data.rewards.experience > 0 && window.game.systems.player) {
window.game.systems.player.addExperience(data.rewards.experience);
console.log('[GAME INITIALIZER] Awarded quest experience:', data.rewards.experience);
}
if (data.rewards.credits > 0 && window.game.systems.economy) {
window.game.systems.economy.credits += data.rewards.credits;
console.log('[GAME INITIALIZER] Awarded quest credits:', data.rewards.credits);
}
}
// Update quest UI if quest system exists
if (window.game && window.game.systems && window.game.systems.questSystem) {
window.game.systems.questSystem.completeQuest(data.questId, data.rewards);
// Force fetch fresh quest data from server (Fix #1 - Corrected)
console.log('[GAME INITIALIZER] Requesting fresh quest data from server after completion');
// Request fresh quest data from server
if (window.gameInitializer && window.gameInitializer.socket) {
window.gameInitializer.socket.emit('get_quests');
}
}
// Force quest UI refresh for server-driven quests
if (typeof updateQuestDisplay === 'function') {
updateQuestDisplay();
}
// Update UI
if (window.game && window.game.ui) {
window.game.ui.updatePlayerStats();
}
}
onPlayerStatUpdate(data) {
console.log('[GAME INITIALIZER] Processing player stat update:', data);
if (window.game && window.game.systems && window.game.systems.player && window.game.systems.player.stats) {
// Update the player stat
window.game.systems.player.stats[data.stat] = data.value;
console.log('[GAME INITIALIZER] Updated player stat:', data.stat, '=', data.value);
// Update UI to reflect the change
if (window.game.ui) {
window.game.ui.updatePlayerStats();
}
// Update quest system to check for quest progress
if (window.game.systems.questSystem) {
window.game.systems.questSystem.checkQuestAvailability();
}
}
}
onQuestsData(data) {
console.log('[GAME INITIALIZER] Processing quest data from server:', data);
console.log('[GAME INITIALIZER] Quest data keys:', Object.keys(data || {}));
console.log('[GAME INITIALIZER] Main quests count:', (data?.mainQuests || []).length);
console.log('[GAME INITIALIZER] Daily quests count:', (data?.dailyQuests || []).length);
console.log('[GAME INITIALIZER] Weekly quests count:', (data?.weeklyQuests || []).length);
if (window.game && window.game.systems && window.game.systems.questSystem) {
// Load quest data into the quest system
if (window.game.systems.questSystem.loadServerQuests) {
console.log('[GAME INITIALIZER] Calling loadServerQuests with data');
window.game.systems.questSystem.loadServerQuests(data);
console.log('[GAME INITIALIZER] Loaded quest data into quest system');
} else {
console.log('[GAME INITIALIZER] loadServerQuests method not found');
}
// Update quest UI
if (typeof updateQuestDisplay === 'function') {
console.log('[GAME INITIALIZER] Calling updateQuestDisplay');
updateQuestDisplay();
} else {
console.log('[GAME INITIALIZER] updateQuestDisplay function not found');
}
} else {
console.log('[GAME INITIALIZER] Quest system not available');
}
}
}
// Create global instance

View File

@ -11,40 +11,15 @@ function integrateWithGameEngine() {
// Store original save method
const originalSave = window.game.save;
// Override save method
// Override game save method
window.game.save = async function() {
console.log('[SAVE INTEGRATION] Game save called');
// console.log('[SAVE INTEGRATION] Game save called');
// In multiplayer mode, don't save - server is authoritative
if (window.smartSaveManager && window.smartSaveManager.isMultiplayer) {
console.log('[SAVE INTEGRATION] Multiplayer mode - client save disabled, server is authoritative');
this.showNotification('Server manages your game data', 'info', 2000);
return true;
}
try {
// Get current game data
const gameData = this.getSaveData ? this.getSaveData() : {};
// Use SmartSaveManager for singleplayer
if (window.smartSaveManager) {
const success = await window.smartSaveManager.savePlayerData(gameData);
if (success) {
this.showNotification('Game saved!', 'success', 2000);
} else {
this.showNotification('Save failed!', 'error', 3000);
}
return success;
} else {
// Fallback to original save
return await originalSave.call(this);
}
} catch (error) {
console.error('[SAVE INTEGRATION] Save error:', error);
this.showNotification('Save error!', 'error', 3000);
return false;
if (window.smartSaveManager) {
await window.smartSaveManager.save();
} else {
// Fallback to original save if SmartSaveManager not available
return await originalSave.call(this);
}
};
@ -225,7 +200,7 @@ function addSaveModeUI() {
indicator.id = 'save-mode-indicator';
indicator.style.cssText = `
position: fixed;
top: 10px;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;

View File

@ -14,9 +14,11 @@ class SmartSaveManager {
}
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) {
@ -136,9 +138,52 @@ class SmartSaveManager {
this.serverPlayerData = serverData;
// Apply to game if game is running
if (window.game && window.game.loadPlayerData) {
window.game.loadPlayerData(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

View File

@ -5,7 +5,9 @@
class DebugLogger {
constructor() {
this.debugEnabled = true; // Always enabled
// 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
@ -15,10 +17,15 @@ class DebugLogger {
this.logger = window.logger || null;
// Log initialization
this.log('=== DEBUG SESSION STARTED ===');
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;
@ -55,13 +62,13 @@ class DebugLogger {
this.debugLogs.shift();
}
// Always log to console
console.log(`[DEBUG] ${message}`, data || '');
// Skip console logging to prevent flooding
// console.log(`[DEBUG] ${message}`, data || '');
// Log performance data to console
if (performanceData.memory) {
console.log(`[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory.used}/${performanceData.memory.total}`);
}
// 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) {
@ -79,6 +86,9 @@ class DebugLogger {
}
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',
@ -88,6 +98,9 @@ class DebugLogger {
}
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';
@ -101,6 +114,9 @@ class DebugLogger {
}
async logStep(stepName, data = null) {
// Skip logging if debug is disabled
if (!this.debugEnabled) return;
await this.log(`STEP: ${stepName}`, {
type: 'step',
step: stepName,

View File

@ -0,0 +1,905 @@
/**
* Galaxy Strike Online - Economy System
* Manages player currency, transactions, and shop functionality
* Now uses server-side ItemSystem for all item data
*/
class Economy {
constructor(gameEngine) {
this.game = gameEngine;
// Currency - don't override in multiplayer mode, will be set by server data
if (window.smartSaveManager?.isMultiplayer) {
this.credits = 0; // Will be updated by server
this.gems = 0; // Will be updated by server
this.premiumCurrency = 0; // Will be updated by server
} else {
this.credits = 10000; // Starting credits for singleplayer
this.gems = 50; // Starting premium currency
this.premiumCurrency = 0; // Additional premium currency
}
// Transaction history
this.transactions = [];
// Shop categories
this.shopCategories = {
ships: 'Ships',
weapons: 'Weapons',
armors: 'Armors',
cosmetics: 'Cosmetics',
consumables: 'Consumables',
materials: 'Materials'
};
// Random shop system - now uses server ItemSystem
this.randomShopItems = {}; // Current random items per category
this.shopRefreshInterval = null; // Timer for 2-hour refresh
this.shopHeartbeatInterval = null; // Timer for live countdown updates
this.lastShopRefresh = null; // Timestamp of last refresh
this.currentShopData = null; // Current shop data from server
this.SHOP_REFRESH_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
this.MAX_ITEMS_PER_CATEGORY = 8;
this.categoryPurchaseLimits = {}; // Track purchases per category per refresh
// Shop items - now loaded from server ItemSystem
this.shopItems = null; // Will be populated by ItemSystem in multiplayer
// Owned cosmetics
this.ownedCosmetics = [];
// Owned ships
this.ownedShips = [];
console.log('[ECONOMY] Economy system initialized');
// Initialize global purchase function
Economy.initGlobalPurchaseFunction();
}
// Create global purchase function for shop buttons
static initGlobalPurchaseFunction() {
window.purchaseShopItem = function(itemId) {
console.log('[GLOBAL] Purchase shop item called:', itemId);
if (window.game && window.game.systems && window.game.systems.economy) {
window.game.systems.economy.purchaseItem(itemId, 1);
} else {
console.error('[GLOBAL] Economy system not available for purchase');
}
};
// Add test function for idle system
window.testIdleRewards = function() {
console.log('[GLOBAL] Testing idle rewards...');
if (window.game && window.game.socket) {
window.game.socket.emit('testIdleRewards', {});
// Listen for response
window.game.socket.once('testIdleRewards', (data) => {
console.log('[GLOBAL] Test idle rewards response:', data);
});
} else {
console.error('[GLOBAL] No socket available for idle test');
}
};
// Add socket event monitor
window.monitorSocketEvents = function() {
if (window.game && window.game.socket) {
console.log('[GLOBAL] Monitoring socket events...');
// Monitor all incoming events
const originalOn = window.game.socket.on;
window.game.socket.on = function(event, callback) {
const wrappedCallback = function(data) {
if (event === 'onlineIdleRewards' || event === 'economy_data') {
console.log('[SOCKET MONITOR] Received event:', event, data);
}
return callback(data);
};
return originalOn.call(this, event, wrappedCallback);
};
console.log('[GLOBAL] Socket event monitoring enabled');
} else {
console.error('[GLOBAL] No socket available for monitoring');
}
};
// Add function to give player energy for testing dungeons
window.addEnergy = function(amount = 50) {
if (window.game && window.game.systems && window.game.systems.player) {
const player = window.game.systems.player;
const oldEnergy = player.attributes.energy || 0;
player.attributes.energy = Math.min(oldEnergy + amount, player.attributes.maxEnergy || 100);
console.log('[GLOBAL] Added energy:', oldEnergy, '->', player.attributes.energy);
// Update UI
if (player.updateUI) {
player.updateUI();
}
// Update dungeon UI if available
if (window.game.systems.dungeonSystem && window.game.systems.dungeonSystem.updateUI) {
window.game.systems.dungeonSystem.updateUI();
}
return player.attributes.energy;
} else {
console.error('[GLOBAL] Player system not available');
return 0;
}
};
console.log('[GLOBAL] Global functions initialized - purchaseShopItem() and testIdleRewards() available');
}
/**
* Set up socket listeners for economy data synchronization
*/
setupSocketListeners() {
if (!this.game.socket) {
console.warn('[ECONOMY] No socket available for economy sync');
return;
}
// Listen for economy data updates from server
this.game.socket.on('economy_data', (data) => {
console.log('[ECONOMY] Received economy data from server:', data);
console.log('[ECONOMY] Current credits before update:', this.credits);
console.log('[ECONOMY] Current gems before update:', this.gems);
this.credits = data.credits || 0;
this.gems = data.gems || 0;
console.log('[ECONOMY] Updated credits:', this.credits);
console.log('[ECONOMY] Updated gems:', this.gems);
// Update UI immediately
if (this.game.ui) {
this.game.ui.updatePlayerStats();
}
console.log('[ECONOMY] Economy synced - Credits:', this.credits, 'Gems:', this.gems);
});
// Note: onlineIdleRewards is handled by GameInitializer to avoid duplicate event handling
// Listen for play time updates from server
this.game.socket.on('playTimeUpdated', (data) => {
console.log('[ECONOMY] Received play time update from server:', data);
// Update player stats if available
if (this.game.systems.player && this.game.systems.player.stats) {
this.game.systems.player.stats.playTime = data.playTime;
}
});
}
/**
* Request economy data from server
*/
requestEconomyData() {
if (this.game.socket) {
console.log('[ECONOMY] Requesting economy data from server');
this.game.socket.emit('get_economy_data');
} else {
console.warn('[ECONOMY] Cannot request economy data - no socket available');
}
}
async initialize() {
console.log('[ECONOMY] Initializing economy system');
// In multiplayer mode, wait for ItemSystem to be ready (handled by event listener)
this.game.on('itemSystemReady', () => {
console.log('[ECONOMY] ItemSystem is ready, updating shop UI');
this.updateShopUI();
});
if (window.smartSaveManager?.isMultiplayer) {
console.log('[ECONOMY] Multiplayer mode - waiting for ItemSystem to be ready');
// ItemSystem initialization removed - wait for event instead
} else {
console.log('[ECONOMY] Singleplayer mode - using local shop data');
// Initialize random shop for singleplayer
this.initializeRandomShop();
}
console.log('[ECONOMY] Economy system initialized');
}
// Shop functionality - now uses ItemSystem in multiplayer
purchaseItem(itemId, quantity = 1) {
const debugLogger = window.debugLogger;
// In multiplayer mode, send request to server
if (window.smartSaveManager?.isMultiplayer) {
if (debugLogger) debugLogger.logStep('Sending purchase request to server', {
itemId: itemId,
quantity: quantity
});
// Send purchase request to server
if (window.game && window.game.socket) {
window.game.socket.emit('purchaseItem', {
itemId: itemId,
quantity: quantity
});
// Show loading message
this.game.showNotification('Processing purchase...', 'info', 2000);
} else {
this.game.showNotification('Not connected to server', 'error', 3000);
}
return;
}
// Singleplayer mode - use local logic
const item = this.findShopItem(itemId);
if (!item) {
if (debugLogger) debugLogger.logStep('Item purchase failed - item not found', {
itemId: itemId,
quantity: quantity
});
this.game.showNotification('Item not found in shop', 'error', 3000);
return false;
}
const totalCost = item.price * quantity;
const currency = item.currency;
const oldCredits = this.credits;
const oldGems = this.gems;
if (debugLogger) debugLogger.logStep('Item purchase attempted', {
itemId: itemId,
itemName: item.name,
itemType: item.type,
quantity: quantity,
unitPrice: item.price,
totalCost: totalCost,
currency: currency,
currentCredits: oldCredits,
currentGems: oldGems
});
// Check if player can afford
if (currency === 'credits' && this.credits < totalCost) {
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient credits', {
totalCost: totalCost,
currentCredits: oldCredits,
deficit: totalCost - oldCredits
});
this.game.showNotification('Not enough credits!', 'error', 3000);
return false;
}
if (currency === 'gems' && this.gems < totalCost) {
if (debugLogger) debugLogger.logStep('Item purchase failed - insufficient gems', {
totalCost: totalCost,
currentGems: oldGems,
deficit: totalCost - oldGems
});
this.game.showNotification('Not enough gems!', 'error', 3000);
return false;
}
// Check if already owns this cosmetic
if (item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id)) {
this.game.showNotification('You already own this cosmetic!', 'error', 3000);
return false;
}
// Process payment and give item based on type
if (currency === 'credits') {
this.credits -= totalCost;
} else if (currency === 'gems') {
this.gems -= totalCost;
}
switch (item.type) {
case 'ship':
this.purchaseShip(item, quantity);
break;
case 'cosmetic':
this.purchaseCosmetic(item, quantity);
break;
case 'consumable':
this.purchaseConsumable(item, quantity);
break;
case 'material':
this.purchaseMaterial(item, quantity);
break;
default:
console.warn(`[ECONOMY] Unknown item type: ${item.type}`);
return false;
}
// Show success message
this.game.showNotification(`Purchased ${item.name}!`, 'success', 3000);
if (debugLogger) debugLogger.logStep('Item purchase completed successfully', {
itemId: itemId,
itemName: item.name,
itemType: item.type,
quantity: quantity,
totalCost: totalCost,
currency: currency,
oldCredits: oldCredits,
newCredits: this.credits,
oldGems: oldGems,
newGems: this.gems
});
// Update UI without calling updateShopUI to avoid circular updates
return true;
}
findShopItem(itemId) {
const debugLogger = window.debugLogger;
console.log('[ECONOMY] Looking for shop item:', itemId);
console.log('[ECONOMY] Multiplayer mode:', window.smartSaveManager?.isMultiplayer);
console.log('[ECONOMY] ItemSystem available:', !!(this.game.systems.itemSystem));
// In multiplayer mode, use ItemSystem (required)
if (window.smartSaveManager?.isMultiplayer) {
// Check if ItemSystem is ready before using it
if (!this.game.systems.itemSystem || !this.game.systems.itemSystem.itemCatalog) {
console.log('[ECONOMY] ItemSystem not ready yet, cannot find shop item');
if (debugLogger) debugLogger.logStep('Shop item lookup failed - ItemSystem not ready', {
itemId: itemId,
multiplayer: true
});
return null;
}
// Search in ItemSystem catalog
const item = this.game.systems.itemSystem.itemCatalog.get(itemId);
if (item) {
console.log('[ECONOMY] Found item in ItemSystem:', item.name);
return item;
} else {
console.log('[ECONOMY] Item not found in ItemSystem:', itemId);
return null;
}
} else {
// Singleplayer mode - search in local random shop
for (const categoryItems of Object.values(this.randomShopItems)) {
const item = categoryItems.find(item => item.id === itemId);
if (item) return item;
}
return null;
}
}
// Purchase methods
purchaseShip(ship) {
const debugLogger = window.debugLogger;
const player = this.game.systems.player;
const oldShipName = player.ship.name;
const oldShipClass = player.ship.class;
const oldAttributes = { ...player.attributes };
// Update player ship
player.ship = {
name: ship.name,
class: ship.id,
texture: ship.texture,
stats: ship.stats || {}
};
// Update player attributes
if (ship.stats) {
player.attributes = { ...player.attributes, ...ship.stats };
}
// Add to owned ships
if (!player.ownedShips) {
player.ownedShips = [];
}
if (!player.ownedShips.includes(ship.id)) {
player.ownedShips.push(ship.id);
}
// Add ship to BaseSystem ship gallery (singleplayer)
if (this.game.systems.baseSystem) {
const shipData = {
id: ship.id,
name: ship.name,
class: ship.name.replace(/\s+/g, '_').toLowerCase(), // Generate class from name
level: 1,
stats: ship.stats || {},
texture: ship.texture || `assets/textures/ships/${ship.id}.png`,
isCurrent: false,
rarity: ship.rarity || 'common'
};
// Initialize ship gallery if needed
if (!this.game.systems.baseSystem.purchasedShips) {
this.game.systems.baseSystem.initializeShipGallery();
}
// Add ship to gallery
this.game.systems.baseSystem.purchasedShips.push(shipData);
// Update the ship gallery UI
this.game.systems.baseSystem.updateShipGallery();
console.log('[ECONOMY] Ship added to BaseSystem gallery (singleplayer):', shipData.name);
}
if (debugLogger) debugLogger.logStep('Ship purchase completed', {
shipId: ship.id,
shipName: ship.name,
oldShipName: oldShipName,
oldShipClass: oldShipClass,
newShipName: ship.name,
newShipClass: ship.id,
oldAttributes: oldAttributes,
newAttributes: player.attributes
});
}
purchaseCosmetic(cosmetic) {
const debugLogger = window.debugLogger;
const oldOwnedCount = this.ownedCosmetics.length;
// Add to owned cosmetics
this.ownedCosmetics.push(cosmetic.id);
this.game.showNotification(`Cosmetic unlocked: ${cosmetic.name}`, 'success', 3000);
if (debugLogger) debugLogger.logStep('Cosmetic purchase completed', {
cosmeticId: cosmetic.id,
cosmeticName: cosmetic.name,
oldOwnedCount: oldOwnedCount,
newOwnedCount: this.ownedCosmetics.length,
totalOwnedCosmetics: this.ownedCosmetics.length
});
}
purchaseConsumable(consumable, quantity) {
const debugLogger = window.debugLogger;
const inventory = this.game.systems.inventory;
// Create item object for inventory
const item = {
id: consumable.id,
name: consumable.name,
type: consumable.type,
rarity: consumable.rarity,
quantity: quantity,
description: consumable.description,
texture: consumable.texture,
stats: consumable.stats || {},
acquired: new Date().toISOString()
};
try {
const oldInventorySize = inventory.items.length;
inventory.addItem(item);
if (debugLogger) debugLogger.logStep('Consumable purchase completed', {
itemId: consumable.id,
itemName: consumable.name,
quantity: quantity,
oldInventorySize: oldInventorySize,
newInventorySize: inventory.items.length
});
} catch (error) {
console.error('[ECONOMY] Error adding consumable to inventory:', error);
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
}
}
purchaseMaterial(material, quantity) {
const debugLogger = window.debugLogger;
const inventory = this.game.systems.inventory;
// Create item object for inventory
const item = {
id: material.id,
name: material.name,
type: material.type,
rarity: material.rarity,
quantity: quantity,
description: material.description,
texture: material.texture,
stackable: material.stackable || true,
acquired: new Date().toISOString()
};
try {
const oldInventorySize = inventory.items.length;
inventory.addItem(item);
if (debugLogger) debugLogger.logStep('Material purchase completed', {
itemId: material.id,
itemName: material.name,
quantity: quantity,
oldInventorySize: oldInventorySize,
newInventorySize: inventory.items.length
});
} catch (error) {
console.error('[ECONOMY] Error adding material to inventory:', error);
this.game.showNotification('Failed to add item to inventory', 'error', 3000);
}
}
// Currency management
addCredits(amount, source = 'unknown') {
const oldCredits = this.credits;
this.credits += amount;
// Add transaction
this.addTransaction({
type: 'credit',
amount: amount,
source: source,
balance: this.credits,
timestamp: new Date().toISOString()
});
console.log(`[ECONOMY] Added ${amount} credits from ${source}. New balance: ${this.credits}`);
this.updateUI();
return this.credits - oldCredits;
}
addGems(amount, source = 'unknown') {
const oldGems = this.gems;
this.gems += amount;
// Add transaction
this.addTransaction({
type: 'gem',
amount: amount,
source: source,
balance: this.gems,
timestamp: new Date().toISOString()
});
console.log(`[ECONOMY] Added ${amount} gems from ${source}. New balance: ${this.gems}`);
this.updateUI();
return this.gems - oldGems;
}
canAfford(cost, currency = 'credits') {
if (currency === 'credits') {
return this.credits >= cost;
} else if (currency === 'gems') {
return this.gems >= cost;
} else if (currency === 'premium') {
return this.premiumCurrency >= cost;
}
return false;
}
// Transaction management
addTransaction(transaction) {
this.transactions.push(transaction);
this.transactionHistory.push(transaction);
// Keep only last 100 transactions in memory
if (this.transactions.length > 100) {
this.transactions = this.transactions.slice(-100);
}
}
// Manual sync with server data - call this to force update
syncWithServerData(serverPlayerData) {
console.log('[ECONOMY] Manual sync with server data:', {
serverCredits: serverPlayerData?.stats?.credits,
serverGems: serverPlayerData?.stats?.gems,
currentCredits: this.credits,
currentGems: this.gems
});
if (serverPlayerData?.stats?.credits !== undefined) {
this.credits = serverPlayerData.stats.credits;
console.log('[ECONOMY] Updated credits from server:', this.credits);
}
if (serverPlayerData?.stats?.gems !== undefined) {
this.gems = serverPlayerData.stats.gems;
console.log('[ECONOMY] Updated gems from server:', this.gems);
}
// Update UI after sync
this.updateUI();
}
// UI updates
updateUI() {
// Debug logging to track current values
console.log('[ECONOMY] updateUI called - Current values:', {
credits: this.credits,
gems: this.gems,
gameSystemsAvailable: !!(this.game && this.game.systems),
uiSystemAvailable: !!(this.game && this.game.systems && this.game.systems.ui)
});
// Update resource display
if (this.game.systems.ui) {
this.game.systems.ui.updateResourceDisplay();
}
// Update shop UI if open
this.updateShopUI();
}
updateShopUI() {
const debugLogger = window.debugLogger;
console.log('[ECONOMY] updateShopUI called');
if (this.game.multiplayerMode && this.game.itemSystem) {
// Support both .catalog getter (new) and .shopItemsByCategory (legacy)
const shopItems = this.game.itemSystem.catalog || this.game.itemSystem.shopItemsByCategory || {};
const activeCategory = this.game.itemSystem.activeCategory || 'ships';
const categoryItems = shopItems[activeCategory] || [];
this.renderShopItems(categoryItems);
} else {
// Singleplayer mode - use local shop data
const items = Object.values(this.randomShopItems).flat();
// Convert to categorized structure for consistency
const categorizedItems = this.randomShopItems || {};
this.renderShopItems(categorizedItems);
}
}
renderShopItems(items) {
const shopItemsElement = document.getElementById('shopItems');
if (!shopItemsElement) return;
const activeCategory = document.querySelector('.shop-cat-btn.active')?.dataset.category || 'ships';
console.log('[ECONOMY] Active shop category:', activeCategory);
// Handle new shop data structure (items by category) or old structure (flat array)
let categoryItems = [];
if (items && typeof items === 'object' && !Array.isArray(items)) {
// New structure: { ships: [...], weapons: [...], ... }
categoryItems = items[activeCategory] || [];
console.log('[ECONOMY] Using new shop structure - found', Object.keys(items).length, 'categories');
} else if (Array.isArray(items)) {
// Old structure: flat array of items
const targetItemType = activeCategory.slice(0, -1); // Remove 's' from 'ships', 'weapons', etc.
categoryItems = items.filter(item => item.type === targetItemType);
console.log('[ECONOMY] Using old shop structure - filtered', items.length, 'total items');
} else {
console.warn('[ECONOMY] Invalid shop items structure:', typeof items);
shopItemsElement.innerHTML = '<p>No items available</p>';
return;
}
console.log('[ECONOMY] Filtered items for category', activeCategory, ':', categoryItems.length, 'items');
console.log('[ECONOMY] Item types in category:', categoryItems.map(item => item.type));
if (categoryItems.length === 0) {
shopItemsElement.innerHTML = '<p>No items available in this category</p>';
return;
}
shopItemsElement.innerHTML = categoryItems.map(item => {
const canAfford = this.canAfford(item.price, item.currency);
const isOwned = item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id);
// Generate image URL - server will serve images
const imageUrl = this.getItemImageUrl(item);
const placeholderUrl = 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
return `
<div class="shop-item ${canAfford ? '' : 'cant-afford'} ${isOwned ? 'owned' : ''}" data-item-id="${item.id}">
<div class="shop-item-content">
<div class="shop-item-image">
<img src="${imageUrl}" alt="${item.name}"
onerror="this.src='${placeholderUrl}'"
loading="lazy">
</div>
<div class="shop-item-info">
<div class="shop-item-header">
<h3 class="shop-item-name">${item.name}</h3>
<span class="shop-item-rarity ${item.rarity}">${item.rarity}</span>
</div>
<div class="shop-item-body">
<p class="shop-item-description">${item.description}</p>
<div class="shop-item-price">
${this.formatPrice(item)}
</div>
</div>
<div class="shop-item-footer">
<button class="shop-item-purchase-btn"
data-item-id="${item.id}"
onclick="purchaseShopItem('${item.id}')"
${!canAfford || isOwned ? 'disabled' : ''}>
${isOwned ? 'Owned' : 'Purchase'}
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
// Add event listeners to purchase buttons
shopItemsElement.querySelectorAll('.shop-item-purchase-btn').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const itemId = button.getAttribute('data-item-id');
if (itemId && !button.disabled) {
console.log('[ECONOMY] Purchase button clicked for item:', itemId);
this.purchaseItem(itemId, 1);
}
});
});
}
formatPrice(item) {
if (!item.price) return 'Free';
const currency = item.currency || 'credits';
const price = this.game.formatNumber(item.price);
return `${price} ${currency}`;
}
/**
* Get image URL for an item from server
*/
getItemImageUrl(item) {
if (!item) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
// For multiplayer, ALWAYS get from server
if (window.smartSaveManager?.isMultiplayer && this.game.socket) {
const serverUrl = this.getServerUrl();
// Map item types to proper server paths
switch (item.type) {
case 'ship':
return `${serverUrl}/images/ships/${item.id}.png`;
case 'weapon':
return `${serverUrl}/images/weapons/${item.id}.png`;
case 'armor':
return `${serverUrl}/images/armors/${item.id}.png`;
case 'material':
return `${serverUrl}/images/items/materials/${item.id}.png`;
case 'consumable':
return `${serverUrl}/images/items/consumables/${item.id}.png`;
case 'cosmetic':
return `${serverUrl}/images/items/cosmetics/${item.id}.png`;
default:
return `${serverUrl}/images/ui/placeholder.png`;
}
}
// For singleplayer, use local texture path (if available)
if (item.texture) {
return item.texture;
}
// Fallback to server placeholder
return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
}
/**
* Get server URL for image requests
*/
getServerUrl() {
// Get server URL from socket connection
if (this.game.socket && this.game.socket.io && this.game.socket.io.uri) {
return this.game.socket.io.uri.replace('/socket.io', '');
}
// Fallback to environment variable or production server
return process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
}
// Save/Load functionality
save() {
return {
credits: this.credits,
gems: this.gems,
premiumCurrency: this.premiumCurrency,
transactions: this.transactions,
ownedCosmetics: this.ownedCosmetics,
shopData: {
randomShopItems: this.randomShopItems,
categoryPurchaseLimits: this.categoryPurchaseLimits,
lastShopRefresh: this.lastShopRefresh
}
};
}
load(data) {
if (data.credits !== undefined) this.credits = data.credits;
if (data.gems !== undefined) this.gems = data.gems;
if (data.premiumCurrency !== undefined) this.premiumCurrency = data.premiumCurrency;
if (data.transactions) this.transactions = data.transactions;
if (data.ownedCosmetics) this.ownedCosmetics = data.ownedCosmetics;
// Load shop data
if (data.shopData) {
this.randomShopItems = data.shopData.randomShopItems || {};
this.categoryPurchaseLimits = data.shopData.categoryPurchaseLimits || {};
this.lastShopRefresh = data.shopData.lastShopRefresh || null;
}
this.updateUI();
}
// Reset functionality
reset() {
const oldState = {
credits: this.credits,
gems: this.gems,
premiumCurrency: this.premiumCurrency,
transactionCount: this.transactions.length,
ownedCosmeticsCount: this.ownedCosmetics.length
};
this.credits = 1000;
this.gems = 10;
this.premiumCurrency = 0;
this.transactions = [];
this.ownedCosmetics = [];
// Reset daily rewards
localStorage.removeItem('lastDailyReward');
this.updateUI();
return oldState;
}
clear() {
const oldState = {
credits: this.credits,
gems: this.gems,
premiumCurrency: this.premiumCurrency,
transactionCount: this.transactions.length,
ownedCosmeticsCount: this.ownedCosmetics.length
};
this.credits = 0;
this.gems = 0;
this.premiumCurrency = 0;
this.transactions = [];
this.ownedCosmetics = [];
this.updateUI();
return oldState;
}
// Initialize random shop for singleplayer (minimal implementation)
initializeRandomShop() {
console.log('[ECONOMY] Random shop not available in singleplayer mode');
this.randomShopItems = {};
}
// Get system statistics
getStats() {
return {
credits: this.credits,
gems: this.gems,
premiumCurrency: this.premiumCurrency,
transactionCount: this.transactions.length,
ownedCosmeticsCount: this.ownedCosmetics.length,
shopItemsCount: this.game.systems.itemSystem && this.game.systems.itemSystem.itemCatalog ?
this.game.systems.itemSystem.getStats().totalItems : 0
};
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = Economy;
} else {
window.Economy = Economy;
}

View File

@ -0,0 +1,753 @@
/**
* 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;
}

View File

@ -95,72 +95,24 @@ class Inventory {
maxSlots: this.maxSlots
});
const startingItems = [
{
id: 'starter_blaster_common',
name: 'Common Blaster',
type: 'weapon',
rarity: 'common',
quantity: 1,
stats: { attack: 5, criticalChance: 0.02 },
description: 'A reliable basic blaster for new pilots',
equipable: true,
slot: 'weapon'
},
{
id: 'basic_armor_common',
name: 'Basic Armor',
type: 'armor',
rarity: 'common',
quantity: 1,
stats: { defense: 3 },
description: 'Light armor providing basic protection',
equipable: true,
slot: 'armor'
}
];
if (debugLogger) debugLogger.logStep('Adding starting items', {
startingItemCount: startingItems.length,
startingItems: startingItems.map(item => ({
id: item.id,
name: item.name,
type: item.type,
quantity: item.quantity
}))
});
startingItems.forEach(item => {
console.log(`[DEBUG] Adding starting item: ${item.name}`);
const result = this.addItem(item);
console.log(`[DEBUG] Starting item add result: ${result}, inventory size: ${this.items.length}`);
});
// Equip starter items
console.log('[INVENTORY] Equipping starter items');
if (debugLogger) debugLogger.logStep('Equipping starter items');
// Equip starter blaster
const blasterItem = this.items.find(item => item.id === 'starter_blaster');
if (blasterItem) {
console.log('[INVENTORY] Equipping starter blaster');
this.equipItem(blasterItem.id);
// In multiplayer mode, starting items should come from server
if (window.smartSaveManager?.isMultiplayer) {
console.log('[INVENTORY] Multiplayer mode - starting items will be provided by server');
if (debugLogger) debugLogger.logStep('Skipping starting items in multiplayer mode');
if (debugLogger) debugLogger.endStep('Inventory.addStartingItems', {
finalItemCount: this.items.length,
itemsAdded: 0
});
return;
}
// Equip basic armor
const armorItem = this.items.find(item => item.id === 'basic_armor');
if (armorItem) {
console.log('[INVENTORY] Equipping basic armor');
this.equipItem(armorItem.id);
}
// Auto-stack starting items
if (debugLogger) debugLogger.logStep('Auto-stacking starting items');
this.autoStackItems();
// Singleplayer mode - no hardcoded starting items available
console.log('[INVENTORY] Singleplayer mode - no hardcoded starting items available');
if (debugLogger) debugLogger.logStep('No starting items available in singleplayer mode');
if (debugLogger) debugLogger.endStep('Inventory.addStartingItems', {
finalItemCount: this.items.length,
itemsAdded: startingItems.length
itemsAdded: 0
});
}

View File

@ -537,9 +537,9 @@ class Player {
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);
// 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 = {};
@ -692,16 +692,43 @@ class Player {
}
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');
console.log('[PLAYER] PlayTime in hours:', this.stats.playTime / 3600000, 'hours');
*/
}
// UI updates
@ -717,10 +744,15 @@ class Player {
// 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} - ${this.info.title}`;
playerNameElement.textContent = this.info.name;
}
if (playerTitleElement) {
playerTitleElement.textContent = ` - ${this.info.title}`;
}
if (playerLevelElement) {
@ -767,6 +799,7 @@ class Player {
if (debugLogger) debugLogger.logStep('Player UI update completed', {
elementsUpdated: {
playerName: !!playerNameElement,
playerTitle: !!playerTitleElement,
playerLevel: !!playerLevelElement,
totalKills: !!totalKillsElement,
dungeonsCleared: !!dungeonsClearedElement,
@ -819,9 +852,29 @@ class Player {
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 };
this.stats = { ...this.stats, ...data.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,

View File

@ -0,0 +1,125 @@
/**
* Galaxy Strike Online - Game Data
* UI constants and configuration only.
* All game content (items, skills, recipes, dungeons, enemies) is loaded from the server.
*/
// Game configuration
const GAME_CONFIG = {
version: '1.0.0',
name: 'Galaxy Strike Online',
maxLevel: 100,
saveInterval: 30000, // 30 seconds
notificationDuration: 3000,
maxNotifications: 5
};
// Player defaults (used only for initial UI state before server data arrives)
const PLAYER_DEFAULTS = {
level: 1,
experience: 0,
skillPoints: 0,
credits: 1000,
gems: 10,
health: 100,
maxHealth: 100,
energy: 100,
maxEnergy: 100,
attack: 10,
defense: 5,
speed: 10,
criticalChance: 0.05,
criticalDamage: 1.5
};
// Experience requirements (client-side display only; server is authoritative)
const EXPERIENCE_TABLE = [];
for (let i = 1; i <= 100; i++) {
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
}
// Item rarity display properties (colours/labels only - drop rates are server-side)
const ITEM_RARITIES = {
common: { name: 'Common', color: '#888888', multiplier: 1.0 },
uncommon: { name: 'Uncommon', color: '#00ff00', multiplier: 1.2 },
rare: { name: 'Rare', color: '#0088ff', multiplier: 1.5 },
epic: { name: 'Epic', color: '#8833ff', multiplier: 2.0 },
legendary: { name: 'Legendary', color: '#ff8800', multiplier: 3.0 }
};
// Game messages and notifications
const GAME_MESSAGES = {
welcome: 'Welcome to Galaxy Strike Online, Commander!',
levelUp: 'Level Up! You are now level {level}!',
questCompleted: 'Quest completed: {questName}!',
dungeonCompleted: 'Dungeon completed! Time: {time}',
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
insufficientCredits: 'Not enough credits!',
insufficientGems: 'Not enough gems!',
insufficientEnergy: 'Not enough energy!',
inventoryFull: 'Inventory is full!',
skillPointsAvailable: 'You have {points} skill points available!',
dailyReward: 'Daily reward claimed! Day {day}',
offlineRewards: 'Welcome back! You were offline for {time}'
};
// Utility functions
const GameUtils = {
getRandomItem(array) {
return array[Math.floor(Math.random() * array.length)];
},
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
},
checkChance(chance) {
return Math.random() < chance;
},
formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return Math.floor(num).toString();
},
formatTime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
},
getExperienceForLevel(level) {
return EXPERIENCE_TABLE[level] || 0;
},
deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
},
generateId() {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
}
};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
GAME_CONFIG,
PLAYER_DEFAULTS,
EXPERIENCE_TABLE,
ITEM_RARITIES,
GAME_MESSAGES,
GameUtils
};
}

View File

@ -1026,8 +1026,15 @@ class BaseSystem {
} else if (view === 'ships') {
this.updateShipGallery();
} else if (view === 'starbases') {
this.updateStarbaseList();
this.updateStarbasePurchaseList();
// Boot the isometric starbase world instead of the old list UI
if (typeof _startStarbaseWorld === 'function') {
_startStarbaseWorld();
}
} else {
// Leaving starbases — stop the world loop
if (typeof _stopStarbaseWorld === 'function') {
_stopStarbaseWorld();
}
}
}

View File

@ -0,0 +1,403 @@
/**
* Galaxy Strike Online - Client Crafting System
* Recipe definitions are loaded from the server; this file handles
* local crafting logic, requirement checking, and UI rendering.
*/
class CraftingSystem extends BaseSystem {
constructor(gameEngine) {
super(gameEngine);
this.recipes = new Map(); // recipeId -> recipe object
this.currentCategory = 'weapons';
this.selectedRecipe = null;
this._loaded = false;
this._loading = false;
}
// ------------------------------------------------------------------ //
// Initialisation — request recipes from the server
// ------------------------------------------------------------------ //
async initialize() {
if (this._loaded || this._loading) return;
this._loading = true;
console.log('[CRAFTING SYSTEM] Requesting recipes from server');
if (!window.game?.socket) {
console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected');
this._loading = false;
return;
}
try {
const recipes = await this._fetchRecipesFromServer();
this._applyServerRecipes(recipes);
this._loaded = true;
console.log(`[CRAFTING SYSTEM] Loaded ${this.recipes.size} recipes from server`);
} catch (err) {
console.error('[CRAFTING SYSTEM] Failed to load recipes from server:', err);
} finally {
this._loading = false;
}
}
_fetchRecipesFromServer() {
return new Promise((resolve, reject) => {
const socket = window.game.socket;
const timeout = setTimeout(() => {
socket.off('recipes_data', handler);
reject(new Error('Recipe data request timed out'));
}, 10000);
const handler = (data) => {
clearTimeout(timeout);
socket.off('recipes_data', handler);
if (data && (Array.isArray(data) || typeof data === 'object')) {
resolve(data);
} else {
reject(new Error('Invalid recipe data from server'));
}
};
socket.on('recipes_data', handler);
socket.emit('get_recipes');
});
}
_applyServerRecipes(serverRecipes) {
this.recipes.clear();
// Server may return array or object keyed by id
const asList = Array.isArray(serverRecipes)
? serverRecipes
: Object.values(serverRecipes);
for (const recipe of asList) {
if (!recipe.id) continue;
// Normalise materials: server uses { itemId: qty } objects, client expects array
let materials = recipe.materials;
if (materials && !Array.isArray(materials)) {
materials = Object.entries(materials).map(([id, quantity]) => ({ id, quantity }));
}
// Normalise results similarly
let results = recipe.results;
if (results && !Array.isArray(results)) {
results = Object.entries(results)
.filter(([k]) => k !== 'experience')
.map(([id, quantity]) => ({ id, quantity }));
}
this.recipes.set(recipe.id, {
...recipe,
materials: materials || [],
results: results || [],
category: recipe.type || recipe.category || 'items',
unlocked: false // will be resolved by checkRecipeUnlocks()
});
}
}
// ------------------------------------------------------------------ //
// Runtime
// ------------------------------------------------------------------ //
addRecipe(id, recipe) {
recipe.id = id;
recipe.unlocked = false;
this.recipes.set(id, recipe);
}
update(deltaTime) {
this.checkRecipeUnlocks();
}
checkRecipeUnlocks() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
for (const [id, recipe] of this.recipes) {
if (recipe.unlocked) continue;
let canUnlock = true;
if (recipe.requirements) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
if (skillSystem.getSkillLevel(skillName) < requiredLevel) {
canUnlock = false;
break;
}
}
}
if (canUnlock) {
recipe.unlocked = true;
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
}
}
}
getRecipesByCategory(category) {
return Array.from(this.recipes.values())
.filter(r => r.category === category || r.type === category);
}
canCraftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
const skillSystem = this.game.systems.skillSystem;
const inventory = this.game.systems.inventory;
if (!recipe) return false;
if (recipe.requirements && skillSystem) {
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false;
}
}
if (recipe.materials && inventory) {
for (const mat of recipe.materials) {
if (!inventory.hasItem(mat.id, mat.quantity)) return false;
}
}
return true;
}
getMissingMaterials(recipeId) {
const recipe = this.recipes.get(recipeId);
const inventory = this.game.systems.inventory;
if (!recipe?.materials) return [];
const missing = [];
for (const mat of recipe.materials) {
let current = 0;
if (inventory?.getItemCount) {
try { current = inventory.getItemCount(mat.id) || 0; } catch (_) {}
}
const required = mat.quantity || 0;
if (current < required) {
missing.push({ id: mat.id, required, current, missing: required - current });
}
}
return missing;
}
async craftRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe || !this.canCraftRecipe(recipeId)) return false;
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
if (recipe.materials) {
for (const mat of recipe.materials) {
this.game.systems.inventory.removeItem(mat.id, mat.quantity);
}
}
if (recipe.experience && this.game.systems.skillSystem) {
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
}
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000));
if (recipe.results) {
for (const result of recipe.results) {
this.game.systems.inventory.addItem(result.id, result.quantity);
}
}
if (this.game.systems.questSystem) {
this.game.systems.questSystem.onItemCrafted?.();
}
console.log(`[CRAFTING] Done: ${recipe.name}`);
return true;
}
selectRecipe(recipeId) {
this.selectedRecipe = this.recipes.get(recipeId);
return this.selectedRecipe;
}
getSelectedRecipe() { return this.selectedRecipe; }
// ------------------------------------------------------------------ //
// UI
// ------------------------------------------------------------------ //
updateUI() {
this.updateRecipeList();
this.updateCraftingDetails();
this.updateCraftingInfo();
}
updateRecipeList() {
const listEl = document.getElementById('recipeList');
if (!listEl) return;
if (!this._loaded) {
listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading recipes from server...</p></div>';
return;
}
const recipes = this.getRecipesByCategory(this.currentCategory);
listEl.innerHTML = '';
if (recipes.length === 0) {
listEl.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
return;
}
recipes.forEach(recipe => {
const el = document.createElement('div');
el.className = 'recipe-item';
el.dataset.recipeId = recipe.id;
const canCraft = this.canCraftRecipe(recipe.id);
const missingMats = this.getMissingMaterials(recipe.id);
const skillSystem = this.game.systems.skillSystem;
let skillsMet = true;
if (recipe.requirements && skillSystem) {
for (const [skill, level] of Object.entries(recipe.requirements)) {
if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; }
}
}
if (!skillsMet) el.classList.add('locked');
else if (!canCraft) el.classList.add('missing-materials');
else el.classList.add('can-craft');
const reqText = recipe.requirements
? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).join(', ')
: 'None';
const matsHtml = recipe.materials.map(mat => {
const mis = missingMats.find(m => m.id === mat.id);
const cur = mis ? mis.current : (this.game.systems.inventory?.getItemCount(mat.id) || 0);
const cls = mis ? 'material-item missing' : 'material-item';
return `<div class="${cls}">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${cur}/${mat.quantity}</span>
</div>`;
}).join('');
el.innerHTML = `
<div class="recipe-header">
<h4>${recipe.name}</h4>
<span class="recipe-level">Level ${reqText}</span>
</div>
<div class="recipe-description">${recipe.description || ''}</div>
<div class="recipe-materials">${matsHtml}</div>
${missingMats.length > 0 ? `
<div class="missing-materials-text">
<i class="fas fa-exclamation-triangle"></i>
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
</div>` : ''}
<div class="recipe-time">
<i class="fas fa-clock"></i>
<span>${(recipe.craftingTime || 0) / 1000}s</span>
</div>
`;
el.addEventListener('click', () => {
this.selectRecipe(recipe.id);
this.updateCraftingDetails();
});
listEl.appendChild(el);
});
}
updateCraftingDetails() {
const detailsEl = document.getElementById('craftingDetails');
if (!detailsEl) return;
if (!this.selectedRecipe) {
detailsEl.innerHTML = `
<div class="selected-recipe">
<h3>Select a Recipe</h3>
<p>Choose a recipe from the list to see details and craft items.</p>
</div>`;
return;
}
const recipe = this.selectedRecipe;
const canCraft = this.canCraftRecipe(recipe.id);
detailsEl.innerHTML = `
<div class="selected-recipe">
<h3>${recipe.name}</h3>
<p class="recipe-description">${recipe.description || ''}</p>
<div class="recipe-requirements">
<h4>Requirements:</h4>
${recipe.requirements
? Object.entries(recipe.requirements).map(([s, l]) =>
`<div class="requirement-item">
<span class="skill-name">${s}</span>
<span class="skill-level">Level ${l}</span>
</div>`).join('')
: '<p>No special requirements</p>'}
</div>
<div class="recipe-materials-needed">
<h4>Materials Needed:</h4>
${recipe.materials.map(mat =>
`<div class="material-needed">
<span class="material-name">${mat.id}</span>
<span class="material-needed">x${mat.quantity}</span>
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
</div>`).join('')}
</div>
<div class="recipe-results">
<h4>Results:</h4>
${recipe.results.map(r =>
`<div class="result-item">
<span class="result-name">${r.id}</span>
<span class="result-quantity">x${r.quantity}</span>
</div>`).join('')}
</div>
<div class="recipe-info">
<div class="experience-reward">
<i class="fas fa-star"></i>
<span>${recipe.experience || 0} XP</span>
</div>
<div class="crafting-time">
<i class="fas fa-clock"></i>
<span>${(recipe.craftingTime || 0) / 1000} seconds</span>
</div>
</div>
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
${canCraft ? 'Craft Item' : 'Cannot Craft'}
</button>
</div>`;
}
updateCraftingInfo() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
const craftingLevel = skillSystem.getSkillLevel('crafting');
const craftingExp = skillSystem.getSkillExperience('crafting');
const expNeeded = skillSystem.getExperienceNeeded('crafting');
const levelEl = document.getElementById('craftingLevel');
const expEl = document.getElementById('craftingExp');
if (levelEl) levelEl.textContent = craftingLevel;
if (expEl) expEl.textContent = `${craftingExp}/${expNeeded}`;
}
switchCategory(category) {
this.currentCategory = category;
this.selectedRecipe = null;
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
}
}
// Export for use in GameEngine
if (typeof module !== 'undefined' && module.exports) {
module.exports = CraftingSystem;
}

View File

@ -0,0 +1,832 @@
/**
* Galaxy Strike Online - Client Dungeon System
* Server-driven dungeon management client
*/
// Create global function for dungeon start that's more reliable
window.startDungeon = function(dungeonId) {
console.log('[DUNGEON SYSTEM] startDungeon called with:', dungeonId);
console.log('[DUNGEON SYSTEM] Game available:', !!window.game);
console.log('[DUNGEON SYSTEM] Game systems available:', !!(window.game && window.game.systems));
console.log('[DUNGEON SYSTEM] Dungeon system available:', !!(window.game && window.game.systems && window.game.systems.dungeonSystem));
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
return window.game.systems.dungeonSystem.startDungeon(dungeonId);
}
console.warn('[DUNGEON SYSTEM] Game systems not available for dungeon start');
};
// Create global function for process encounter that's more reliable
window.processEncounter = function() {
console.log('[DUNGEON SYSTEM] processEncounter called');
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
return window.game.systems.dungeonSystem.processEncounter();
}
console.warn('[DUNGEON SYSTEM] Game systems not available for process encounter');
};
// Create global function for dungeon toggle that's more reliable
window.toggleDungeonSection = function(sectionId) {
// Try to use the dungeon system if available
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
return window.game.systems.dungeonSystem.toggleDungeonSection(sectionId);
}
// Fallback: Direct DOM manipulation
const section = document.getElementById(sectionId);
const indicator = document.getElementById(`${sectionId}-indicator`);
if (!section || !indicator) {
console.warn('[DUNGEON SYSTEM] Section or indicator not found:', sectionId);
return;
}
const isCollapsed = section.classList.contains('collapsed');
if (isCollapsed) {
// Expand
section.classList.remove('collapsed');
indicator.classList.remove('fa-chevron-right');
indicator.classList.add('fa-chevron-down');
} else {
// Collapse
section.classList.add('collapsed');
indicator.classList.remove('fa-chevron-down');
indicator.classList.add('fa-chevron-right');
}
// Save the state in the dungeon system if available
if (window.game && window.game.systems && window.game.systems.dungeonSystem) {
window.game.systems.dungeonSystem.collapseStates.set(sectionId, !isCollapsed);
}
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
};
class DungeonSystem {
constructor(gameEngine) {
this.game = gameEngine;
// Current dungeon state (runtime only)
this.currentDungeon = null;
this.currentRoom = null;
this.dungeonProgress = 0;
this.isExploring = false;
// Debouncing to prevent multiple rapid clicks
this.lastProcessTime = 0;
this.processCooldown = 1000; // 1 second cooldown
// Prevent duplicate event processing
this.lastEncounterData = null;
this.lastNextRoomData = null;
// Store collapse states to preserve them during regeneration
this.collapseStates = new Map();
// Track last generation to prevent unnecessary regenerations
this.lastGenerationTime = 0;
this.generationThrottle = 500; // 500ms throttle
// Server dungeons data
this.serverDungeons = null;
this.roomTypes = {};
this.enemyTemplates = {};
console.log('[DUNGEON SYSTEM] Client DungeonSystem initialized - server-driven mode');
// Set up socket event listeners
this.setupSocketListeners();
}
/**
* Set up Socket.IO event listeners for dungeon data
*/
setupSocketListeners() {
if (!this.game.socket) {
console.warn('[DUNGEON SYSTEM] No socket available for event listeners');
return;
}
// Listen for dungeon data response
this.game.socket.on('dungeons_data', (data) => {
console.log('[DUNGEON SYSTEM] Received dungeons data:', data);
this.serverDungeons = data.dungeons || data;
console.log('[DUNGEON SYSTEM] Loaded grouped dungeons from server:', Object.keys(this.serverDungeons));
// Update UI when data is loaded
this.forceGenerateDungeonList();
});
// Listen for room types response
this.game.socket.on('room_types_data', (data) => {
console.log('[DUNGEON SYSTEM] Received room types data:', data);
this.roomTypes = data;
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.roomTypes).length} room types from server`);
// Update UI when room data is loaded
this.forceGenerateDungeonList();
});
// Listen for enemy templates response
this.game.socket.on('enemy_templates_data', (data) => {
console.log('[DUNGEON SYSTEM] Received enemy templates data:', data);
this.enemyTemplates = data;
console.log(`[DUNGEON SYSTEM] Loaded ${Object.keys(this.enemyTemplates).length} enemy templates from server`);
// Update UI when enemy data is loaded
this.forceGenerateDungeonList();
});
// Listen for dungeon start response
this.game.socket.on('dungeon_started', (data) => {
console.log('[DUNGEON SYSTEM] Dungeon started:', data);
// Handle error responses
if (data.success === false) {
console.error('[DUNGEON SYSTEM] Failed to start dungeon:', data.error);
if (this.game && this.game.showNotification) {
this.game.showNotification(data.error, 'error', 5000);
}
return;
}
// Clear any existing dungeon state first
if (this.currentDungeon) {
console.warn('[DUNGEON SYSTEM] Clearing existing dungeon state before starting new one');
this.currentDungeon = null;
this.currentRoom = null;
this.isExploring = false;
this.dungeonProgress = 0;
}
this.currentDungeon = data.instance;
this.isExploring = true;
this.dungeonProgress = 0;
console.log('[DUNGEON SYSTEM] About to update UI - State:', {
currentDungeon: !!this.currentDungeon,
isExploring: this.isExploring,
dungeonProgress: this.dungeonProgress,
gameUIManager: !!this.game.systems.ui,
instanceId: this.currentDungeon?.id
});
// Update UI to show dungeon exploration
this.updateUI();
// Show notification to player
if (this.game && this.game.showNotification) {
this.game.showNotification(`Entered ${data.instance.dungeonId} dungeon!`, 'success', 3000);
}
});
// Listen for encounter response
this.game.socket.on('encounter_data', (data) => {
// Skip duplicate events
if (this.lastEncounterData &&
this.lastEncounterData.encounterIndex === data.encounterIndex &&
this.lastEncounterData.encounter?.name === data.encounter?.name) {
console.log('[DUNGEON SYSTEM] Skipping duplicate encounter data');
return;
}
this.lastEncounterData = data;
console.log('[DUNGEON SYSTEM] Encounter received:', data);
console.log('[DUNGEON SYSTEM] Current state before update:', {
currentDungeonId: this.currentDungeon?.id,
currentProgress: this.dungeonProgress,
newEncounterIndex: data.encounterIndex,
encounterType: data.encounter?.type,
encounterName: data.encounter?.name
});
this.currentRoom = data.encounter;
this.dungeonProgress = data.encounterIndex; // Use server data, not local increment
// Update UI to show the new encounter
this.updateUI();
});
// Listen for encounter completion (auto-combat)
this.game.socket.on('encounter_completed', (data) => {
console.log('[DUNGEON SYSTEM] Encounter completed:', data);
if (data.success) {
// Check if dungeon is complete
if (data.isComplete) {
console.log('[DUNGEON SYSTEM] Dungeon completed!');
// Clear all dungeon state
this.currentDungeon = null;
this.currentRoom = null;
this.dungeonProgress = 0;
this.isExploring = false;
this.lastEncounterData = null;
this.lastNextRoomData = null;
// Show completion notification
if (this.game && this.game.showNotification) {
this.game.showNotification('Dungeon completed! 🎉', 'success', 5000);
}
// Force UI to show dungeon list
setTimeout(() => {
this.updateUI();
}, 1000);
} else {
this.currentRoom = data.nextEncounter;
this.dungeonProgress = data.encounterIndex;
// Show rewards notification
if (data.rewards && (data.rewards.credits > 0 || data.rewards.experience > 0)) {
const rewardText = [];
if (data.rewards.credits > 0) rewardText.push(`${data.rewards.credits} credits`);
if (data.rewards.experience > 0) rewardText.push(`${data.rewards.experience} exp`);
if (this.game && this.game.showNotification) {
this.game.showNotification(`Combat complete! Gained: ${rewardText.join(', ')}`, 'success', 3000);
}
}
}
// Update UI to show the new state
this.updateUI();
} else {
console.error('[DUNGEON SYSTEM] Error completing encounter:', data.error);
}
});
// Listen for next room response
this.game.socket.on('next_room_data', (data) => {
// Skip duplicate events
if (this.lastNextRoomData &&
this.lastNextRoomData.encounterIndex === data.encounterIndex &&
this.lastNextRoomData.encounter?.name === data.encounter?.name) {
console.log('[DUNGEON SYSTEM] Skipping duplicate next room data');
return;
}
this.lastNextRoomData = data;
console.log('[DUNGEON SYSTEM] Next room received:', data);
console.log('[DUNGEON SYSTEM] Current state before update:', {
currentDungeonId: this.currentDungeon?.id,
currentProgress: this.dungeonProgress,
newEncounterIndex: data.encounterIndex,
encounterType: data.encounter?.type,
encounterName: data.encounter?.name,
isComplete: data.isComplete
});
if (data.success) {
this.currentRoom = data.encounter;
this.dungeonProgress = data.encounterIndex;
// Update UI to show the new room
this.updateUI();
} else {
console.error('[DUNGEON SYSTEM] Error moving to next room:', data.error);
}
});
// Listen for dungeon completion response
this.game.socket.on('dungeon_completed', (data) => {
console.log('[DUNGEON SYSTEM] Dungeon completed:', data);
// Reset dungeon state
this.currentDungeon = null;
this.currentRoom = null;
this.isExploring = false;
this.dungeonProgress = 0;
});
// Listen for dungeon status response
this.game.socket.on('dungeon_status', (data) => {
console.log('[DUNGEON SYSTEM] Dungeon status received:', data);
});
console.log('[DUNGEON SYSTEM] Socket event listeners set up');
}
/**
* Load dungeon data from server using Socket.IO packets
*/
async loadServerData() {
try {
console.log('[DUNGEON SYSTEM] Loading dungeon data from server via packets...');
if (!this.game.socket) {
console.error('[DUNGEON SYSTEM] No socket connection available');
return;
}
// Request dungeons from server
this.game.socket.emit('get_dungeons');
// Request room types from server
this.game.socket.emit('get_room_types');
// Request enemy templates from server
this.game.socket.emit('get_enemy_templates');
console.log('[DUNGEON SYSTEM] Server data requests sent via packets');
} catch (error) {
console.error('[DUNGEON SYSTEM] Error loading server data:', error);
}
}
/**
* Get all available dungeons
*/
getAllDungeons() {
return this.serverDungeons;
}
/**
* Get dungeons by difficulty
*/
getDungeonsByDifficulty(difficulty) {
return this.serverDungeons.filter(dungeon => dungeon.difficulty === difficulty);
}
/**
* Get specific dungeon by ID
*/
getDungeon(dungeonId) {
return this.serverDungeons.find(dungeon => dungeon.id === dungeonId);
}
/**
* Get room type by ID
*/
getRoomType(roomTypeId) {
return this.roomTypes[roomTypeId];
}
/**
* Get enemy template by ID
*/
getEnemyTemplate(enemyId) {
return this.enemyTemplates[enemyId];
}
/**
* Start exploring a dungeon using Socket.IO packets
*/
async startDungeon(dungeonId) {
try {
console.log(`[DUNGEON SYSTEM] Starting dungeon: ${dungeonId}`);
if (!this.game.socket) {
console.error('[DUNGEON SYSTEM] No socket connection available');
return null;
}
// Send packet to start dungeon
this.game.socket.emit('start_dungeon', {
dungeonId: dungeonId,
userId: this.game.systems.player?.id || 'anonymous'
});
console.log('[DUNGEON SYSTEM] Dungeon start packet sent');
return true;
} catch (error) {
console.error('[DUNGEON SYSTEM] Error starting dungeon:', error);
return null;
}
}
/**
* Process encounter in current dungeon room
*/
async processEncounter() {
// Debounce to prevent multiple rapid clicks
const now = Date.now();
if (now - this.lastProcessTime < this.processCooldown) {
console.log('[DUNGEON SYSTEM] Process throttled, please wait...');
return null;
}
this.lastProcessTime = now;
try {
// Safety check - make sure we have an active dungeon
if (!this.currentDungeon) {
console.error('[DUNGEON SYSTEM] No active dungeon to process encounter for');
return null;
}
console.log(`[DUNGEON SYSTEM] Processing encounter for dungeon: ${this.currentDungeon.id}`);
if (!this.game.socket) {
console.error('[DUNGEON SYSTEM] No socket connection available');
return null;
}
// Send packet to process encounter
this.game.socket.emit('process_encounter', {
instanceId: this.currentDungeon.id,
userId: this.game.systems.player?.id || 'anonymous'
});
console.log('[DUNGEON SYSTEM] Encounter process packet sent');
return true;
} catch (error) {
console.error('[DUNGEON SYSTEM] Error processing encounter:', error);
return null;
}
}
/**
* Complete current dungeon using Socket.IO packets
*/
async completeDungeon() {
if (!this.currentDungeon || !this.isExploring) {
console.warn('[DUNGEON SYSTEM] No active dungeon to complete');
return null;
}
try {
console.log(`[DUNGEON SYSTEM] Completing dungeon: ${this.currentDungeon.id}`);
if (!this.game.socket) {
console.error('[DUNGEON SYSTEM] No socket connection available');
return null;
}
// Send packet to complete dungeon
this.game.socket.emit('complete_dungeon', {
instanceId: this.currentDungeon.id,
userId: this.game.systems.player?.id || 'anonymous'
});
console.log('[DUNGEON SYSTEM] Dungeon completion packet sent');
return true;
} catch (error) {
console.error('[DUNGEON SYSTEM] Error completing dungeon:', error);
return null;
}
}
/**
* Get player's current dungeon status using Socket.IO packets
*/
async getDungeonStatus() {
try {
if (!this.game.socket) {
console.error('[DUNGEON SYSTEM] No socket connection available');
return null;
}
// Send packet to get dungeon status
this.game.socket.emit('get_dungeon_status', {
userId: this.game.systems.player?.id || 'anonymous'
});
console.log('[DUNGEON SYSTEM] Dungeon status request packet sent');
return true;
} catch (error) {
console.error('[DUNGEON SYSTEM] Error getting dungeon status:', error);
}
return null;
}
/**
* Force generate dungeon list (bypasses throttle)
*/
forceGenerateDungeonList() {
this.lastGenerationTime = 0; // Reset throttle
this.generateDungeonList();
}
/**
* Generate dungeon list UI using server data
*/
generateDungeonList() {
const now = Date.now();
// Throttle generation to prevent excessive calls
if (now - this.lastGenerationTime < this.generationThrottle) {
return; // Silently skip instead of logging
}
this.lastGenerationTime = now;
// console.log('[DUNGEON SYSTEM] Generating dungeon list UI');
const dungeonListElement = document.getElementById('dungeonList');
if (!dungeonListElement) {
console.error('[DUNGEON SYSTEM] Dungeon list element not found');
return;
}
// Clear existing content
dungeonListElement.innerHTML = '';
if (!this.serverDungeons || Object.keys(this.serverDungeons).length === 0) {
dungeonListElement.innerHTML = '<p>Loading dungeons from server...</p>';
return;
}
// Generate HTML for each difficulty category
let html = '';
Object.entries(this.serverDungeons).forEach(([difficulty, dungeons]) => {
if (!dungeons || dungeons.length === 0) return;
const difficultyClass = difficulty === 'tutorial' ? 'tutorial' : difficulty;
const difficultyTitle = difficulty === 'tutorial' ? 'Tutorial Dungeons' :
difficulty.charAt(0).toUpperCase() + difficulty.slice(1) + ' Dungeons';
const difficultyIcon = this.getDifficultyIcon(difficulty);
const sectionId = `dungeon-section-${difficulty}`;
// Add collapsible difficulty header
html += `
<div class="dungeon-section">
<div class="difficulty-header ${difficultyClass} collapsible" onclick="toggleDungeonSection('${sectionId}')">
<div class="header-content">
<i class="${difficultyIcon}"></i>
<span>${difficultyTitle}</span>
<span class="dungeon-count">(${dungeons.length})</span>
</div>
<div class="collapse-indicator">
<i class="fas fa-chevron-down" id="${sectionId}-indicator"></i>
</div>
</div>
<div class="dungeon-content" id="${sectionId}">
`;
dungeons.forEach(dungeon => {
const canEnter = this.canEnterDungeon(dungeon);
const statusClass = canEnter ? 'available' : 'locked';
const energyCost = dungeon.energyCost || 0;
const healthType = dungeon.healthType || 'player';
const healthIcon = healthType === 'ship' ? '🚀' : '👤';
// Each dungeon in its own individual container using proper CSS classes
html += `
<div class="dungeon-item ${statusClass}" data-dungeon-id="${dungeon.id}">
<div class="dungeon-name">${dungeon.name}</div>
<div class="dungeon-difficulty ${difficulty}">
<i class="${difficultyIcon}"></i> ${difficulty} - ${energyCost} Energy
</div>
<div class="dungeon-description">${dungeon.description}</div>
<div class="health-type">${healthIcon}</div>
<div class="dungeon-enemies">
<strong>Enemies:</strong>
<div class="enemy-list">
${this.generateEnemyList(dungeon.enemyTypes || [])}
</div>
</div>
<button class="dungeon-btn" ${!canEnter ? 'disabled' : ''}
onclick="startDungeon('${dungeon.id}')">
${canEnter ? 'Enter Dungeon' : 'Locked'}
</button>
</div>
`;
});
// Close the section
html += `
</div>
</div>
`;
});
dungeonListElement.innerHTML = html;
// Initialize default collapse states
this.initializeDungeonSections();
}
/**
* Initialize dungeon sections with saved collapse states
*/
initializeDungeonSections() {
// Default states: tutorial and easy expanded, others collapsed
const defaultStates = {
'dungeon-section-tutorial': false, // expanded
'dungeon-section-easy': false, // expanded
'dungeon-section-medium': true, // collapsed
'dungeon-section-hard': true, // collapsed
'dungeon-section-extreme': true // collapsed
};
Object.entries(defaultStates).forEach(([sectionId, defaultCollapsed]) => {
const section = document.getElementById(sectionId);
const indicator = document.getElementById(`${sectionId}-indicator`);
if (section && indicator) {
// Use saved state if available, otherwise use default
const shouldCollapse = this.collapseStates.has(sectionId) ?
this.collapseStates.get(sectionId) : defaultCollapsed;
if (shouldCollapse) {
section.classList.add('collapsed');
indicator.classList.remove('fa-chevron-down');
indicator.classList.add('fa-chevron-right');
}
}
});
}
/**
* Toggle dungeon section collapse/expand
*/
toggleDungeonSection(sectionId) {
// Check if game and systems are available
if (!window.game || !window.game.systems || !window.game.systems.dungeonSystem) {
console.warn('[DUNGEON SYSTEM] Game systems not available for toggle');
return;
}
const section = document.getElementById(sectionId);
const indicator = document.getElementById(`${sectionId}-indicator`);
if (!section || !indicator) return;
const isCollapsed = section.classList.contains('collapsed');
if (isCollapsed) {
// Expand
section.classList.remove('collapsed');
indicator.classList.remove('fa-chevron-right');
indicator.classList.add('fa-chevron-down');
} else {
// Collapse
section.classList.add('collapsed');
indicator.classList.remove('fa-chevron-down');
indicator.classList.add('fa-chevron-right');
}
// Save the state
this.collapseStates.set(sectionId, !isCollapsed);
console.log(`[DUNGEON SYSTEM] Toggled section ${sectionId}: ${isCollapsed ? 'expanded' : 'collapsed'}`);
}
/**
* Get difficulty icon for dungeon
*/
getDifficultyIcon(difficulty) {
const icons = {
tutorial: 'fas fa-graduation-cap',
easy: 'fas fa-smile',
medium: 'fas fa-meh',
hard: 'fas fa-frown',
extreme: 'fas fa-skull'
};
return icons[difficulty] || 'fas fa-question';
}
/**
* Generate enemy list HTML for dungeon
*/
generateEnemyList(enemyTypes) {
if (!enemyTypes || enemyTypes.length === 0) {
return '<span class="no-enemies">No enemies</span>';
}
let html = '';
enemyTypes.forEach(enemyType => {
const enemy = this.getEnemyTemplate(enemyType);
if (enemy) {
html += `
<div class="enemy-item">
<span class="enemy-name">${enemy.name}</span>
<div class="enemy-stats">
<span class="health"> ${enemy.health}</span>
<span class="attack"> ${enemy.attack}</span>
<span class="defense">🛡 ${enemy.defense}</span>
</div>
</div>
`;
}
});
return html;
}
/**
* Check if player can enter dungeon
*/
canEnterDungeon(dungeon) {
if (!this.game.systems.player) {
return false;
}
const playerLevel = this.game.systems.player.stats?.level || 1;
const minLevel = dungeon.minLevel || 1;
const maxLevel = dungeon.maxLevel || 999;
const energyCost = dungeon.energyCost || 0;
const playerEnergy = this.game.systems.player.attributes?.energy || 0;
return playerLevel >= minLevel &&
playerLevel <= maxLevel &&
playerEnergy >= energyCost;
}
/**
* Exit current dungeon
*/
exitDungeon() {
console.log('[DUNGEON SYSTEM] Exiting dungeon');
if (this.currentDungeon) {
// Send exit packet to server
if (this.game.socket) {
this.game.socket.emit('exit_dungeon', {
instanceId: this.currentDungeon.id,
userId: this.game.systems.player?.id || 'anonymous'
});
}
}
// Reset local state
this.currentDungeon = null;
this.currentRoom = null;
this.isExploring = false;
this.dungeonProgress = 0;
// Update UI to show dungeon list
this.updateUI();
// Show notification
if (this.game && this.game.showNotification) {
this.game.showNotification('Exited dungeon', 'info', 2000);
}
}
/**
* Move to next room (for rooms without enemies)
*/
moveToNextRoom() {
console.log('[DUNGEON SYSTEM] Moving to next room');
if (!this.currentDungeon) {
console.warn('[DUNGEON SYSTEM] No active dungeon to continue');
return;
}
// Request next room from server
if (this.game.socket) {
this.game.socket.emit('next_room', {
instanceId: this.currentDungeon.id,
userId: this.game.systems.player?.id || 'anonymous'
});
}
}
/**
* Update UI with current dungeon information
*/
updateUI() {
if (this.game.systems.ui) {
this.game.systems.ui.updateDungeonUI({
currentDungeon: this.currentDungeon,
currentRoom: this.currentRoom,
progress: this.dungeonProgress,
isExploring: this.isExploring
});
} else {
console.warn('[DUNGEON SYSTEM] UI manager not available in game.systems.ui');
}
}
/**
* Initialize system and load server data
*/
async initialize() {
console.log('[DUNGEON SYSTEM] Initializing client dungeon system...');
// Set up socket listeners if not already done
if (!this.game.socket) {
console.warn('[DUNGEON SYSTEM] Socket not available during initialization, will retry...');
// Retry after a short delay
setTimeout(() => {
if (this.game.socket) {
this.setupSocketListeners();
this.loadServerData();
} else {
console.error('[DUNGEON SYSTEM] Socket still not available after retry');
}
}, 1000);
return;
}
this.setupSocketListeners();
await this.loadServerData();
}
}
// Export DungeonSystem to global scope
if (typeof window !== 'undefined') {
window.DungeonSystem = DungeonSystem;
}
// Export for use in GameEngine
if (typeof module !== 'undefined' && module.exports) {
module.exports = DungeonSystem;
}

View File

@ -12,11 +12,18 @@ class IdleSystem {
this.lastActiveTime = Date.now();
this.accumulatedTime = 0; // Track time for resource generation
// Idle production rates
// Idle production rates (online rates)
this.productionRates = {
credits: 10, // credits per second (increased for better gameplay)
experience: 1, // experience per second (increased for better progression)
energy: 0.5 // energy regeneration per second
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
@ -144,6 +151,20 @@ class IdleSystem {
}
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) {

View File

@ -0,0 +1,468 @@
/**
* 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;
}

View File

@ -13,6 +13,10 @@ class QuestSystem {
this.game = gameEngine;
// Server time synchronization
this.serverTimeOffset = 0; // Difference between server and client time
this.lastServerTimeSync = 0;
// Quest types
this.questTypes = {
main: 'Main Story',
@ -35,682 +39,56 @@ class QuestSystem {
questStatus: Object.keys(this.questStatus)
});
// Main story quests
this.mainQuests = [
{
id: 'tutorial_complete',
name: 'First Steps',
description: 'Complete the tutorial dungeon and learn the basics',
type: 'main',
status: 'available',
requirements: { level: 1 },
objectives: [
{ id: 'clear_tutorial_dungeon', description: 'Complete the tutorial dungeon', target: 1, current: 0, type: 'tutorial_dungeon' },
{ id: 'reach_level_2', description: 'Reach level 2', target: 2, current: 0, type: 'level' }
],
rewards: { credits: 500, experience: 100, gems: 5 },
nextQuest: 'first_ship_upgrade'
},
{
id: 'first_ship_upgrade',
name: 'Ship Enhancement',
description: 'Upgrade your ship for better performance',
type: 'main',
status: 'available',
requirements: { quest: 'tutorial_complete' },
objectives: [
{ id: 'upgrade_weapon', description: 'Upgrade ship weapons', target: 1, current: 0, type: 'upgrade' },
{ id: 'upgrade_shield', description: 'Upgrade ship shields', target: 1, current: 0, type: 'upgrade' }
],
rewards: { credits: 1000, experience: 200, gems: 10 },
nextQuest: 'join_guild'
},
{
id: 'join_guild',
name: 'Guild Recruitment',
description: 'Join a guild and participate in guild activities',
type: 'main',
status: 'available',
requirements: { quest: 'first_ship_upgrade', level: 5 },
objectives: [
{ id: 'join_guild', description: 'Join a guild', target: 1, current: 0, type: 'guild' },
{ id: 'guild_contribution', description: 'Contribute to guild', target: 100, current: 0, type: 'contribution' }
],
rewards: { credits: 2000, experience: 500, gems: 20 },
nextQuest: 'master_commander'
},
{
id: 'master_commander',
name: 'Master Commander',
description: 'Become a master commander and lead your fleet to victory',
type: 'main',
status: 'available',
requirements: { quest: 'join_guild', level: 10 },
objectives: [
{ id: 'reach_level_10', description: 'Reach level 10', target: 10, current: 0, type: 'level' },
{ id: 'clear_10_dungeons', description: 'Clear 10 dungeons', target: 10, current: 0, type: 'dungeon' },
{ id: 'max_skill', description: 'Max out one skill', target: 10, current: 0, type: 'skill' }
],
rewards: { credits: 5000, experience: 1000, gems: 50, item: 'legendary_weapon' }
}
];
// Main story quests - populated by server
this.mainQuests = [];
// All possible daily quests (20 total)
this.allDailyQuests = [
// Easy quests (difficulty: 1)
{
id: 'daily_dungeon_easy',
name: 'Quick Dungeon Run',
description: 'Complete any dungeon',
type: 'daily',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'clear_dungeon', description: 'Clear 1 dungeon', target: 1, current: 0, type: 'dungeon' }
],
rewards: { credits: 100, experience: 25, gems: 1 }
},
{
id: 'daily_combat_easy',
name: 'Light Combat',
description: 'Defeat a few enemies',
type: 'daily',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'defeat_enemies', description: 'Defeat 10 enemies', target: 10, current: 0, type: 'combat' }
],
rewards: { credits: 80, experience: 20, gems: 1 }
},
{
id: 'daily_crafting_easy',
name: 'Basic Crafting',
description: 'Craft some items',
type: 'daily',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'craft_items', description: 'Craft 2 items', target: 2, current: 0, type: 'crafting' }
],
rewards: { credits: 90, experience: 22, gems: 1 }
},
{
id: 'daily_level_easy',
name: 'Level Up',
description: 'Gain experience and level up',
type: 'daily',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'gain_level', description: 'Gain 1 level', target: 1, current: 0, type: 'level' }
],
rewards: { credits: 120, experience: 30, gems: 2 }
},
{
id: 'daily_energy_easy',
name: 'Energy Management',
description: 'Use energy efficiently',
type: 'daily',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'use_energy', description: 'Use 50 energy', target: 50, current: 0, type: 'energy' }
],
rewards: { credits: 70, experience: 18, gems: 1 }
},
// Medium quests (difficulty: 2)
{
id: 'daily_dungeon_medium',
name: 'Dungeon Explorer',
description: 'Complete multiple dungeons',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'clear_dungeons', description: 'Clear 3 dungeons', target: 3, current: 0, type: 'dungeon' }
],
rewards: { credits: 300, experience: 75, gems: 3 }
},
{
id: 'daily_combat_medium',
name: 'Combat Training',
description: 'Defeat enemies in combat',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'defeat_enemies', description: 'Defeat 20 enemies', target: 20, current: 0, type: 'combat' }
],
rewards: { credits: 150, experience: 40, gems: 1 }
},
{
id: 'daily_combat_hard',
name: 'Combat Veteran',
description: 'Defeat many enemies',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'defeat_enemies', description: 'Defeat 50 enemies', target: 50, current: 0, type: 'combat' }
],
rewards: { credits: 250, experience: 60, gems: 3 }
},
{
id: 'daily_crafting_medium',
name: 'Master Crafter',
description: 'Craft many items',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'craft_items', description: 'Craft 5 items', target: 5, current: 0, type: 'crafting' }
],
rewards: { credits: 280, experience: 70, gems: 3 }
},
{
id: 'daily_upgrade_medium',
name: 'Equipment Upgrade',
description: 'Upgrade your equipment',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'upgrade_items', description: 'Upgrade 3 items', target: 3, current: 0, type: 'upgrade' }
],
rewards: { credits: 320, experience: 80, gems: 4 }
},
{
id: 'daily_wealth_medium',
name: 'Wealth Accumulator',
description: 'Earn credits',
type: 'daily',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'earn_credits', description: 'Earn 1000 credits', target: 1000, current: 0, type: 'credits' }
],
rewards: { credits: 400, experience: 50, gems: 3 }
},
// Hard quests (difficulty: 3)
{
id: 'daily_dungeon_hard',
name: 'Dungeon Master',
description: 'Complete many dungeons',
type: 'daily',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'clear_dungeons', description: 'Clear 5 dungeons', target: 5, current: 0, type: 'dungeon' }
],
rewards: { credits: 600, experience: 150, gems: 6 }
},
{
id: 'daily_combat_hard',
name: 'Combat Master',
description: 'Defeat many powerful enemies',
type: 'daily',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'defeat_enemies', description: 'Defeat 100 enemies', target: 100, current: 0, type: 'combat' }
],
rewards: { credits: 500, experience: 120, gems: 5 }
},
{
id: 'daily_level_hard',
name: 'Power Leveling',
description: 'Gain multiple levels',
type: 'daily',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'gain_levels', description: 'Gain 3 levels', target: 3, current: 0, type: 'level' }
],
rewards: { credits: 700, experience: 200, gems: 7 }
},
{
id: 'daily_boss_hard',
name: 'Boss Hunter',
description: 'Defeat boss enemies',
type: 'daily',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'defeat_bosses', description: 'Defeat 3 bosses', target: 3, current: 0, type: 'boss' }
],
rewards: { credits: 800, experience: 180, gems: 8 }
},
{
id: 'daily_collection_hard',
name: 'Master Collector',
description: 'Collect rare items',
type: 'daily',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'collect_rare', description: 'Collect 10 rare items', target: 10, current: 0, type: 'collection' }
],
rewards: { credits: 650, experience: 140, gems: 6 }
},
// Special quests (difficulty: 4)
{
id: 'daily_speedrun',
name: 'Speed Runner',
description: 'Complete dungeons quickly',
type: 'daily',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'fast_dungeon', description: 'Complete 2 dungeons under 5 minutes', target: 2, current: 0, type: 'speedrun' }
],
rewards: { credits: 1000, experience: 250, gems: 10 }
},
{
id: 'daily_perfection',
name: 'Perfectionist',
description: 'Complete objectives without taking damage',
type: 'daily',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'perfect_runs', description: '3 perfect dungeon runs', target: 3, current: 0, type: 'perfect' }
],
rewards: { credits: 1200, experience: 300, gems: 12 }
},
{
id: 'daily_multitask',
name: 'Multitask Master',
description: 'Complete multiple quest types',
type: 'daily',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'dungeon_task', description: 'Clear 2 dungeons', target: 2, current: 0, type: 'dungeon' },
{ id: 'combat_task', description: 'Defeat 30 enemies', target: 30, current: 0, type: 'combat' },
{ id: 'craft_task', description: 'Craft 2 items', target: 2, current: 0, type: 'crafting' }
],
rewards: { credits: 1500, experience: 400, gems: 15 }
},
{
id: 'daily_endurance',
name: 'Endurance Test',
description: 'Complete long activities',
type: 'daily',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'long_dungeon', description: 'Complete 1 dungeon without healing', target: 1, current: 0, type: 'endurance' }
],
rewards: { credits: 1100, experience: 280, gems: 11 }
},
{
id: 'daily_legendary',
name: 'Legendary Challenge',
description: 'Complete legendary difficulty content',
type: 'daily',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'legendary_content', description: 'Complete 1 legendary dungeon', target: 1, current: 0, type: 'legendary' }
],
rewards: { credits: 2000, experience: 500, gems: 20, item: 'rare_material' }
}
];
// Weekly quests (25 total quests with varied objectives)
this.allWeeklyQuests = [
// Combat-focused weekly quests
{
id: 'weekly_combat_basic',
name: 'Weekly Combat Duty',
description: 'Complete combat objectives throughout the week',
type: 'weekly',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'defeat_enemies', description: 'Defeat 100 enemies', target: 100, current: 0, type: 'combat' }
],
rewards: { credits: 800, experience: 200, gems: 8 }
},
{
id: 'weekly_combat_elite',
name: 'Elite Hunter Weekly',
description: 'Hunt down elite enemies',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'defeat_elites', description: 'Defeat 25 elite enemies', target: 25, current: 0, type: 'elite_combat' }
],
rewards: { credits: 1500, experience: 400, gems: 15 }
},
{
id: 'weekly_boss_hunter',
name: 'Boss Hunter Weekly',
description: 'Defeat powerful boss enemies',
type: 'weekly',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'defeat_bosses', description: 'Defeat 10 bosses', target: 10, current: 0, type: 'boss' }
],
rewards: { credits: 2500, experience: 600, gems: 25, item: 'boss_material' }
},
// Dungeon-focused weekly quests
{
id: 'weekly_dungeon_explorer',
name: 'Weekly Dungeon Explorer',
description: 'Explore various dungeons',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'clear_dungeons', description: 'Clear 15 dungeons', target: 15, current: 0, type: 'dungeon' }
],
rewards: { credits: 1200, experience: 300, gems: 12 }
},
{
id: 'weekly_dungeon_master',
name: 'Weekly Dungeon Master',
description: 'Master difficult dungeons',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'clear_hard_dungeons', description: 'Clear 10 hard dungeons', target: 10, current: 0, type: 'hard_dungeon' }
],
rewards: { credits: 2000, experience: 500, gems: 20 }
},
{
id: 'weekly_dungeon_extreme',
name: 'Extreme Dungeon Challenge',
description: 'Conquer extreme difficulty dungeons',
type: 'weekly',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'clear_extreme_dungeons', description: 'Clear 5 extreme dungeons', target: 5, current: 0, type: 'extreme_dungeon' }
],
rewards: { credits: 3500, experience: 800, gems: 35, item: 'extreme_material' }
},
// Crafting and upgrade weekly quests
{
id: 'weekly_crafting_basic',
name: 'Weekly Crafting Session',
description: 'Craft items throughout the week',
type: 'weekly',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'craft_items', description: 'Craft 20 items', target: 20, current: 0, type: 'crafting' }
],
rewards: { credits: 600, experience: 150, gems: 6 }
},
{
id: 'weekly_crafting_master',
name: 'Master Crafter Weekly',
description: 'Craft advanced items',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'craft_advanced', description: 'Craft 10 advanced items', target: 10, current: 0, type: 'advanced_crafting' }
],
rewards: { credits: 1800, experience: 450, gems: 18 }
},
{
id: 'weekly_upgrade_specialist',
name: 'Weekly Upgrade Specialist',
description: 'Upgrade equipment and systems',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'upgrade_items', description: 'Upgrade 15 items', target: 15, current: 0, type: 'upgrade' }
],
rewards: { credits: 1400, experience: 350, gems: 14 }
},
// Progression weekly quests
{
id: 'weekly_level_up',
name: 'Weekly Level Up Challenge',
description: 'Gain levels throughout the week',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'gain_levels', description: 'Gain 5 levels', target: 5, current: 0, type: 'level' }
],
rewards: { credits: 1000, experience: 250, gems: 10 }
},
{
id: 'weekly_skill_master',
name: 'Weekly Skill Mastery',
description: 'Improve your skills',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'improve_skills', description: 'Gain 20 skill points', target: 20, current: 0, type: 'skill' }
],
rewards: { credits: 1600, experience: 400, gems: 16 }
},
// Resource and wealth weekly quests
{
id: 'weekly_wealth_collector',
name: 'Weekly Wealth Collector',
description: 'Accumulate wealth',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'earn_credits', description: 'Earn 5000 credits', target: 5000, current: 0, type: 'credits' }
],
rewards: { credits: 2000, experience: 300, gems: 12 }
},
{
id: 'weekly_resource_gatherer',
name: 'Weekly Resource Gathering',
description: 'Collect valuable resources',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'collect_resources', description: 'Collect 500 resources', target: 500, current: 0, type: 'collection' }
],
rewards: { credits: 1200, experience: 320, gems: 13 }
},
// Special activity weekly quests
{
id: 'weekly_energy_management',
name: 'Weekly Energy Management',
description: 'Use energy efficiently',
type: 'weekly',
difficulty: 1,
status: 'available',
objectives: [
{ id: 'use_energy', description: 'Use 500 energy', target: 500, current: 0, type: 'energy' }
],
rewards: { credits: 800, experience: 180, gems: 8 }
},
{
id: 'weekly_speed_demon',
name: 'Weekly Speed Demon',
description: 'Complete activities quickly',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'speed_runs', description: 'Complete 10 speed runs', target: 10, current: 0, type: 'speedrun' }
],
rewards: { credits: 2200, experience: 550, gems: 22 }
},
{
id: 'weekly_perfectionist',
name: 'Weekly Perfectionist',
description: 'Complete flawless runs',
type: 'weekly',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'perfect_runs', description: 'Complete 8 perfect runs', target: 8, current: 0, type: 'perfect' }
],
rewards: { credits: 3000, experience: 700, gems: 30, item: 'perfection_material' }
},
// Multi-objective weekly quests
{
id: 'weekly_all_rounder',
name: 'Weekly All-Rounder',
description: 'Complete various activities',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'dungeon_task', description: 'Clear 8 dungeons', target: 8, current: 0, type: 'dungeon' },
{ id: 'combat_task', description: 'Defeat 50 enemies', target: 50, current: 0, type: 'combat' },
{ id: 'craft_task', description: 'Craft 5 items', target: 5, current: 0, type: 'crafting' }
],
rewards: { credits: 2500, experience: 600, gems: 25 }
},
{
id: 'weekly_specialist',
name: 'Weekly Specialist',
description: 'Focus on specialized activities',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'special_dungeons', description: 'Clear 5 themed dungeons', target: 5, current: 0, type: 'themed_dungeon' },
{ id: 'special_crafting', description: 'Craft 8 themed items', target: 8, current: 0, type: 'themed_crafting' }
],
rewards: { credits: 2300, experience: 580, gems: 23 }
},
// Exploration and discovery weekly quests
{
id: 'weekly_explorer',
name: 'Weekly Explorer',
description: 'Explore new areas and content',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'explore_areas', description: 'Explore 20 new areas', target: 20, current: 0, type: 'exploration' }
],
rewards: { credits: 1300, experience: 340, gems: 13 }
},
{
id: 'weekly_discovery',
name: 'Weekly Discovery',
description: 'Discover hidden secrets',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'discover_secrets', description: 'Discover 15 secrets', target: 15, current: 0, type: 'discovery' }
],
rewards: { credits: 1900, experience: 480, gems: 19 }
},
// Endurance and challenge weekly quests
{
id: 'weekly_endurance',
name: 'Weekly Endurance Test',
description: 'Complete long-form challenges',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'endurance_runs', description: 'Complete 5 endurance runs', target: 5, current: 0, type: 'endurance' }
],
rewards: { credits: 2100, experience: 530, gems: 21 }
},
{
id: 'weekly_survivor',
name: 'Weekly Survivor',
description: 'Survive challenging conditions',
type: 'weekly',
difficulty: 4,
status: 'available',
objectives: [
{ id: 'survival_runs', description: 'Complete 3 survival runs', target: 3, current: 0, type: 'survival' }
],
rewards: { credits: 3200, experience: 750, gems: 32, item: 'survival_material' }
},
// Social and community weekly quests
{
id: 'weekly_helper',
name: 'Weekly Helper',
description: 'Assist other players',
type: 'weekly',
difficulty: 2,
status: 'available',
objectives: [
{ id: 'assist_players', description: 'Assist 10 players', target: 10, current: 0, type: 'assist' }
],
rewards: { credits: 1100, experience: 280, gems: 11 }
},
{
id: 'weekly_leader',
name: 'Weekly Leader',
description: 'Lead group activities',
type: 'weekly',
difficulty: 3,
status: 'available',
objectives: [
{ id: 'lead_activities', description: 'Lead 5 group activities', target: 5, current: 0, type: 'leadership' }
],
rewards: { credits: 1700, experience: 430, gems: 17 }
},
// Legendary weekly quests
{
id: 'weekly_legendary',
name: 'Weekly Legendary Challenge',
description: 'Complete legendary difficulty content',
type: 'weekly',
difficulty: 5,
status: 'available',
objectives: [
{ id: 'legendary_content', description: 'Complete 3 legendary dungeons', target: 3, current: 0, type: 'legendary' }
],
rewards: { credits: 5000, experience: 1200, gems: 50, item: 'legendary_material' }
},
{
id: 'weekly_mythic',
name: 'Weekly Mythic Trial',
description: 'Face mythic level challenges',
type: 'weekly',
difficulty: 5,
status: 'available',
objectives: [
{ id: 'mythic_trials', description: 'Complete 2 mythic trials', target: 2, current: 0, type: 'mythic' }
],
rewards: { credits: 6000, experience: 1500, gems: 60, item: 'mythic_material' }
}
];
// Currently active daily quests (3 random from allDailyQuests)
// Daily quests - populated by server
this.dailyQuests = [];
this.selectedDailyQuests = [];
// Currently active weekly quests (5 random from allWeeklyQuests)
this.weeklyQuests = [];
this.selectedWeeklyQuests = [];
// Current active quests
this.activeQuests = [];
this.completedQuests = [];
this.failedQuests = [];
this.completedDailyQuests = []; // History of completed daily quests
this.completedWeeklyQuests = []; // History of completed weekly quests
// Quest tracking arrays
this.selectedDailyQuests = [];
this.selectedWeeklyQuests = [];
this.completedDailyQuests = [];
this.completedWeeklyQuests = [];
// Procedural quest templates (server-driven)
this.proceduralTemplates = {};
// Quest generation settings
this.maxProceduralQuests = 3;
this.proceduralQuestRefresh = 30 * 60 * 1000; // 30 minutes
// Initialize stats
this.stats = {
questsCompleted: 0,
dailyQuestsCompleted: 0,
weeklyQuestsCompleted: 0,
totalRewardsEarned: { credits: 0, experience: 0, gems: 0 },
lastDailyReset: this.getServerTime(),
lastWeeklyReset: this.getServerTime()
};
console.log('[QUEST SYSTEM] Client quest system initialized - waiting for server data');
if (debugLogger) debugLogger.endStep('QuestSystem.constructor', {
mainQuestsCount: this.mainQuests.length,
dailyQuestsCount: this.dailyQuests.length,
weeklyQuestsCount: this.weeklyQuests.length,
maxProceduralQuests: this.maxProceduralQuests,
proceduralQuestRefresh: this.proceduralQuestRefresh,
initialStats: this.stats,
dailyQuestsInitialized: this.dailyQuests.length,
weeklyQuestsInitialized: this.weeklyQuests.length
});
// Start countdown timers for server-driven quests
console.log('[QUEST SYSTEM] Starting countdown timers');
this.startDailyCountdown();
this.startWeeklyCountdown();
// Initialize daily quests with safety check
try {
@ -788,14 +166,15 @@ class QuestSystem {
this.maxProceduralQuests = 3;
this.proceduralQuestRefresh = 30 * 60 * 1000; // 30 minutes
// Statistics
// Initialize stats
this.stats = {
questsCompleted: 0,
questsFailed: 0,
dailyQuestsCompleted: 0,
weeklyQuestsCompleted: 0,
totalRewardsEarned: { credits: 0, experience: 0, gems: 0 },
lastDailyReset: Date.now(),
lastWeeklyReset: Date.now()
lastDailyReset: this.getServerTime(),
lastWeeklyReset: this.getServerTime()
};
// Initialize daily quests
@ -816,6 +195,43 @@ class QuestSystem {
});
}
// Server time synchronization methods
getServerTime() {
// In multiplayer mode, use UTC time as server time
if (window.smartSaveManager?.isMultiplayer) {
// Get current UTC timestamp
const utcTime = Date.now();
console.log('[QUEST SYSTEM] Using UTC time as server time:', utcTime);
console.log('[QUEST SYSTEM] Local time:', Date.now());
return utcTime;
}
// Fallback to client time in singleplayer
return Date.now();
}
getServerDate() {
// Create a date that displays in UTC
const timestamp = this.getServerTime();
const utcDate = new Date(timestamp);
// Force UTC display by using UTC methods
return {
getTime: () => timestamp,
getDay: () => utcDate.getUTCDay(),
getHours: () => utcDate.getUTCHours(),
getMinutes: () => utcDate.getUTCMinutes(),
getSeconds: () => utcDate.getUTCSeconds(),
getDate: () => utcDate.getUTCDate(),
getFullYear: () => utcDate.getUTCFullYear(),
getMonth: () => utcDate.getUTCMonth(),
toString: () => utcDate.toUTCString(),
valueOf: () => timestamp
};
}
async initialize() {
const debugLogger = window.debugLogger;
@ -1119,7 +535,7 @@ class QuestSystem {
// Complete quest
quest.status = 'completed';
quest.completedAt = Date.now();
quest.completedAt = this.getServerTime();
this.completedQuests.push(quest);
if (debugLogger) debugLogger.logStep('Quest marked as completed', {
@ -1131,7 +547,7 @@ class QuestSystem {
// Save completed daily quests to history
if (quest.type === 'daily') {
const questCopy = { ...quest, completedAt: Date.now() };
const questCopy = { ...quest, completedAt: this.getServerTime() };
this.completedDailyQuests.push(questCopy);
if (debugLogger) debugLogger.logStep('Daily quest added to history', {
@ -1143,7 +559,7 @@ class QuestSystem {
// Save completed weekly quests to history
if (quest.type === 'weekly') {
const questCopy = { ...quest, completedAt: Date.now() };
const questCopy = { ...quest, completedAt: this.getServerTime() };
this.completedWeeklyQuests.push(questCopy);
if (debugLogger) debugLogger.logStep('Weekly quest added to history', {
@ -1542,7 +958,7 @@ class QuestSystem {
this.completedQuests = [];
this.dailyQuests = [];
this.selectedDailyQuests = [];
this.lastDailyReset = Date.now();
this.lastDailyReset = this.getServerTime();
// Reset main quest statuses
this.mainQuests.forEach(quest => {
@ -1575,7 +991,7 @@ class QuestSystem {
checkDailyReset() {
const debugLogger = window.debugLogger;
const now = Date.now();
const now = this.getServerTime();
const lastReset = this.lastDailyReset;
const daysSinceReset = Math.floor((now - lastReset) / (24 * 60 * 60 * 1000));
@ -1646,10 +1062,10 @@ class QuestSystem {
updateDailyCountdown() {
// Always update countdown so it's ready when user switches to daily tab
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0); // Set to midnight
const now = this.getServerDate();
const tomorrow = new Date();
tomorrow.setUTCDate(now.getDate() + 1);
tomorrow.setUTCHours(0, 0, 0, 0); // Set to midnight UTC
const timeUntilReset = tomorrow - now;
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
@ -1714,11 +1130,11 @@ class QuestSystem {
checkWeeklyReset() {
const debugLogger = window.debugLogger;
const now = Date.now();
const now = this.getServerTime();
const lastReset = this.lastWeeklyReset || 0;
// Calculate if we need to reset based on Saturday midnight
const currentDateTime = new Date();
// Calculate if we need to reset based on Saturday midnight (server time)
const currentDateTime = this.getServerDate();
const dayOfWeek = currentDateTime.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
const currentHour = currentDateTime.getHours();
const currentMinute = currentDateTime.getMinutes();
@ -1814,7 +1230,7 @@ class QuestSystem {
}
updateWeeklyCountdown() {
const now = new Date();
const now = this.getServerDate();
const dayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
@ -1848,19 +1264,19 @@ class QuestSystem {
console.log('[QUEST SYSTEM] Days until Saturday:', daysUntilSaturday);
// Create the target reset time (Saturday midnight)
const nextSaturday = new Date(now);
nextSaturday.setDate(now.getDate() + daysUntilSaturday);
nextSaturday.setHours(0, 0, 0, 0); // Set to midnight
// Create the target reset time (Saturday midnight UTC)
const nextSaturday = new Date();
nextSaturday.setUTCDate(now.getDate() + daysUntilSaturday);
nextSaturday.setUTCHours(0, 0, 0, 0); // Set to midnight UTC
console.log('[QUEST SYSTEM] Next Saturday reset time:', nextSaturday.toString());
console.log('[QUEST SYSTEM] Next Saturday reset time:', nextSaturday.toUTCString());
let timeUntilReset = nextSaturday.getTime() - now.getTime();
// Ensure timeUntilReset is positive (handle edge cases)
if (timeUntilReset <= 0) {
console.log('[QUEST SYSTEM] Time until reset is negative or zero, adding 7 days');
nextSaturday.setDate(nextSaturday.getDate() + 7);
nextSaturday.setUTCDate(nextSaturday.getDate() + 7);
timeUntilReset = nextSaturday.getTime() - now.getTime();
}
@ -2065,8 +1481,58 @@ class QuestSystem {
return completed.sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0));
}
// Load quests from server data
loadServerQuests(serverQuestData) {
console.log('[QUEST SYSTEM] Loading server quest data:', serverQuestData);
if (!serverQuestData) {
console.log('[QUEST SYSTEM] No server quest data provided');
return;
}
// Clear existing quests
this.mainQuests = [];
this.dailyQuests = [];
this.weeklyQuests = [];
this.activeQuests = [];
this.completedQuests = [];
// Load quests from server data
if (serverQuestData.mainQuests && Array.isArray(serverQuestData.mainQuests)) {
this.mainQuests = serverQuestData.mainQuests;
console.log('[QUEST SYSTEM] Loaded', this.mainQuests.length, 'main quests');
}
if (serverQuestData.dailyQuests && Array.isArray(serverQuestData.dailyQuests)) {
this.dailyQuests = serverQuestData.dailyQuests;
console.log('[QUEST SYSTEM] Loaded', this.dailyQuests.length, 'daily quests');
}
if (serverQuestData.activeQuests && Array.isArray(serverQuestData.activeQuests)) {
this.activeQuests = serverQuestData.activeQuests;
console.log('[QUEST SYSTEM] Loaded', this.activeQuests.length, 'active quests');
}
if (serverQuestData.completedQuests && Array.isArray(serverQuestData.completedQuests)) {
this.completedQuests = serverQuestData.completedQuests;
console.log('[QUEST SYSTEM] Loaded', this.completedQuests.length, 'completed quests');
}
console.log('[QUEST SYSTEM] Server quest data loaded successfully');
console.log('[QUEST SYSTEM] Total quests loaded:', {
main: this.mainQuests.length,
daily: this.dailyQuests.length,
active: this.activeQuests.length,
completed: this.completedQuests.length
});
}
// UI updates
updateUI() {
console.log('[QUEST SYSTEM] updateUI called');
console.log('[QUEST SYSTEM] Game available:', !!this.game);
console.log('[QUEST SYSTEM] Multiplayer mode:', window.smartSaveManager?.isMultiplayer);
this.updateQuestList();
this.updateQuestStats();
}
@ -2084,6 +1550,7 @@ class QuestSystem {
const quests = this.getQuestsByType(activeType);
console.log(`[QUEST SYSTEM] Getting quests for type: ${activeType}, found: ${quests.length} quests`);
console.log('[QUEST SYSTEM] First few quests:', quests.slice(0, 3));
questListElement.innerHTML = '';

View File

@ -60,6 +60,22 @@ class ShipSystem {
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');
@ -68,17 +84,13 @@ class ShipSystem {
card.innerHTML = `
<div class="ship-card-header">
<img src="${ship.image}" alt="${ship.name}" class="ship-card-image">
<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>
<div class="ship-card-actions">
<button class="btn-action btn-switch" onclick="game.systems.ship.switchShip('${ship.id}')"
${ship.status === 'active' ? 'disabled' : ''}>
${ship.status === 'active' ? 'ACTIVE' : 'SWITCH'}
</button>
</div>
`;
return card;
@ -106,10 +118,22 @@ class ShipSystem {
const ship = player.ship;
if (elements.currentShipImage) {
// Use the ship's texture if available, otherwise fallback
const imagePath = ship.texture || `assets/textures/ships/starter_cruiser.png`;
// 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';

View File

@ -0,0 +1,407 @@
/**
* Galaxy Strike Online - Client Skill System
* Skill definitions are loaded from the server; this file handles
* local progression tracking, UI rendering, and skill-point spending.
*/
class SkillSystem {
constructor(gameEngine) {
this.game = gameEngine;
// Populated after server responds to 'get_skills'
this.skills = { combat: {}, science: {}, crafting: {} };
this.categories = {
combat: 'Combat',
science: 'Science',
crafting: 'Crafting'
};
this.experienceRates = { combat: 1.0, science: 0.8, crafting: 0.6 };
this.activeBuffs = {};
// Loading state
this._loaded = false;
this._loading = false;
}
// ------------------------------------------------------------------ //
// Initialisation — request skill definitions from the server
// ------------------------------------------------------------------ //
async initialize() {
if (this._loaded || this._loading) return;
this._loading = true;
console.log('[SKILL SYSTEM] Requesting skill definitions from server');
if (!window.game?.socket) {
console.warn('[SKILL SYSTEM] No socket connection — skills will load when connected');
this._loading = false;
return;
}
try {
const serverSkills = await this._fetchSkillsFromServer();
this._applyServerDefinitions(serverSkills);
this._loaded = true;
console.log('[SKILL SYSTEM] Skill definitions loaded from server');
} catch (err) {
console.error('[SKILL SYSTEM] Failed to load skills from server:', err);
} finally {
this._loading = false;
}
}
_fetchSkillsFromServer() {
return new Promise((resolve, reject) => {
const socket = window.game.socket;
const timeout = setTimeout(() => {
socket.off('skills_data', handler);
reject(new Error('Skill data request timed out'));
}, 10000);
const handler = (data) => {
clearTimeout(timeout);
socket.off('skills_data', handler);
if (data && (Array.isArray(data) || typeof data === 'object')) {
resolve(data);
} else {
reject(new Error('Invalid skill data from server'));
}
};
socket.on('skills_data', handler);
socket.emit('get_skills');
});
}
/**
* Merge server skill definitions into the local skill map.
* Preserves any progress already loaded from playerData.
*/
_applyServerDefinitions(serverSkills) {
// Server may return an array or a categorised object
const asList = Array.isArray(serverSkills)
? serverSkills
: Object.values(serverSkills).flat();
// Reset to empty categories first
this.skills = { combat: {}, science: {}, crafting: {} };
for (const skill of asList) {
const cat = skill.category || 'combat';
if (!this.skills[cat]) this.skills[cat] = {};
// Keep any existing progress if already loaded from save data
const existing = this.skills[cat][skill.id] || {};
this.skills[cat][skill.id] = {
name: skill.name,
description: skill.description,
maxLevel: skill.maxLevel || 100,
effects: skill.bonuses || skill.effects || {},
icon: skill.icon || 'fa-star',
// Progress fields — kept from existing save data if present
currentLevel: existing.currentLevel ?? 0,
experience: existing.experience ?? 0,
experienceToNext: existing.experienceToNext ?? (skill.experiencePerLevel || 1000),
unlocked: existing.unlocked ?? (skill.defaultUnlocked !== false),
requiredLevel: skill.requiredLevel ?? null
};
}
console.log('[SKILL SYSTEM] Applied server definitions. Categories:', Object.keys(this.skills));
}
// ------------------------------------------------------------------ //
// Skill progression
// ------------------------------------------------------------------ //
addSkillExperience(category, skillId, amount) {
const skill = this.skills[category]?.[skillId];
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
skill.experience += amount;
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
this.levelUpSkill(category, skillId);
}
this.applySkillEffects();
return true;
}
levelUpSkill(category, skillId) {
const skill = this.skills[category][skillId];
const excess = skill.experience - skill.experienceToNext;
skill.currentLevel++;
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
skill.experience = Math.max(0, excess);
this.applySkillEffects();
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
}
upgradeSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
if (!skill.unlocked) { this.game.showNotification('Skill is locked', 'error', 3000); return false; }
if (skill.currentLevel >= skill.maxLevel){ this.game.showNotification('Skill is at maximum level', 'warning', 3000); return false; }
if (player.stats.skillPoints < 1) { this.game.showNotification('Not enough skill points', 'error', 3000); return false; }
player.stats.skillPoints--;
this.levelUpSkill(category, skillId);
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
return true;
}
unlockSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
if (skill.unlocked) { this.game.showNotification('Skill is already unlocked', 'warning', 3000); return false; }
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
return false;
}
if (player.stats.skillPoints < 2) {
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
return false;
}
player.stats.skillPoints -= 2;
skill.unlocked = true;
skill.currentLevel = 1;
this.applySkillEffects();
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
return true;
}
applySkillEffects() {
const player = this.game.systems.player;
this.resetToBaseStats();
for (const category of Object.values(this.skills)) {
for (const skill of Object.values(category)) {
if (!skill.unlocked || skill.currentLevel <= 0) continue;
for (const [effect, value] of Object.entries(skill.effects || {})) {
const total = value * skill.currentLevel;
switch (effect) {
case 'attack': player.attributes.attack += total; break;
case 'defense': player.attributes.defense += total; break;
case 'speed': player.attributes.speed += total; break;
case 'health':
case 'maxHealth': player.attributes.maxHealth += total; break;
case 'maxEnergy': player.attributes.maxEnergy += total; break;
case 'criticalChance': player.attributes.criticalChance += total; break;
case 'criticalDamage': player.attributes.criticalDamage += total; break;
default:
if (!this.activeBuffs[effect]) this.activeBuffs[effect] = 0;
this.activeBuffs[effect] += total;
}
}
}
}
player.updateUI();
}
resetToBaseStats() {
const player = this.game.systems.player;
const lvl = player.stats.level || 1;
Object.assign(player.attributes, {
attack: 10 + (lvl - 1) * 2,
defense: 5 + (lvl - 1) * 1,
speed: 10,
maxHealth: 100 + (lvl - 1) * 10,
maxEnergy: 100 + (lvl - 1) * 5,
criticalChance: 0.05,
criticalDamage: 1.5
});
this.activeBuffs = {};
}
// ------------------------------------------------------------------ //
// Combat / science / crafting XP helpers
// ------------------------------------------------------------------ //
awardCombatExperience(amount) {
this.addSkillExperience('combat', 'weapons_mastery', amount);
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
}
awardScienceExperience(amount) {
this.addSkillExperience('science', 'energy_manipulation', amount);
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
}
awardCraftingExperience(amount) {
this.addSkillExperience('crafting', 'weapons_crafting', amount);
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
}
// ------------------------------------------------------------------ //
// Queries
// ------------------------------------------------------------------ //
getSkillLevel(category, skillId) {
// Support single-arg form used by CraftingSystem: getSkillLevel('crafting')
if (skillId === undefined) {
let max = 0;
for (const cat of Object.values(this.skills)) {
if (cat[category]) max = Math.max(max, cat[category].currentLevel || 0);
}
return max;
}
return this.skills[category]?.[skillId]?.currentLevel || 0;
}
getSkillExperience(skillId) {
for (const cat of Object.values(this.skills)) {
if (cat[skillId]) return cat[skillId].experience || 0;
}
return 0;
}
getExperienceNeeded(skillId) {
for (const cat of Object.values(this.skills)) {
if (cat[skillId]) return cat[skillId].experienceToNext || 0;
}
return 0;
}
hasSkill(category, skillId, minimumLevel = 1) {
const skill = this.skills[category]?.[skillId];
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
}
getSkillBonus(effect) {
return this.activeBuffs[effect] || 0;
}
// ------------------------------------------------------------------ //
// UI
// ------------------------------------------------------------------ //
updateUI() {
this.updateSkillsGrid();
this.updateSkillPointsDisplay();
}
updateSkillsGrid() {
const grid = document.getElementById('skillsGrid');
if (!grid) return;
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
const skills = this.skills[activeCategory] || {};
if (!this._loaded) {
grid.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading skills from server...</p></div>';
return;
}
grid.innerHTML = '';
Object.entries(skills).forEach(([skillId, skill]) => {
const el = document.createElement('div');
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
const progressPercent = skill.currentLevel > 0
? (skill.experience / skill.experienceToNext) * 100
: 0;
const iconClass = this.game.systems.textureManager
? this.game.systems.textureManager.getIcon(skill.icon)
: (skill.icon || 'fa-question');
el.innerHTML = `
<div class="skill-header">
<div class="skill-icon"><i class="fas ${iconClass}"></i></div>
<div class="skill-info">
<div class="skill-name">${skill.name}</div>
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
</div>
</div>
<div class="skill-description">${skill.description}</div>
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
<div class="skill-progress">
<div class="progress-bar">
<div class="progress-fill" style="width:${progressPercent}%"></div>
</div>
<span>${skill.experience}/${skill.experienceToNext} XP</span>
</div>
` : skill.currentLevel >= skill.maxLevel ? `
<div class="skill-max-level"><span>MAX LEVEL</span></div>
` : ''}
<div class="skill-actions">
${!skill.unlocked ? `
<button class="btn btn-warning" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.unlockSkill('${activeCategory}','${skillId}')">
Unlock (2 Points)
</button>
` : skill.currentLevel < skill.maxLevel ? `
<button class="btn btn-primary" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.upgradeSkill('${activeCategory}','${skillId}')">
Upgrade (1 Point)
</button>
` : `<span class="max-level">MAX LEVEL</span>`}
</div>
${skill.requiredLevel && !skill.unlocked ? `
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
` : ''}
`;
grid.appendChild(el);
});
}
updateSkillPointsDisplay() {
const player = this.game.systems.player;
document.querySelectorAll('.skill-points').forEach(el => {
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
});
}
// ------------------------------------------------------------------ //
// Save / Load
// ------------------------------------------------------------------ //
save() {
return { skills: this.skills, activeBuffs: this.activeBuffs };
}
load(data) {
if (data.skills) {
for (const [category, skills] of Object.entries(data.skills)) {
if (!this.skills[category]) this.skills[category] = {};
for (const [skillId, skillData] of Object.entries(skills)) {
if (this.skills[category][skillId]) {
Object.assign(this.skills[category][skillId], skillData);
} else {
// Store progress even before server definitions arrive
this.skills[category][skillId] = { ...skillData };
}
}
}
}
if (data.activeBuffs) this.activeBuffs = data.activeBuffs;
this.applySkillEffects();
}
reset() {
this.activeBuffs = {};
for (const category of Object.values(this.skills)) {
for (const skill of Object.values(category)) {
skill.currentLevel = 0;
skill.experience = 0;
}
}
}
clear() { this.reset(); }
}

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ class LiveMainMenu {
this.currentUser = null;
this.servers = []; // Renamed from serverList to avoid conflict with DOM element
this.apiBaseUrl = 'https://api.korvarix.com/api'; // API Server URL
this.gameServerUrl = 'https://dev.gameserver.galaxystrike.online'; // Game Server URL for Socket.IO (local dev server)
this.gameServerUrl = 'https://dev.gameserver.galaxystrike.online'; // Game Server URL for Socket.IO
this.localGameServerUrl = 'http://localhost:3002'; // Local Game Server URL
this.isLocalMode = false; // Track if we're in local mode

View File

@ -43,16 +43,19 @@ class UIManager {
const gameInterface = document.getElementById('gameInterface');
const navButtons = document.querySelectorAll('.nav-btn');
if (debugLogger) debugLogger.logStep('DOM Check', {
console.log('[UI MANAGER] DOM Check:', {
gameInterfaceExists: !!gameInterface,
gameInterfaceHidden: gameInterface?.classList.contains('hidden'),
navButtonsFound: navButtons.length,
documentReady: document.readyState
});
if (gameInterface && !gameInterface.classList.contains('hidden') && navButtons.length > 0) {
// Less strict condition - proceed if we have nav buttons, even if interface is still hidden
if (navButtons.length > 0) {
console.log('[UI MANAGER] Navigation buttons found, proceeding with initialization');
this.proceedWithInitialization();
} else {
console.log('[UI MANAGER] Waiting for navigation buttons...');
setTimeout(waitForDOM, 100);
}
};
@ -117,10 +120,6 @@ class UIManager {
});
if (debugLogger) debugLogger.endStep('UIManager.setupNavigation', {
navButtonsSetup: navButtons.length,
skillCatButtonsSetup: skillCatButtons.length,
shopCatButtonsSetup: shopCatButtons.length,
questTabButtonsSetup: questTabButtons.length,
craftingCatButtonsSetup: craftingCatButtons.length
});
}
@ -140,10 +139,11 @@ class UIManager {
if (navButtons.length === 0) {
console.warn('[UIManager] No navigation buttons found!');
if (debugLogger) debugLogger.logStep('No navigation buttons found');
return;
}
navButtons.forEach((btn, index) => {
console.log(`[UIManager] Setting up button ${index}:`, btn.dataset.tab);
console.log(`[UIManager] Setting up button ${index}:`, btn.dataset.tab, btn);
// Check if button already has a listener to prevent duplicates
if (btn.getAttribute('data-has-listener') === 'true') {
console.log(`[UIManager] Button ${btn.dataset.tab} already has listener, skipping`);
@ -151,16 +151,30 @@ class UIManager {
}
btn.addEventListener('click', (e) => {
console.log(`[UIManager] Navigation button clicked: ${btn.dataset.tab}`);
console.log(`[UIManager] Navigation button clicked: ${btn.dataset.tab}`, e);
const tab = btn.dataset.tab;
if (tab) {
this.switchTab(tab);
} else {
console.warn('[UIManager] Button clicked but no tab data found');
}
});
// Mark button as having a listener
// Mark as having listener
btn.setAttribute('data-has-listener', 'true');
console.log(`[UIManager] Button ${btn.dataset.tab} setup complete`);
console.log(`[UIManager] Event listener added to button: ${btn.dataset.tab}`);
});
// Add a global fallback click handler for navigation buttons
document.addEventListener('click', (e) => {
if (e.target.classList.contains('nav-btn') || e.target.closest('.nav-btn')) {
const button = e.target.classList.contains('nav-btn') ? e.target : e.target.closest('.nav-btn');
const tab = button.dataset.tab;
if (tab && window.game && window.game.systems && window.game.systems.ui) {
console.log('[UI MANAGER] Global fallback: Navigation button clicked:', tab);
window.game.systems.ui.switchTab(tab);
}
}
});
// Set up UI update event listener from GameEngine
@ -765,7 +779,7 @@ class UIManager {
// Stop economy system timers
if (this.game.systems.economy) {
this.game.systems.economy.stopShopTimers();
this.game.systems.economy.stopShopRefreshTimer();
}
if (isMultiplayer) {
@ -1037,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 => {
@ -1292,8 +1306,14 @@ class UIManager {
});
try {
// Call economy system update
this.game.systems.economy.updateUI();
// Also call global shop display function for additional debugging
if (typeof updateShopDisplay === 'function') {
updateShopDisplay();
}
if (debugLogger) debugLogger.endStep('UIManager.switchShopCategory', {
success: true,
category: category,
@ -1313,13 +1333,7 @@ class UIManager {
}
}
switchQuestType(type) {
const debugLogger = window.debugLogger;
if (debugLogger) debugLogger.startStep('UIManager.switchQuestType', {
type: type
});
updateQuestTabs(type) {
const questTabButtons = document.querySelectorAll('.quest-tab-btn');
let buttonsUpdated = 0;
@ -1339,7 +1353,13 @@ class UIManager {
});
try {
this.game.systems.questSystem.updateUI();
// REMOVED: Client-side QuestSystem is now server-driven
// this.game.systems.questSystem.updateUI();
// Call the global quest display update function instead
if (typeof updateQuestDisplay === 'function') {
updateQuestDisplay();
}
if (debugLogger) debugLogger.endStep('UIManager.switchQuestType', {
success: true,
@ -1611,20 +1631,20 @@ class UIManager {
// In multiplayer mode, ensure we're using server data
if (window.smartSaveManager?.isMultiplayer) {
console.log('[UI MANAGER] Updating resource display in multiplayer mode - using server data');
// console.log('[UI MANAGER] Updating resource display in multiplayer mode - using server data');
}
const debugLogger = window.debugLogger;
// const debugLogger = window.debugLogger;
try {
// Safety checks - return early if systems aren't available
if (!this.game || !this.game.systems) {
if (debugLogger) debugLogger.log('Game systems not available, skipping resource display update');
// if (debugLogger) debugLogger.log('Game systems not available, skipping resource display update');
return;
}
if (!this.game.systems.player || !this.game.systems.economy) {
if (debugLogger) debugLogger.log('Player or economy system not available, skipping resource display update');
// if (debugLogger) debugLogger.log('Player or economy system not available, skipping resource display update');
return;
}
@ -1633,18 +1653,18 @@ class UIManager {
// Additional safety checks for player properties
if (!player.stats || !player.attributes || !player.ship) {
if (debugLogger) debugLogger.log('Player properties not fully initialized, skipping resource display update');
// if (debugLogger) debugLogger.log('Player properties not fully initialized, skipping resource display update');
return;
}
if (debugLogger) debugLogger.startStep('UIManager.updateResourceDisplay', {
gameSystemsAvailable: !!(this.game && this.game.systems),
playerSystemAvailable: !!(this.game && this.game.systems && this.game.systems.player),
economySystemAvailable: !!(this.game && this.game.systems && this.game.systems.economy),
playerStatsAvailable: !!player.stats,
playerAttributesAvailable: !!player.attributes,
playerShipAvailable: !!player.ship
});
// if (debugLogger) debugLogger.startStep('UIManager.updateResourceDisplay', {
// gameSystemsAvailable: !!(this.game && this.game.systems),
// playerSystemAvailable: !!(this.game && this.game.systems && this.game.systems.player),
// economySystemAvailable: !!(this.game && this.game.systems && this.game.systems.economy),
// playerStatsAvailable: !!player.stats,
// playerAttributesAvailable: !!player.attributes,
// playerShipAvailable: !!player.ship
// });
let elementsUpdated = 0;
let elementsNotFound = 0;
@ -1654,7 +1674,7 @@ class UIManager {
if (playerLevelElement) {
const oldLevel = playerLevelElement.textContent;
const playerLevel = player.stats.level || 1;
console.log('[UI MANAGER] Updating player level:', { oldLevel, newLevel: playerLevel, playerStats: player.stats });
// console.log('[UI MANAGER] Updating player level:', { oldLevel, newLevel: playerLevel, playerStats: player.stats });
playerLevelElement.textContent = `Lv. ${playerLevel}`;
elementsUpdated++;
@ -1665,7 +1685,7 @@ class UIManager {
});
} else {
elementsNotFound++;
if (debugLogger) debugLogger.log('Player level element not found');
// if (debugLogger) debugLogger.log('Player level element not found');
}
// Update ship level with safety checks
@ -1692,7 +1712,9 @@ class UIManager {
if (creditsElement) {
const oldCredits = creditsElement.textContent;
const creditsAmount = economy.credits || 0;
console.log('[UI MANAGER] Updating credits:', { oldCredits, newCredits: creditsAmount, economyCredits: economy.credits, economySystem: !!economy });
if (oldCredits !== this.game.formatNumber(creditsAmount)) {
console.log('[UI MANAGER] Credits updated:', this.game.formatNumber(creditsAmount));
}
creditsElement.textContent = this.game.formatNumber(creditsAmount);
elementsUpdated++;
@ -1712,6 +1734,9 @@ class UIManager {
if (gemsElement) {
const oldGems = gemsElement.textContent;
const gemsAmount = economy.gems || 0;
if (oldGems !== this.game.formatNumber(gemsAmount)) {
console.log('[UI MANAGER] Gems updated:', this.game.formatNumber(gemsAmount));
}
gemsElement.textContent = this.game.formatNumber(gemsAmount);
elementsUpdated++;
@ -1730,10 +1755,13 @@ class UIManager {
const energyElement = document.getElementById('energy');
if (energyElement) {
const oldEnergy = energyElement.textContent;
// Ensure we're using the correct energy values, not credits
const currentEnergy = Math.floor(player.attributes.energy || 0);
const maxEnergy = Math.floor(player.attributes.maxEnergy || 100);
energyElement.textContent = `${currentEnergy}/${maxEnergy}`;
const currentEnergy = player.attributes.currentEnergy || player.attributes.energy || 0;
const maxEnergy = player.attributes.maxEnergy || 100;
const newEnergy = `${currentEnergy}/${maxEnergy}`;
if (oldEnergy !== newEnergy) {
console.log('[UI MANAGER] Energy updated:', newEnergy);
}
energyElement.textContent = newEnergy;
elementsUpdated++;
if (debugLogger) debugLogger.logStep('Energy updated', {
@ -1749,6 +1777,65 @@ class UIManager {
if (debugLogger) debugLogger.log('Energy element not found');
}
// Update play time (keep this here as it's part of the core resource display)
const playTimeElement = document.getElementById('playTime');
if (playTimeElement) {
const playTimeMs = player.stats.playTime || 0;
playTimeElement.textContent = this.formatPlayTime(playTimeMs);
elementsUpdated++;
}
// Update player stats panel
const playerLevelDisplayElement = document.getElementById('playerLevelDisplay');
if (playerLevelDisplayElement) {
playerLevelDisplayElement.textContent = player.stats.level || 1;
elementsUpdated++;
}
const playerXPElement = document.getElementById('playerXP');
if (playerXPElement) {
const currentXP = player.stats.experience || 0;
const xpToNext = player.stats.experienceToNext || 100;
playerXPElement.textContent = `${currentXP} / ${xpToNext}`;
elementsUpdated++;
}
const skillPointsElement = document.getElementById('skillPoints');
if (skillPointsElement) {
skillPointsElement.textContent = player.stats.skillPoints || 0;
elementsUpdated++;
}
const totalXPElement = document.getElementById('totalXP');
if (totalXPElement) {
totalXPElement.textContent = this.game.formatNumber(player.stats.totalXP || 0);
elementsUpdated++;
}
const questsCompletedElement = document.getElementById('questsCompleted');
if (questsCompletedElement) {
// Use player stats from server first, then quest system stats as fallback
const questsCompleted = player.stats.questsCompleted ||
(this.game.systems.questSystem?.stats?.questsCompleted) || 0;
questsCompletedElement.textContent = questsCompleted;
elementsUpdated++;
}
const lastLoginElement = document.getElementById('lastLogin');
if (lastLoginElement) {
const lastLogin = player.stats.lastLogin;
if (lastLogin) {
const loginDate = new Date(lastLogin);
lastLoginElement.textContent = loginDate.toLocaleDateString() + ' ' + loginDate.toLocaleTimeString();
} else {
lastLoginElement.textContent = 'Never';
}
elementsUpdated++;
}
// Update all player stats
this.updatePlayerStats();
if (debugLogger) debugLogger.endStep('UIManager.updateResourceDisplay', {
elementsUpdated: elementsUpdated,
elementsNotFound: elementsNotFound,
@ -1767,33 +1854,132 @@ class UIManager {
}
}
updateUI() {
const debugLogger = window.debugLogger;
formatPlayTime(milliseconds) {
// Format play time showing only the two most relevant units
const totalSeconds = Math.floor(milliseconds / 1000);
if (debugLogger) debugLogger.startStep('UIManager.updateUI', {
currentTab: this.currentTab,
modalOpen: this.modalOpen
});
if (totalSeconds < 60) {
// Less than 1 minute: show seconds only
return `${totalSeconds}s`;
} else if (totalSeconds < 3600) {
// Less than 1 hour: show minutes and seconds
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}m ${seconds}s`;
} else if (totalSeconds < 86400) {
// Less than 1 day: show hours and minutes
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
return `${hours}h ${minutes}m`;
} else {
// 1 day or more: show days and hours
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
return `${days}d ${hours}h`;
}
}
updatePlayerStats() {
// Update just the player stats panel (excluding those handled in updateResourceDisplay)
if (!this.game || !this.game.systems || !this.game.systems.player) {
return;
}
const player = this.game.systems.player;
let elementsUpdated = 0;
// Update player stats panel
const playerLevelDisplayElement = document.getElementById('playerLevelDisplay');
if (playerLevelDisplayElement) {
playerLevelDisplayElement.textContent = player.stats.level || 1;
elementsUpdated++;
}
const playerXPElement = document.getElementById('playerXP');
if (playerXPElement) {
const currentXP = player.stats.experience || 0;
const xpToNext = player.stats.experienceToNext || 100;
playerXPElement.textContent = `${currentXP} / ${xpToNext}`;
elementsUpdated++;
}
const skillPointsElement = document.getElementById('skillPoints');
if (skillPointsElement) {
skillPointsElement.textContent = player.stats.skillPoints || 0;
elementsUpdated++;
}
const totalXPElement = document.getElementById('totalXP');
if (totalXPElement) {
totalXPElement.textContent = this.game.formatNumber(player.stats.totalXP || 0);
elementsUpdated++;
}
const questsCompletedElement = document.getElementById('questsCompleted');
if (questsCompletedElement) {
// Use player stats from server first, then quest system stats as fallback
const questsCompleted = player.stats.questsCompleted ||
(this.game.systems.questSystem?.stats?.questsCompleted) || 0;
questsCompletedElement.textContent = questsCompleted;
elementsUpdated++;
}
const lastLoginElement = document.getElementById('lastLogin');
if (lastLoginElement) {
const lastLogin = player.stats.lastLogin;
if (lastLogin) {
const loginDate = new Date(lastLogin);
lastLoginElement.textContent = loginDate.toLocaleDateString() + ' ' + loginDate.toLocaleTimeString();
} else {
lastLoginElement.textContent = 'Never';
}
elementsUpdated++;
}
// Update stats moved from Idle Progress card
const totalKillsElement = document.getElementById('totalKills');
if (totalKillsElement) {
totalKillsElement.textContent = this.game.formatNumber(player.stats.totalKills || 0);
elementsUpdated++;
}
const dungeonsClearedElement = document.getElementById('dungeonsCleared');
if (dungeonsClearedElement) {
dungeonsClearedElement.textContent = player.stats.dungeonsCleared || 0;
elementsUpdated++;
}
// DISABLED: Reduce console spam for quest debugging
// console.log(`[UI MANAGER] Player stats updated: ${elementsUpdated} elements`);
}
updateUI() {
// const debugLogger = window.debugLogger;
// if (debugLogger) debugLogger.startStep('UIManager.updateUI', {
// currentTab: this.currentTab,
// modalOpen: this.modalOpen
// });
// Update resource display only if in multiplayer mode or game is actively running
if (this.shouldUpdateUI()) {
if (debugLogger) debugLogger.logStep('Updating resource display');
// if (debugLogger) debugLogger.logStep('Updating resource display');
this.updateResourceDisplay();
}
// Update tab content only if in multiplayer mode or game is actively running
if (this.shouldUpdateUI()) {
if (debugLogger) debugLogger.logStep('Updating tab content', {
currentTab: this.currentTab
});
// if (debugLogger) debugLogger.logStep('Updating tab content', {
// currentTab: this.currentTab
// });
this.updateTabContent(this.currentTab);
}
if (debugLogger) debugLogger.endStep('UIManager.updateUI', {
resourceDisplayUpdated: true,
tabContentUpdated: true,
currentTab: this.currentTab
});
// if (debugLogger) debugLogger.endStep('UIManager.updateUI', {
// resourceDisplayUpdated: true,
// tabContentUpdated: true,
// currentTab: this.currentTab
// });
}
// Menus
@ -1945,8 +2131,8 @@ class UIManager {
} else {
}
// Reset economy
if (this.game.systems.economy) {
// Reset economy - only reset if not in multiplayer mode
if (this.game.systems.economy && !window.smartSaveManager?.isMultiplayer) {
this.game.systems.economy.credits = 1000;
this.game.systems.economy.gems = 10;
}
@ -2065,8 +2251,8 @@ class UIManager {
};
}
// Clear economy data
if (this.game.systems.economy) {
// Clear economy data - only reset if not in multiplayer mode
if (this.game.systems.economy && !window.smartSaveManager?.isMultiplayer) {
this.game.systems.economy.credits = 1000;
this.game.systems.economy.gems = 10;
}
@ -2451,6 +2637,94 @@ class UIManager {
</button>
`;
}
// Update dungeon UI when player enters/exits dungeons
updateDungeonUI(dungeonData) {
console.log('[UI MANAGER] Updating dungeon UI:', dungeonData);
console.log('[UI MANAGER] Dungeon state check:', {
isExploring: dungeonData.isExploring,
hasCurrentDungeon: !!dungeonData.currentDungeon,
hasCurrentRoom: !!dungeonData.currentRoom,
progress: dungeonData.progress,
dungeonViewElement: !!document.getElementById('dungeonView')
});
// Check if dungeon is completed (progress = -1)
if (dungeonData.progress === -1 || !dungeonData.isExploring) {
// Player is not in dungeon or dungeon is completed - show normal dungeon list
console.log('[UI MANAGER] Player not exploring or dungeon completed, showing dungeon list');
this.switchTab('dungeons');
return;
}
// Player is in dungeon - show exploration interface
const dungeonView = document.getElementById('dungeonView');
if (dungeonView) {
console.log('[UI MANAGER] Found dungeonView element, creating exploration UI');
const dungeon = dungeonData.currentDungeon;
const progress = dungeonData.progress || 0;
const currentRoom = dungeonData.currentRoom;
let content = `
<div class="dungeon-exploration">
<div class="dungeon-header">
<h3>${dungeon.dungeonId} Dungeon</h3>
<div class="dungeon-progress">
<span>Progress: Room ${progress + 1}</span>
</div>
<button class="btn btn-danger" onclick="if(window.game && window.game.systems && window.game.systems.dungeonSystem) window.game.systems.dungeonSystem.exitDungeon()">
Exit Dungeon
</button>
</div>
`;
if (currentRoom) {
content += `
<div class="current-room">
<h4>${currentRoom.name}</h4>
<p>${currentRoom.description}</p>
<div class="room-enemies">
<h5>Enemies:</h5>
${currentRoom.enemies && currentRoom.enemies.length > 0 ?
currentRoom.enemies.map((enemy, index) => {
console.log(`[UI MANAGER] Rendering enemy ${index}:`, enemy);
return `
<div class="enemy-info">
<strong>${enemy.name || 'Unknown Enemy'}</strong>
<div>HP: ${enemy.health || '??'} | ATK: ${enemy.attack || '??'} | DEF: ${enemy.defense || '??'}</div>
</div>
`}).join('') :
'<p>No enemies in this room</p>'
}
</div>
${currentRoom.enemies && currentRoom.enemies.length > 0 ?
`<button class="btn btn-primary" onclick="if(window.game && window.game.systems && window.game.systems.dungeonSystem) window.game.systems.dungeonSystem.processEncounter()">
Start Combat
</button>` :
`<button class="btn btn-success" onclick="if(window.game && window.game.systems && window.game.systems.dungeonSystem) window.game.systems.dungeonSystem.moveToNextRoom()">
Continue to Next Room
</button>`
}
</div>
`;
} else {
content += `
<div class="room-loading">
<p>Loading next room...</p>
<button class="btn btn-primary" onclick="if(window.game && window.game.systems && window.game.systems.dungeonSystem) window.game.systems.dungeonSystem.processEncounter()">
Start First Room
</button>
</div>
`;
}
content += '</div>';
dungeonView.innerHTML = content;
console.log('[UI MANAGER] Dungeon exploration UI applied successfully');
} else {
console.error('[UI MANAGER] dungeonView element not found!');
}
}
}
// Export UIManager to global scope

View File

@ -0,0 +1 @@
{"_meta":{"lang":"de","name":"de","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1,58 @@
{
"_meta": { "lang": "en", "name": "English", "version": "1.0" },
"nav": {
"dashboard": "Dashboard",
"dungeons": "Dungeons",
"skills": "Skills",
"base": "Base",
"quests": "Quests",
"inventory": "Inventory",
"crafting": "Crafting",
"shop": "Shop",
"fleet": "Fleet",
"galaxy": "Galaxy",
"research": "Research",
"leaderboard": "Ranks",
"missions": "Missions",
"alliance": "Alliance",
"market": "Market",
"social": "Social"
},
"base": {
"buildings": "🏗 Buildings",
"shipyard": "🚀 Shipyard",
"starbase": "🌌 Starbase",
"overview": "📊 Overview"
},
"resources": {
"metal": "Metal",
"gas": "Gas",
"crystal": "Crystal",
"energyCells": "Energy Cells",
"darkMatter": "Dark Matter",
"credits": "Credits",
"gems": "Gems"
},
"actions": {
"build": "Build",
"upgrade": "Upgrade",
"cancel": "Cancel",
"collect": "Collect",
"launch": "Launch",
"search": "Search",
"join": "Join",
"leave": "Leave",
"deposit": "Deposit",
"withdraw": "Withdraw",
"buy": "Buy",
"sell": "Sell"
},
"status": {
"online": "Online",
"offline": "Offline",
"inProgress": "In Progress",
"completed": "Completed",
"locked": "Locked",
"maxLevel": "Max Level"
}
}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"es","name":"es","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"fr","name":"fr","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"ja","name":"ja","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"ko","name":"ko","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"pt","name":"pt","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1 @@
{"_meta":{"lang":"zh","name":"zh","status":"placeholder - community translation needed"}}

View File

@ -0,0 +1,216 @@
/*
Galaxy Strike Online Component Styles (mobile-first)
*/
/* ── Buttons ─────────────────────────────────────────────────────────── */
.btn {
padding:.6rem 1.1rem;border:none;border-radius:8px;
font-family:'Space Mono',monospace;font-weight:700;cursor:pointer;
transition:all .25s;text-transform:uppercase;letter-spacing:1px;
position:relative;overflow:hidden;font-size:.82rem;display:inline-flex;align-items:center;gap:.35rem;
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);transition:left .5s}
.btn:hover::before{left:100%}
.btn-primary{background:var(--gradient-primary);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,212,255,.3)}
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 5px 18px rgba(0,212,255,.4)}
.btn-secondary{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color)}
.btn-secondary:hover{border-color:var(--primary-color);background:var(--hover-bg)}
.btn-success{background:linear-gradient(135deg,#00ff88,#00cc66);color:var(--bg-primary);box-shadow:0 3px 12px rgba(0,255,136,.3)}
.btn-warning{background:linear-gradient(135deg,#ffaa00,#ff8800);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,170,0,.3)}
.btn-danger,.btn-error{background:linear-gradient(135deg,#ff3366,#ff0033);color:var(--bg-primary);box-shadow:0 3px 12px rgba(255,51,102,.3)}
.btn-info{background:linear-gradient(135deg,#4fc3f7,#0288d1);color:var(--bg-primary)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none!important}
/* Larger tap targets on touch */
@media(pointer:coarse){.btn{min-height:42px;padding:.6rem 1.2rem}}
/* ── Health / Progress bars ──────────────────────────────────────────── */
.health-bar{margin:.75rem 0;padding:.6rem;border-radius:8px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08)}
.health-label{font-size:.82rem;font-weight:600;margin-bottom:.4rem;text-transform:uppercase;letter-spacing:1px}
.ship-health .health-label{color:#4a9eff}
.player-health .health-label{color:#4ade80}
.ship-health-fill{background:linear-gradient(90deg,#4a9eff,#00d4ff);border-radius:4px;transition:width .3s}
.player-health-fill{background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:4px;transition:width .3s}
.health-bar span{display:block;text-align:center;margin-top:.4rem;font-size:.8rem;color:rgba(255,255,255,.75)}
.progress-bar{width:100%;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden;position:relative}
.progress-fill{height:100%;background:var(--gradient-primary);border-radius:4px;transition:width .3s;position:relative}
.progress-fill::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:progressShine 2s infinite}
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
/* ── Ship stats ──────────────────────────────────────────────────────── */
.current-ship-stats{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:.75rem;margin-bottom:.75rem}
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:.4rem 0;border-bottom:1px solid rgba(255,255,255,.05)}
.stat-row:last-child{border-bottom:none}
.stat-row .stat-label{color:#4a9eff;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:.8rem}
.stat-row .stat-value{color:#fff;font-weight:700;font-size:.85rem}
/* ── Modals ──────────────────────────────────────────────────────────── */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:flex-end;justify-content:center;z-index:1000;backdrop-filter:blur(4px)}
.modal{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px 16px 0 0;width:100%;max-height:90dvh;overflow-y:auto;box-shadow:0 -10px 40px rgba(0,0,0,.5);animation:modalSlideUp .3s ease}
@keyframes modalSlideUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
.modal-header{padding:1rem 1.25rem;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center}
.modal-header h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;font-size:.95rem}
.modal-close{background:transparent;border:none;color:var(--text-secondary);font-size:1.4rem;cursor:pointer;padding:.4rem;transition:all .25s;border-radius:6px;-webkit-tap-highlight-color:transparent}
.modal-close:hover{color:var(--error-color);transform:rotate(90deg)}
.modal-body{padding:1rem 1.25rem}
/* Alert modal */
.alert-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
.alert-modal .alert-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
.alert-modal .modal-footer{padding:0 1.25rem 1.25rem;text-align:center}
.alert-modal .btn-alert{background:var(--gradient-primary);color:#fff;border:none;padding:.65rem 1.75rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
.alert-modal.success .modal-header h3{color:var(--success-color)} .alert-modal.success .btn-alert{background:var(--success-color)}
.alert-modal.error .modal-header h3{color:var(--error-color)} .alert-modal.error .btn-alert{background:var(--error-color)}
.alert-modal.warning .modal-header h3{color:var(--warning-color)} .alert-modal.warning .btn-alert{background:var(--warning-color)}
/* Confirmation modal */
.confirmation-modal .modal-body{text-align:center;padding:1.5rem 1.25rem}
.confirmation-modal .confirm-message{color:var(--text-primary);font-size:.95rem;line-height:1.5;margin-bottom:1.25rem;white-space:pre-line}
.confirmation-modal .modal-footer{padding:0 1.25rem 1.25rem;display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
.confirmation-modal .btn-confirm{background:var(--error-color);color:#fff;border:none;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
.confirmation-modal .btn-cancel{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);padding:.65rem 1.5rem;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s;text-transform:uppercase;letter-spacing:1px;min-height:42px}
.confirmation-modal .btn-cancel:hover{background:var(--hover-bg);border-color:var(--primary-color)}
/* Settings */
.settings-menu{max-width:600px;margin:0 auto}
.settings-section{margin-bottom:1.5rem;padding:1.1rem;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)}
.settings-section h3{color:var(--primary-color);font-family:'Orbitron',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:.75rem;font-size:1rem}
.settings-section h4{color:var(--text-primary);font-family:'Orbitron',sans-serif;font-weight:600;margin-bottom:.4rem;font-size:.9rem}
.settings-section p{color:var(--text-secondary);margin-bottom:.75rem;line-height:1.5;font-size:.88rem}
.setting-group{margin-bottom:1.25rem}
.setting-group label{display:block;color:var(--text-primary);font-weight:600;margin-bottom:.4rem;font-size:.88rem}
.setting-select{width:100%;padding:.65rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;color:var(--text-primary);font-size:.88rem;cursor:pointer;transition:all .3s;-webkit-appearance:none;appearance:none}
.setting-select:hover{border-color:var(--primary-color)}
.setting-select:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(74,158,255,.2)}
.setting-actions{display:flex;gap:.75rem;justify-content:flex-end;margin-top:.75rem;flex-wrap:wrap}
.reset-options{display:flex;flex-direction:column;gap:.75rem}
.reset-option{padding:.85rem;background:var(--bg-secondary);border-radius:8px;border:1px solid var(--border-color)}
.reset-option h4{color:var(--warning-color);margin-bottom:.4rem}
.reset-option ul{margin:.4rem 0;padding-left:1.25rem;color:var(--text-secondary);font-size:.82rem}
.reset-option li{margin-bottom:.2rem}
/* ── Tooltips (desktop only — use title attr on mobile) ──────────────── */
@media(min-width:1024px){
.tooltip{position:relative;cursor:help}
.tooltip::before{content:attr(data-tooltip);position:absolute;bottom:125%;left:50%;transform:translateX(-50%);background:var(--bg-tertiary);color:var(--text-primary);padding:.4rem .85rem;border-radius:6px;border:1px solid var(--border-color);font-size:.78rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .25s;z-index:1000}
.tooltip::after{content:'';position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--border-color);opacity:0;pointer-events:none;transition:opacity .25s}
.tooltip:hover::before,.tooltip:hover::after{opacity:1}
}
/* ── Notifications ───────────────────────────────────────────────────── */
.notification{
position:fixed;top:12px;right:12px;left:12px;
padding:.85rem 1.1rem;border-radius:10px;border:1px solid var(--border-color);
background:var(--bg-secondary);color:var(--text-primary);
box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:2000;
animation:notifSlideIn .3s ease;font-size:.88rem;
max-width:420px;margin:0 auto;
}
@keyframes notifSlideIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}
.notification.success{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.12),rgba(0,204,102,.08))}
.notification.warning{border-color:var(--warning-color);background:linear-gradient(135deg,rgba(255,170,0,.12),rgba(255,136,0,.08))}
.notification.error {border-color:var(--error-color); background:linear-gradient(135deg,rgba(255,51,102,.12),rgba(255,0,51,.08))}
.notification.info {border-color:var(--primary-color);background:linear-gradient(135deg,rgba(0,212,255,.12),rgba(0,153,204,.08))}
@media(min-width:640px){
.notification{left:auto;right:16px;top:16px;width:300px;margin:0}
@keyframes notifSlideIn{from{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}
}
/* ── Inventory slots & item cards ────────────────────────────────────── */
.inventory-slot.starbase-bonus-slot::before{content:'+';position:absolute;top:2px;right:2px;background:var(--primary-color);color:#fff;width:12px;height:12px;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center;font-weight:bold}
.item-card{width:100%;height:100%;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.4rem;cursor:pointer;transition:all .25s;position:relative;overflow:hidden;display:flex;flex-direction:column;align-items:center;justify-content:center}
.item-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--gradient-primary);transform:scaleX(0);transition:transform .3s}
.item-card:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
.item-card:hover::before{transform:scaleX(1)}
.item-card.rare{border-color:#0088ff;box-shadow:0 0 8px rgba(0,136,255,.25)}
.item-card.epic{border-color:#8833ff;box-shadow:0 0 10px rgba(136,51,255,.25)}
.item-card.legendary{border-color:#ff8800;box-shadow:0 0 14px rgba(255,136,0,.25)}
.item-icon{flex:1;display:flex;align-items:center;justify-content:center;margin-bottom:.35rem}
.item-icon img,.item-icon i{width:50%;height:50%;min-width:50px;min-height:50px;max-width:120px;max-height:120px;object-fit:contain}
.item-info{text-align:center;font-size:clamp(.58rem,1.4vw,.82rem);line-height:1.1}
.item-name{font-weight:600;margin-bottom:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
.item-rarity{font-size:clamp(.52rem,1.1vw,.7rem);opacity:.8}
.item-quantity{position:absolute;top:3px;right:3px;background:var(--primary-color);color:#fff;font-size:clamp(.5rem,1.1vw,.68rem);font-weight:bold;padding:2px 5px;border-radius:5px;min-width:18px;text-align:center}
/* ── Skill items ─────────────────────────────────────────────────────── */
.skill-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;cursor:pointer;transition:all .25s}
.skill-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
.skill-item.locked{opacity:.5;cursor:not-allowed}
.skill-item.locked:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
.skill-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem}
.skill-name{font-weight:700;color:var(--text-primary);font-size:.9rem}
.skill-level{color:var(--primary-color);font-weight:700;font-size:.9rem}
.skill-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
.skill-progress{margin-top:.4rem}
/* ── Quest items ─────────────────────────────────────────────────────── */
.quest-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.9rem;cursor:pointer;transition:all .25s}
.quest-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
.quest-item.completed{border-color:var(--success-color);background:linear-gradient(135deg,rgba(0,255,136,.08),rgba(0,204,102,.06))}
.quest-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;gap:.3rem}
.quest-title{font-weight:700;color:var(--text-primary);font-size:.9rem}
.quest-reward{color:var(--warning-color);font-weight:700;font-size:.9rem}
.quest-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
.quest-progress{margin-top:.4rem}
/* ── Dungeon items ───────────────────────────────────────────────────── */
.dungeon-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
.dungeon-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
.dungeon-item.selected{border-color:var(--primary-color);background:rgba(0,212,255,.15)}
.dungeon-name{font-weight:700;color:var(--text-primary);margin-bottom:.2rem;font-size:.9rem}
.dungeon-difficulty{font-size:.78rem;margin-bottom:.2rem}
.dungeon-difficulty.easy{color:var(--success-color)} .dungeon-difficulty.medium{color:var(--warning-color)} .dungeon-difficulty.hard{color:var(--error-color)}
.dungeon-rewards{font-size:.78rem;color:var(--text-secondary)}
/* Collapsible dungeon sections */
.dungeon-section{margin-bottom:.75rem;border:1px solid var(--border-color);border-radius:8px;overflow:hidden;background:var(--bg-secondary)}
.difficulty-header.collapsible{padding:.8rem 1rem;cursor:pointer;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;margin:0;border:none;border-radius:0}
.difficulty-header.collapsible:hover{background:var(--bg-tertiary)}
.header-content{display:flex;align-items:center;gap:.4rem}
.header-content i{font-size:1.1rem}
.header-content span{font-weight:600;font-size:1rem}
.dungeon-count{background:rgba(255,255,255,.18);padding:.15rem .45rem;border-radius:10px;font-size:.75rem;font-weight:600;min-width:1.8rem;text-align:center}
.collapse-indicator{transition:transform .3s;background:rgba(255,255,255,.08);width:1.8rem;height:1.8rem;border-radius:50%;display:flex;align-items:center;justify-content:center}
.collapse-indicator i{font-size:.82rem;color:rgba(255,255,255,.85)}
.dungeon-content{padding:0 .75rem .75rem;max-height:2000px;overflow:hidden;transition:all .3s;opacity:1}
.dungeon-content.collapsed{max-height:0;padding:0 .75rem;opacity:0}
.difficulty-header.tutorial{background:linear-gradient(135deg,#1a56db,#2c5aa0);color:#fff}
.difficulty-header.easy{background:linear-gradient(135deg,var(--success-color),#27ae60);color:#fff}
.difficulty-header.medium{background:linear-gradient(135deg,var(--warning-color),#f39c12);color:#fff}
.difficulty-header.hard{background:linear-gradient(135deg,var(--error-color),#e74c3c);color:#fff}
.difficulty-header.extreme{background:linear-gradient(135deg,#8e44ad,#9b59b6);color:#fff}
.dungeon-content .dungeon-item{margin-bottom:.6rem;border-left:4px solid transparent}
.dungeon-content .dungeon-item:hover{border-left-color:var(--primary-color);transform:translateX(3px)}
.dungeon-content .dungeon-item:last-child{margin-bottom:0}
/* ── Shop items (legacy card style) ─────────────────────────────────── */
.shop-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .25s}
.shop-item:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.18)}
.shop-item.purchased{opacity:.5;cursor:not-allowed}
.shop-item.purchased:hover{transform:none;box-shadow:none;border-color:var(--border-color)}
.shop-name{font-weight:700;color:var(--text-primary);margin-bottom:.4rem;font-size:.9rem}
.shop-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.75rem}
.shop-price{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.3rem}
.shop-cost{color:var(--warning-color);font-weight:700;font-size:.9rem}
/* ── Loading states ──────────────────────────────────────────────────── */
.loading{position:relative;overflow:hidden}
.loading::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,212,255,.18),transparent);animation:loadingShine 1.5s infinite}
@keyframes loadingShine{0%{left:-100%}100%{left:100%}}
/* ── Misc animations ─────────────────────────────────────────────────── */
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
@keyframes rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes glow{0%,100%{box-shadow:0 0 5px rgba(0,212,255,.5)}50%{box-shadow:0 0 18px rgba(0,212,255,.8)}}
.pulse{animation:pulse 2s infinite}
.bounce{animation:bounce 2s infinite}
.rotate{animation:rotate 2s linear infinite}
.glow{animation:glow 2s infinite}
/* ── Desktop modal — centred (override bottom-sheet) ────────────────── */
@media(min-width:640px){
.modal-overlay{align-items:center}
.modal{border-radius:14px;max-width:580px;width:90%;max-height:80dvh}
@keyframes modalSlideUp{from{opacity:0;transform:translateY(-30px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}
}

View File

@ -1533,6 +1533,134 @@ body.fullscreen .shop-container {
color: var(--text-secondary);
}
/* Collapsible Dungeon Sections */
.dungeon-section {
margin-bottom: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
background: var(--bg-secondary);
}
.difficulty-header.collapsible {
padding: 1rem;
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.3s ease;
margin: 0;
border: none;
border-radius: 0;
}
.difficulty-header.collapsible:hover {
background: var(--bg-tertiary);
}
.header-content {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-content i {
font-size: 1.2rem;
}
.header-content span {
font-weight: 600;
font-size: 1.1rem;
}
.dungeon-count {
background: rgba(255, 255, 255, 0.2);
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
min-width: 2rem;
text-align: center;
}
.collapse-indicator {
transition: transform 0.3s ease;
background: rgba(255, 255, 255, 0.1);
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.collapse-indicator i {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.9);
}
.difficulty-header.collapsible:hover .collapse-indicator {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.dungeon-content {
padding: 0 1rem 1rem;
max-height: 2000px;
overflow: hidden;
transition: all 0.3s ease;
opacity: 1;
}
.dungeon-content.collapsed {
max-height: 0;
padding: 0 1rem;
opacity: 0;
}
/* Difficulty-specific colors for collapsible headers */
.difficulty-header.tutorial {
background: linear-gradient(135deg, var(--info-color), #2c5aa0);
color: white;
}
.difficulty-header.easy {
background: linear-gradient(135deg, var(--success-color), #27ae60);
color: white;
}
.difficulty-header.medium {
background: linear-gradient(135deg, var(--warning-color), #f39c12);
color: white;
}
.difficulty-header.hard {
background: linear-gradient(135deg, var(--error-color), #e74c3c);
color: white;
}
.difficulty-header.extreme {
background: linear-gradient(135deg, #8e44ad, #9b59b6);
color: white;
}
/* Enhanced Dungeon Items in Collapsible Sections */
.dungeon-content .dungeon-item {
margin-bottom: 0.75rem;
border-left: 4px solid transparent;
transition: all 0.3s ease;
}
.dungeon-content .dungeon-item:hover {
border-left-color: var(--primary-color);
transform: translateX(4px);
}
.dungeon-content .dungeon-item:last-child {
margin-bottom: 0;
}
/* Shop Items */
.shop-item {
background: var(--bg-tertiary);

View File

@ -0,0 +1,606 @@
/*
Galaxy Strike Online Main Styles
Mobile-first. Scales up to tablet (640px) and desktop (1024px).
*/
/* ── Reset ───────────────────────────────────────────────────────────── */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary-color: #00d4ff;
--secondary-color: #ff6b35;
--accent-color: #ff00ff;
--bg-primary: #0a0e1a;
--bg-secondary: #151923;
--bg-tertiary: #1e2433;
--text-primary: #ffffff;
--text-secondary: #b8c5d6;
--text-muted: #6b7c93;
--border-color: #2a3241;
--success-color: #00ff88;
--warning-color: #ffaa00;
--error-color: #ff3366;
--card-bg: rgba(30,36,51,0.8);
--hover-bg: rgba(0,212,255,0.1);
--gradient-primary: linear-gradient(135deg,#00d4ff,#0099cc);
--gradient-secondary: linear-gradient(135deg,#ff6b35,#ff4500);
/* Layout tokens */
--header-h: 52px;
--nav-h: 56px;
--pg: 0.75rem;
--r: 10px;
}
html { font-size: 14px; -webkit-text-size-adjust: 100%; }
body {
font-family: 'Space Mono', monospace;
background: var(--bg-primary); color: var(--text-primary);
overflow: hidden; height: 100dvh;
background-image:
radial-gradient(circle at 20% 50%,rgba(0,212,255,.08) 0%,transparent 50%),
radial-gradient(circle at 80% 80%,rgba(255,107,53,.08) 0%,transparent 50%),
radial-gradient(circle at 40% 20%,rgba(255,0,255,.04) 0%,transparent 50%);
}
h1,h2,h3,.logo,.section-title,.menu-title { font-family:'Orbitron',sans-serif; }
.hidden { display:none!important; }
.text-center{text-align:center} .w-full{width:100%}
.flex{display:flex} .flex-column{flex-direction:column}
.flex-center{align-items:center;justify-content:center}
.flex-between{justify-content:space-between} .flex-wrap{flex-wrap:wrap}
.mt-1{margin-top:.5rem} .mt-2{margin-top:1rem}
.mb-1{margin-bottom:.5rem} .mb-2{margin-bottom:1rem}
.p-1{padding:.5rem} .p-2{padding:1rem}
/* ── Scrollbars ──────────────────────────────────────────────────────── */
::-webkit-scrollbar{width:5px;height:5px}
::-webkit-scrollbar-track{background:var(--bg-tertiary);border-radius:3px}
::-webkit-scrollbar-thumb{background:rgba(0,212,255,.35);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:rgba(0,212,255,.6)}
/*
LOADING SCREEN
*/
.loading-screen {
position:fixed;inset:0;background:var(--bg-primary);
display:flex;align-items:center;justify-content:center;
z-index:9999;transition:opacity .5s ease;
}
.loading-content{text-align:center;width:90%;max-width:340px;padding:2rem}
.game-title {
font-family:'Orbitron',sans-serif;
font-size:clamp(1.6rem,8vw,3rem);font-weight:900;
background:var(--gradient-primary);
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
margin-bottom:2rem;text-transform:uppercase;letter-spacing:3px;
}
.loading-bar{width:100%;height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden;margin-bottom:1rem}
.loading-progress{height:100%;background:var(--gradient-primary);width:0%;transition:width .3s ease;animation:loading-pulse 2s infinite}
@keyframes loading-pulse{0%,100%{opacity:1}50%{opacity:.7}}
.loading-text{color:var(--text-secondary);font-size:.8rem}
.loading-indicator {
position:fixed;top:0;left:0;width:100%;height:3px;
background:linear-gradient(90deg,var(--primary-color) 0%,rgba(0,212,255,.3) 50%,var(--primary-color) 100%);
background-size:200% 100%;animation:loading-gradient 2s ease-in-out infinite;
z-index:10000;transition:opacity .3s ease;
}
.loading-indicator.hidden{opacity:0;pointer-events:none}
.loading-indicator.complete{background:linear-gradient(90deg,#4CAF50,#45a049);animation:none}
.loading-indicator.error{background:linear-gradient(90deg,#f44336,#d32f2f);animation:none}
@keyframes loading-gradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
.loading-status {
position:fixed;top:8px;left:50%;transform:translateX(-50%);
background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;
padding:6px 14px;color:var(--text-primary);font-size:.82rem;z-index:10001;
transition:all .3s ease;box-shadow:0 4px 12px rgba(0,0,0,.15);
}
.loading-status.error{background:linear-gradient(90deg,#f44336,#d32f2f);color:#fff;border-color:#d32f2f}
.loading-status.hidden{opacity:0;transform:translateX(-50%) translateY(-20px)}
/*
DESKTOP ELECTRON TITLE BAR
*/
.title-bar {
position:fixed;top:0;left:0;right:0;height:32px;
background:var(--bg-primary);border-bottom:1px solid var(--border-color);
display:none;justify-content:space-between;align-items:center;padding:0 8px;
z-index:10000;-webkit-app-region:drag;user-select:none;
}
body.electron-app .title-bar{display:flex}
body.electron-app #app{margin-top:32px}
.title-bar-title{font-size:13px;font-weight:600;color:var(--text-primary);font-family:'Orbitron',monospace}
.title-bar-right{display:flex;gap:4px}
.title-bar-btn{
width:24px;height:24px;border:none;background:transparent;
color:var(--text-primary);border-radius:4px;cursor:pointer;
display:flex;align-items:center;justify-content:center;
font-size:12px;-webkit-app-region:no-drag;transition:background .2s;
}
.title-bar-btn:hover{background:var(--bg-secondary)}
.title-bar-btn.close-btn:hover{background:#e74c3c;color:#fff}
body.fullscreen .title-bar{display:none}
body.fullscreen #app{margin-top:0}
/*
MAIN MENU
*/
.main-menu {
position:fixed;inset:0;background:var(--bg-primary);
display:flex;align-items:center;justify-content:center;
overflow-y:auto;padding:1rem 0;
background-image:radial-gradient(circle at 20% 50%,rgba(0,212,255,.1) 0%,transparent 50%),
radial-gradient(circle at 80% 80%,rgba(255,107,53,.1) 0%,transparent 50%);
}
.menu-container {
width:95%;max-width:800px;background:var(--card-bg);
border-radius:16px;border:1px solid var(--border-color);
box-shadow:0 20px 60px rgba(0,0,0,.5);backdrop-filter:blur(10px);overflow:hidden;margin:auto;
}
.menu-header{text-align:center;padding:clamp(1.5rem,5vw,2.5rem) 1rem 1.2rem;background:var(--gradient-primary)}
.menu-title{font-size:clamp(1.4rem,6vw,3rem);font-weight:900;color:var(--text-primary);text-transform:uppercase;letter-spacing:3px;margin-bottom:.5rem;text-shadow:0 0 20px rgba(0,212,255,.5)}
.menu-subtitle{font-size:clamp(.85rem,2.5vw,1.2rem);color:var(--text-secondary)}
.menu-content{padding:clamp(1rem,4vw,2rem)}
.menu-section{animation:fadeInUp .5s ease-out}
.section-title{font-size:clamp(1.1rem,4vw,1.8rem);color:var(--primary-color);text-align:center;margin-bottom:1.5rem;text-transform:uppercase;letter-spacing:2px}
.login-options{display:flex;flex-direction:column;gap:1rem;margin-bottom:1rem}
.btn-large{
padding:clamp(.9rem,3vw,1.25rem) clamp(1rem,4vw,2.5rem);
font-size:clamp(.9rem,2.5vw,1.2rem);font-weight:600;border-radius:10px;
transition:all .3s;width:100%;display:flex;align-items:center;justify-content:center;gap:.5rem;
}
.login-notice{text-align:center;padding:.75rem;background:rgba(0,212,255,.1);border:1px solid var(--primary-color);border-radius:8px;color:var(--text-secondary);font-size:.85rem}
.login-notice i{color:var(--primary-color);margin-right:.4rem}
/* Server browser */
.server-controls{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;gap:.75rem;flex-wrap:wrap}
.server-filters{display:flex;gap:.5rem;flex-wrap:wrap}
.filter-select{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.5rem .75rem;color:var(--text-primary);font-family:'Space Mono',monospace;font-size:.85rem;cursor:pointer}
.filter-select:focus{outline:none;border-color:var(--primary-color)}
.server-list{max-height:min(55vh,360px);overflow-y:auto;border:1px solid var(--border-color);border-radius:10px;background:var(--bg-tertiary);margin-bottom:1rem}
.server-item{display:flex;justify-content:space-between;align-items:center;padding:.85rem 1rem;border-bottom:1px solid var(--border-color);cursor:pointer;transition:background .2s,border-left .2s}
.server-item:last-child{border-bottom:none}
.server-item:hover{background:var(--hover-bg);border-left:4px solid var(--primary-color)}
.server-name{font-size:1rem;font-weight:600;margin-bottom:3px}
.server-details{font-size:.8rem;color:var(--text-secondary);display:flex;gap:.75rem;flex-wrap:wrap}
.server-actions-right{display:flex;align-items:center;gap:.5rem;flex-shrink:0}
.server-player-count,.server-region{background:var(--bg-secondary);padding:3px 8px;border-radius:4px;font-size:.75rem;color:var(--text-secondary);border:1px solid var(--border-color)}
.server-type{padding:3px 8px;border-radius:4px;font-size:.75rem;font-weight:600;text-transform:uppercase}
.server-type.public{background:rgba(0,255,136,.15);color:var(--success-color);border:1px solid var(--success-color)}
.server-type.private{background:rgba(255,170,0,.15);color:var(--warning-color);border:1px solid var(--warning-color)}
.server-loading,.server-empty{text-align:center;padding:3rem 1rem;color:var(--text-muted)}
.server-loading i,.server-empty i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5;display:block}
.server-loading i{animation:pulse 1.5s infinite}
/* Server/Save confirmation — mobile: stacked */
.server-confirmation,.save-confirmation,.options-grid{display:flex;flex-direction:column;gap:1rem;margin-bottom:1.5rem}
.server-preview,.save-preview,.save-info-display{background:rgba(0,212,255,.08);border:2px solid rgba(0,212,255,.3);border-radius:10px;padding:1rem;font-family:'Space Mono',monospace}
.server-preview h3,.save-preview h3,.save-info-display h3{color:var(--primary-color);margin-bottom:.75rem;text-align:center;font-size:1rem}
.server-details{color:var(--text-secondary);font-size:.9rem;line-height:1.6}
.server-info{margin:6px 0;display:flex;justify-content:space-between;align-items:center}
.server-info span{font-weight:600;color:var(--text-primary)}
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{display:flex;flex-direction:row;gap:.75rem;flex-wrap:wrap}
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{position:static;width:auto;flex:1;min-width:130px}
.confirm-navigation,.options-actions,.save-actions{display:flex;justify-content:center;margin-top:1rem}
.btn-join-server{background:linear-gradient(135deg,#00ff88,#00cc66)!important;color:#000!important;border:3px solid #00ff88!important;box-shadow:0 6px 20px rgba(0,255,136,.4)!important;font-weight:700!important}
.btn-join-server:hover{background:linear-gradient(135deg,#00ffaa,#00ff88)!important;transform:scale(1.04) translateY(-2px);box-shadow:0 8px 25px rgba(0,255,136,.5)!important}
.selected-server-info-center,.selected-save-info-center,.options-center{display:flex;flex-direction:column;justify-content:center;align-items:center;flex:1}
.save-details{color:#fff;font-size:.9em;line-height:1.6;white-space:pre-wrap}
.save-info{margin:3px 0;font-family:'Space Mono',monospace}
#saveInfoDetails{color:#fff;font-size:.9em;line-height:1.4}
.save-info-content{background:rgba(0,100,200,.2);border-radius:8px;padding:.75rem;margin:0}
/* Save slots */
.save-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem}
.save-slot{background:var(--bg-secondary);border:2px solid var(--border-color);border-radius:10px;padding:1rem;cursor:pointer;transition:all .3s}
.save-slot:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 8px 25px rgba(0,212,255,.2)}
.slot-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}
.slot-number{font-family:'Orbitron',sans-serif;font-weight:700}
.slot-status{padding:3px 10px;border-radius:20px;font-size:.75rem;font-weight:600;text-transform:uppercase}
.slot-status.empty{background:var(--bg-tertiary);color:var(--text-muted)}
.slot-status.has-data{background:var(--success-color);color:var(--bg-primary)}
.slot-name{font-weight:700;color:var(--text-primary);margin-bottom:4px}
.slot-details{color:var(--text-muted);font-size:.9rem}
.slot-btn{width:100%;padding:.55rem;background:var(--gradient-primary);border:none;border-radius:6px;color:var(--text-primary);font-weight:600;cursor:pointer;transition:all .3s}
.slot-btn:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(0,212,255,.3)}
/* Menu footer */
.menu-footer{padding:.9rem 1.5rem;background:var(--bg-secondary);border-top:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
.version-text{color:var(--text-muted);font-size:.8rem}
.footer-links{display:flex;gap:1rem}
.link-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:.85rem;transition:color .3s}
.link-btn:hover{color:var(--primary-color)}
/*
GAME INTERFACE
*/
.game-interface{width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
/* Header */
.game-header{
height:var(--header-h);background:var(--bg-secondary);
border-bottom:1px solid var(--border-color);
display:flex;align-items:center;justify-content:space-between;
padding:0 .75rem;backdrop-filter:blur(10px);flex-shrink:0;
position:sticky;top:0;z-index:500;
}
.header-left{display:flex;align-items:center;gap:.6rem;min-width:0}
.logo{font-size:clamp(1rem,4vw,1.5rem);font-weight:900;color:var(--primary-color);text-shadow:0 0 10px rgba(0,212,255,.5);white-space:nowrap}
.player-info{display:flex;flex-direction:column;gap:.1rem;min-width:0}
.player-info>div{display:flex;align-items:center;gap:.25rem;min-width:0}
.player-name{font-weight:700;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:18ch}
.player-title{font-size:.75rem;color:var(--text-secondary);display:none}
.player-username{font-weight:600;color:var(--accent-color);font-size:.78rem}
.player-level{font-size:.75rem;color:var(--primary-color);white-space:nowrap}
.header-center{flex:1;display:flex;justify-content:center;overflow:hidden;min-width:0;padding:0 .4rem}
.resources{display:flex;gap:.35rem;align-items:center;overflow-x:auto;flex-wrap:nowrap;scrollbar-width:none;-ms-overflow-style:none;padding:.15rem 0}
.resources::-webkit-scrollbar{display:none}
.resource{display:flex;align-items:center;gap:.3rem;padding:.28rem .55rem;background:var(--bg-tertiary);border-radius:20px;border:1px solid var(--border-color);white-space:nowrap;font-size:.75rem;transition:border-color .2s;flex-shrink:0}
.resource:hover{border-color:var(--primary-color)}
.resource i{color:var(--primary-color);font-size:.78rem}
.header-right{display:flex;gap:.3rem;flex-shrink:0}
.header-right .btn{padding:.4rem .55rem;font-size:.8rem;border-radius:7px}
/* ── TOP NAV — desktop only ──────────────────────────────────────────── */
.main-nav{
height:46px;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color);
display:none;align-items:center;padding:0 .75rem;gap:.3rem;
overflow-x:auto;position:sticky;top:var(--header-h);z-index:490;
scrollbar-width:none;
}
.main-nav::-webkit-scrollbar{display:none}
.nav-btn{
display:flex;align-items:center;gap:.35rem;padding:.38rem .7rem;
background:transparent;border:1px solid transparent;border-radius:7px;
color:var(--text-secondary);cursor:pointer;transition:all .25s;
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;
}
.nav-btn:hover{background:var(--hover-bg);color:var(--text-primary);border-color:var(--primary-color)}
.nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent;font-weight:700}
.nav-btn i{font-size:.85rem}
/* ── BOTTOM NAV — mobile primary navigation ──────────────────────────── */
.bottom-nav{
position:fixed;bottom:0;left:0;right:0;
height:var(--nav-h);
background:var(--bg-secondary);border-top:1px solid var(--border-color);
display:flex;align-items:stretch;z-index:600;
overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;
overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;
padding-bottom:env(safe-area-inset-bottom,0px);
}
.bottom-nav::-webkit-scrollbar{display:none}
.bottom-nav-btn{
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:2px;padding:.28rem .4rem;background:transparent;border:none;
color:var(--text-muted);cursor:pointer;transition:color .2s,background .2s;
font-family:'Space Mono',monospace;font-size:.52rem;line-height:1.1;
flex:1;min-width:48px;max-width:68px;white-space:nowrap;overflow:hidden;
border-top:2px solid transparent;
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.bottom-nav-btn i{font-size:1rem;display:block}
.bottom-nav-btn.active{color:var(--primary-color);border-top-color:var(--primary-color);background:rgba(0,212,255,.06)}
.bottom-nav-more{
background:transparent;border:none;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:2px;padding:.28rem .4rem;min-width:48px;max-width:68px;flex-shrink:0;
color:var(--text-muted);cursor:pointer;font-size:.52rem;border-top:2px solid transparent;
-webkit-tap-highlight-color:transparent;touch-action:manipulation;font-family:'Space Mono',monospace;
}
.bottom-nav-more i{font-size:1rem;display:block}
.bottom-nav-more:hover,.bottom-nav-more.open{color:var(--primary-color)}
/* ── NAV DRAWER ──────────────────────────────────────────────────────── */
.nav-drawer{
position:fixed;bottom:var(--nav-h);left:0;right:0;
background:var(--bg-secondary);border-top:1px solid var(--border-color);
border-radius:16px 16px 0 0;z-index:590;
transform:translateY(100%);transition:transform .3s cubic-bezier(.4,0,.2,1);
max-height:58dvh;overflow-y:auto;padding:.75rem 0;
box-shadow:0 -8px 32px rgba(0,0,0,.4);
}
.nav-drawer.open{transform:translateY(0)}
.nav-drawer-handle{width:36px;height:4px;background:var(--border-color);border-radius:2px;margin:0 auto .75rem;cursor:pointer}
.nav-drawer-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.25rem;padding:0 .75rem .5rem}
.nav-drawer-btn{
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:4px;padding:.6rem .4rem;
background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:10px;
color:var(--text-secondary);cursor:pointer;font-size:.68rem;line-height:1.2;
transition:all .2s;text-align:center;font-family:'Space Mono',monospace;
-webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.nav-drawer-btn i{font-size:1.1rem;color:var(--text-muted)}
.nav-drawer-btn:hover,.nav-drawer-btn.active{background:var(--hover-bg);border-color:var(--primary-color);color:var(--primary-color)}
.nav-drawer-btn.active i{color:var(--primary-color)}
.nav-drawer-overlay{position:fixed;inset:0;z-index:580;background:rgba(0,0,0,.45);display:none}
.nav-drawer-overlay.open{display:block}
/* ── MAIN CONTENT ────────────────────────────────────────────────────── */
.main-content{
flex:1;overflow-y:auto;overflow-x:hidden;
padding:var(--pg);background:var(--bg-primary);
padding-bottom:calc(var(--nav-h) + var(--pg) + env(safe-area-inset-bottom,0px));
-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;
}
.tab-content{display:none;animation:fadeIn .2s ease}
.tab-content.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeInUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
/* ── DASHBOARD ───────────────────────────────────────────────────────── */
.dashboard-grid{display:grid;grid-template-columns:1fr;gap:.75rem;max-width:1200px;margin:0 auto}
.card{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;backdrop-filter:blur(10px);transition:border-color .3s,box-shadow .3s}
.card:hover{border-color:var(--primary-color);box-shadow:0 0 18px rgba(0,212,255,.15)}
.card h3{color:var(--primary-color);margin-bottom:.75rem;font-family:'Orbitron',sans-serif;font-size:.88rem;font-weight:700;text-transform:uppercase;letter-spacing:1px}
.fleet-info,.idle-stats,.quick-actions{display:flex;flex-direction:column;gap:.6rem}
.ship-status{display:flex;align-items:center;gap:.75rem}
.ship-status i{font-size:1.6rem;color:var(--secondary-color)}
.player-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:.35rem}
.stat{display:flex;justify-content:space-between;padding:.35rem 0;border-bottom:1px solid var(--border-color);font-size:.8rem}
.stat:last-child{border-bottom:none}
.stat-label{color:var(--text-secondary)} .stat-value{color:var(--primary-color);font-weight:700}
/* ── DUNGEONS ────────────────────────────────────────────────────────── */
.dungeons-container{display:flex;flex-direction:column;gap:.75rem}
.dungeon-selector{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
.dungeon-list{display:flex;flex-direction:column;gap:.4rem}
.dungeon-view{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:1rem;min-height:180px;display:flex;align-items:center;justify-content:center}
.dungeon-placeholder{text-align:center;color:var(--text-muted)}
.dungeon-placeholder i{font-size:2.8rem;margin-bottom:.75rem;opacity:.5;display:block}
/* ── SKILLS ──────────────────────────────────────────────────────────── */
.skills-container{max-width:1200px;margin:0 auto}
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{
display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap;overflow-x:auto;
scrollbar-width:none;padding-bottom:.2rem;
}
.skill-categories::-webkit-scrollbar,.quest-tabs::-webkit-scrollbar,.crafting-categories::-webkit-scrollbar,.shop-categories::-webkit-scrollbar{display:none}
.skill-cat-btn,.quest-tab-btn,.crafting-cat-btn,.shop-cat-btn{
padding:.4rem .85rem;background:var(--bg-tertiary);border:1px solid var(--border-color);
border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;
white-space:nowrap;font-family:'Space Mono',monospace;font-size:.78rem;flex-shrink:0;
}
.skill-cat-btn:hover,.quest-tab-btn:hover,.crafting-cat-btn:hover,.shop-cat-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
.skill-cat-btn.active,.quest-tab-btn.active,.crafting-cat-btn.active,.shop-cat-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
.skills-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
.skills-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
.skills-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
.skill-points-display{background:var(--bg-tertiary);padding:.4rem .75rem;border-radius:7px;border:1px solid var(--border-color)}
.skill-points{color:var(--warning-color);font-weight:600;font-size:.95rem}
/* ── BASE ────────────────────────────────────────────────────────────── */
.base-container{display:flex;flex-direction:column;gap:.75rem}
.base-view,.base-upgrades{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem}
.base-navigation{display:flex;gap:.35rem;margin-bottom:1rem;flex-wrap:wrap}
.base-nav-btn{padding:.4rem .8rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:7px;color:var(--text-secondary);cursor:pointer;transition:all .25s;font-size:.78rem;white-space:nowrap;font-family:'Space Mono',monospace}
.base-nav-btn:hover{border-color:var(--primary-color);color:var(--text-primary)}
.base-nav-btn.active{background:var(--gradient-primary);color:var(--bg-primary);border-color:transparent}
.base-rooms{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:.5rem}
.room-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.55rem .4rem;cursor:pointer;transition:all .25s;text-align:center;min-height:70px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.room-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
.room-item i{font-size:1.2rem;margin-bottom:.3rem;color:var(--primary-color)}
.room-item h4{margin:0;color:var(--text-primary);font-size:.7rem;font-weight:600}
.room-item p{margin:.15rem 0 0;color:var(--text-secondary);font-size:.62rem}
.starbases-container{display:flex;flex-direction:column;gap:.75rem}
.starbase-section{display:flex;flex-direction:column}
.starbase-section h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
.starbase-list,.starbase-shop{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:45dvh;overflow-y:auto}
.starbase-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:.5rem;cursor:pointer;transition:all .25s;max-height:140px;overflow-y:auto}
.starbase-item:hover{border-color:var(--primary-color)}
.starbase-item h4{margin:0 0 .35rem;color:var(--text-primary);font-size:.85rem}
.starbase-item p{margin:0;color:var(--text-secondary);font-size:.78rem}
.starbase-item .level{color:var(--primary-color);font-weight:600}
.starbase-item .description{margin-top:.25rem;line-height:1.3;max-height:55px;overflow-y:auto;padding-right:.4rem}
.starbase-purchase-list{overflow-y:auto;max-height:40dvh}
.base-visualization-container{display:flex;flex-direction:column;gap:.75rem}
#baseCanvas{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);width:100%;height:min(260px,38dvh)}
.base-info-overlay{display:flex;flex-direction:column;gap:.5rem}
.base-stats-overlay{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;max-height:220px;overflow-y:auto}
.base-stats-overlay h3{margin:0 0 .6rem;color:var(--text-primary);font-size:1rem;font-weight:600}
#baseInfoDisplay{color:var(--text-secondary);font-size:.88rem;line-height:1.4}
.upgrade-list{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;max-height:380px;overflow-y:auto;padding:.3rem}
.upgrade-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;cursor:pointer;transition:all .25s}
.upgrade-item:hover{border-color:var(--primary-color);transform:translateY(-2px)}
.upgrade-item h4{margin:0 0 .3rem;color:var(--text-primary);font-size:.82rem}
.upgrade-item p{margin:0;color:var(--text-secondary);font-size:.75rem}
.upgrade-item .cost{margin-top:.3rem;color:var(--primary-color);font-weight:600;font-size:.8rem}
/* ── QUESTS ──────────────────────────────────────────────────────────── */
.quests-container{max-width:1200px;margin:0 auto}
.quest-list{display:flex;flex-direction:column;gap:.75rem}
.quest-difficulty{display:flex;gap:2px;font-size:.88rem;color:#ffd700;margin-right:.75rem}
.difficulty-1{color:#4ade80} .difficulty-2{color:#60a5fa} .difficulty-3{color:#f59e0b} .difficulty-4{color:#ef4444}
.quest-header-info{display:flex;align-items:center;gap:.75rem}
.all-objectives-completed{color:#4ade80;font-weight:600;padding:.4rem;background:rgba(74,222,128,.1);border-radius:4px;text-align:center;font-size:.85rem}
.completion-time{font-size:.78rem;color:var(--text-secondary);margin-top:.4rem;text-align:right}
.daily-countdown,.weekly-countdown{margin-bottom:1rem;padding:.65rem;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px}
.countdown-container{display:flex;align-items:center;gap:.4rem;justify-content:center;color:var(--text-primary);font-size:.85rem}
.countdown-container i{color:var(--primary-color)}
/* ── INVENTORY ───────────────────────────────────────────────────────── */
.inventory-container{display:flex;flex-direction:column;gap:.75rem}
.inventory-grid{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;overflow-y:auto;max-height:50dvh}
.item-details{background:var(--card-bg);border:1px solid var(--border-color);border-radius:var(--r);padding:.75rem;min-height:110px}
.inventory-main{display:flex;gap:1rem;flex:1;min-height:0;flex-direction:column}
.inventory-section{flex:1;min-height:0;display:flex;flex-direction:column}
.inventory-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
#inventoryGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:.5rem;padding:.35rem}
.inventory-slot{width:100%;aspect-ratio:1;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;position:relative}
.inventory-slot.starbase-bonus-slot{border:2px solid var(--primary-color);background:rgba(0,212,255,.08)}
/* Equipment */
.equipment-section{margin-bottom:1.5rem;padding:.75rem;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)}
.equipment-section h3{margin:0 0 .75rem;color:var(--text-primary);font-size:1rem;font-weight:600}
.equipment-slots{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:.75rem}
.equipment-slot{display:flex;flex-direction:column;align-items:center;text-align:center}
.slot-label{font-size:.82rem;color:var(--text-secondary);margin-bottom:.4rem;font-weight:500}
.slot-container{width:70px;height:70px;border:2px solid var(--border-color);border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary);transition:all .3s;cursor:pointer}
.slot-container:hover{border-color:var(--primary-color);box-shadow:0 0 10px rgba(0,123,255,.3)}
/* ── SHOP ────────────────────────────────────────────────────────────── */
.shop-container{max-width:1200px;margin:0 auto}
.shop-items{display:grid;grid-template-columns:1fr;gap:.75rem}
.shop-item.legacy{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:1rem;transition:all .3s}
.shop-item.legacy:hover{border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 5px 15px rgba(0,212,255,.2)}
.shop-item-content{display:flex!important;align-items:flex-start!important;gap:.75rem!important}
.shop-item-image{flex-shrink:0!important;width:64px!important;height:64px!important;border-radius:6px!important;overflow:hidden!important;background:rgba(0,0,0,.3)!important;display:flex!important;align-items:center!important;justify-content:center!important}
.shop-item-name{font-size:1rem!important;font-weight:600!important;color:#00d4ff!important;margin-bottom:4px!important}
.shop-item-description{color:#fff!important;font-size:.85rem!important;margin-bottom:.5rem!important;line-height:1.4!important}
.shop-item-stats{display:flex!important;flex-wrap:wrap!important;gap:.4rem!important;margin-bottom:.5rem!important}
.shop-item-price{font-size:.95rem!important;font-weight:600!important;color:#ffd700!important;margin-bottom:.25rem!important}
.shop-item-rarity{display:inline-block!important;padding:2px 8px!important;border-radius:4px!important;font-size:.75rem!important;font-weight:600!important;text-transform:uppercase!important;margin-bottom:.5rem!important}
.shop-item-rarity.common{background:rgba(128,128,128,.2)!important;color:#808080!important;border:1px solid rgba(128,128,128,.4)!important}
.shop-item-rarity.uncommon{background:rgba(0,255,0,.2)!important;color:#00ff00!important;border:1px solid rgba(0,255,0,.4)!important}
.shop-item-rarity.rare{background:rgba(0,100,255,.2)!important;color:#0064ff!important;border:1px solid rgba(0,100,255,.4)!important}
.shop-item-rarity.epic{background:rgba(128,0,255,.2)!important;color:#8000ff!important;border:1px solid rgba(128,0,255,.4)!important}
.shop-item-rarity.legendary{background:rgba(255,128,0,.2)!important;color:#ff8000!important;border:1px solid rgba(255,128,0,.4)!important}
.shop-item-purchase-btn{width:100%!important;padding:.5rem 1rem!important;background:var(--gradient-primary)!important;color:#fff!important;border:none!important;border-radius:4px!important;cursor:pointer!important;font-weight:600!important;transition:all .3s!important;margin-top:.5rem!important}
.shop-item-purchase-btn:hover:not(.disabled){background:linear-gradient(135deg,#00ffcc,#00ccaa)!important;transform:translateY(-1px)!important}
.shop-item-purchase-btn.disabled{background:rgba(100,100,100,.3)!important;color:#666!important;cursor:not-allowed!important}
.shop-refresh-info{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.75rem;margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
.refresh-info-left{display:flex;flex-direction:column;gap:.35rem}
.refresh-countdown,.purchase-limit-info{display:flex;align-items:center;gap:.4rem;color:var(--text-secondary);font-size:.82rem}
.refresh-countdown i{color:var(--primary-color)}
.purchase-limit-info i{color:#ffd700}
.refresh-shop-btn{background:var(--gradient-primary);color:#fff;border:none;border-radius:6px;padding:.4rem .85rem;font-size:.82rem;cursor:pointer;transition:all .3s;display:flex;align-items:center;gap:.4rem;white-space:nowrap}
.refresh-shop-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,212,255,.3)}
/* ── CRAFTING ────────────────────────────────────────────────────────── */
.crafting-container{max-width:1200px;margin:0 auto}
.crafting-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:.75rem 1rem;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;flex-wrap:wrap;gap:.5rem}
.crafting-header h2{color:var(--primary-color);font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem;margin:0}
.crafting-info{display:flex;gap:.75rem;align-items:center;flex-wrap:wrap}
.crafting-level,.crafting-experience{background:var(--bg-tertiary);padding:.38rem .7rem;border-radius:7px;border:1px solid var(--border-color);display:flex;align-items:center;gap:.4rem;color:var(--text-primary);font-size:.78rem}
.crafting-level i,.crafting-experience i{color:var(--primary-color)}
.crafting-content{display:flex;flex-direction:column;gap:.75rem}
.crafting-sidebar{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
.crafting-categories h3{color:var(--primary-color);font-size:1rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
.crafting-main{display:flex;flex-direction:column;gap:.75rem}
.recipe-list{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem;overflow-y:auto;max-height:45dvh}
.crafting-details{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:.75rem}
.crafting-grid{display:grid;grid-template-columns:1fr;gap:.75rem}
.selected-recipe{display:flex;flex-direction:column;align-items:center;text-align:center;color:var(--text-muted)}
.selected-recipe i{font-size:2.5rem;margin-bottom:.75rem;opacity:.5}
.selected-recipe h3{margin-bottom:.5rem;color:var(--text-secondary)}
/* Recipe items */
.recipe-item{background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:.85rem;margin-bottom:.75rem;cursor:pointer;transition:all .3s;position:relative;overflow:hidden}
.recipe-item:hover{background:var(--hover-bg);border-color:var(--primary-color);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,212,255,.2)}
.recipe-item.selected{background:var(--gradient-primary);border-color:var(--primary-color);box-shadow:0 4px 12px rgba(0,212,255,.3)}
.recipe-item.locked{opacity:.55;cursor:not-allowed}
.recipe-item.locked:hover{transform:none;border-color:var(--border-color)}
.recipe-item.can-craft{border-color:var(--success-color)}
.recipe-item.can-craft:hover{box-shadow:0 4px 12px rgba(0,255,136,.2)}
.recipe-item.missing-materials{opacity:.7;border-color:var(--warning-color)}
.recipe-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
.recipe-header h4{color:var(--text-primary);font-size:.95rem}
.recipe-level{background:var(--bg-primary);color:var(--warning-color);padding:3px 7px;border-radius:4px;font-size:.75rem;font-weight:600}
.recipe-description{color:var(--text-secondary);font-size:.82rem;margin-bottom:.6rem}
.recipe-materials{display:flex;gap:.4rem;flex-wrap:wrap}
.material-tag{background:var(--bg-primary);color:var(--text-muted);padding:2px 6px;border-radius:4px;font-size:.72rem}
.material-item{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.08);font-size:.82rem}
.material-item:last-child{border-bottom:none}
.material-item.missing{color:var(--error-color)}
.material-name{color:var(--text-secondary)} .material-quantity{font-size:.82rem;font-weight:600;color:var(--text-primary)}
.material-item.missing .material-quantity{color:var(--error-color);font-weight:600}
.missing-materials-text{color:var(--error-color);font-size:.78rem;margin-top:.5rem;padding:.5rem;background:rgba(255,51,102,.1);border-radius:4px;border:1px solid rgba(255,51,102,.3)}
.recipe-time{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);font-size:.78rem;margin-top:.4rem}
.recipe-time i{color:var(--primary-color)}
/* ── CONSOLE WINDOW ──────────────────────────────────────────────────── */
.console-window{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(580px,95vw);height:min(380px,75dvh);background:var(--bg-secondary);border:2px solid var(--primary-color);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.8);display:none;flex-direction:column;z-index:10000;font-family:'Courier New',monospace}
.console-header{background:var(--gradient-primary);padding:10px 15px;border-radius:6px 6px 0 0;display:flex;justify-content:space-between;align-items:center;font-weight:bold;font-size:14px}
.console-close{background:none;border:none;color:var(--text-primary);font-size:18px;cursor:pointer;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background .3s}
.console-close:hover{background:rgba(255,255,255,.2)}
.console-content{flex:1;display:flex;flex-direction:column;overflow:hidden}
.console-output{flex:1;padding:12px;overflow-y:auto;background:var(--bg-primary);color:var(--text-primary);font-size:12px;line-height:1.4;border-bottom:1px solid var(--border-color)}
.console-line{margin-bottom:4px;word-wrap:break-word}
.console-line.console-info{color:var(--primary-color)} .console-line.console-success{color:var(--success-color)} .console-line.console-error{color:var(--error-color)} .console-line.console-warning{color:var(--warning-color)}
.console-input-container{padding:10px;background:var(--bg-secondary)}
.console-input{width:100%;background:var(--bg-primary);border:1px solid var(--border-color);color:var(--text-primary);padding:7px 11px;font-family:'Courier New',monospace;font-size:12px;border-radius:4px;outline:none}
.console-input:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(0,212,255,.2)}
/*
ANIMATIONS
*/
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes progressShine{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
/*
TABLET 640px
*/
@media(min-width:640px){
:root{--header-h:56px;--pg:1rem}
html{font-size:15px}
.player-title{display:inline}
.player-name{max-width:22ch}
.dashboard-grid{grid-template-columns:repeat(2,1fr)}
.skills-grid{grid-template-columns:repeat(2,1fr)}
.shop-items{grid-template-columns:repeat(2,1fr)}
.crafting-grid{grid-template-columns:repeat(2,1fr)}
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr))}
.starbases-container{flex-direction:row}
.starbase-section{flex:1}
.base-rooms{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
.upgrade-list{grid-template-columns:repeat(3,1fr)}
.nav-drawer-grid{grid-template-columns:repeat(5,1fr)}
}
/*
DESKTOP 1024px switch to top nav
*/
@media(min-width:1024px){
:root{--header-h:60px;--pg:1rem}
html{font-size:15px}
.main-nav{display:flex}
.bottom-nav,.bottom-nav-more,.nav-drawer,.nav-drawer-overlay{display:none!important}
.main-content{padding-bottom:var(--pg)}
.player-title{display:inline}
.player-name{max-width:26ch;font-size:.95rem}
.resource{font-size:.85rem}
.dashboard-grid{grid-template-columns:repeat(3,1fr);gap:1rem}
.dungeons-container{flex-direction:row}
.dungeon-selector{width:270px;flex-shrink:0;max-height:calc(100dvh - 160px);overflow-y:auto}
.dungeon-view{flex:1;min-height:300px}
.base-container{flex-direction:row}
.base-view{flex:1}
.base-upgrades{width:270px;flex-shrink:0}
.skills-grid{grid-template-columns:repeat(3,1fr)}
.shop-items{grid-template-columns:repeat(3,1fr)}
.crafting-grid{grid-template-columns:repeat(3,1fr)}
.crafting-content{flex-direction:row}
.crafting-sidebar{width:210px;flex-shrink:0}
.crafting-main{flex-direction:row;flex:1}
.recipe-list{flex:1;max-height:none}
.crafting-details{width:290px;flex-shrink:0}
.inventory-container{flex-direction:row}
.inventory-main{flex-direction:row}
.inventory-grid{flex:1;max-height:none}
.item-details{width:270px;flex-shrink:0}
.starbases-container{flex-direction:row}
.starbase-list,.starbase-shop{max-height:calc(100dvh - 250px)}
.base-visualization-container{flex-direction:row}
#baseCanvas{flex:1;height:auto;min-height:280px}
.base-info-overlay{width:270px;flex-shrink:0}
.base-stats-overlay{max-height:calc(100dvh - 310px)}
.upgrade-list{grid-template-columns:repeat(auto-fill,minmax(170px,1fr))}
#inventoryGrid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}
.server-confirmation,.save-confirmation,.options-grid{flex-direction:row;gap:2rem}
.confirm-actions-left,.confirm-actions-right,.options-left,.options-right{flex-direction:column;gap:.75rem;min-width:175px}
.confirm-actions-left .btn-large,.confirm-actions-right .btn-large,.options-left .btn-large,.options-right .btn-large{width:100%;flex:none;min-width:auto}
.base-navigation{justify-content:center}
.skill-categories,.quest-tabs,.crafting-categories,.shop-categories{justify-content:center;flex-wrap:nowrap}
.player-stats-grid{grid-template-columns:repeat(2,1fr)}
}
/*
WIDE 1280px
*/
@media(min-width:1280px){
.dashboard-grid{grid-template-columns:repeat(4,1fr)}
.skills-grid{grid-template-columns:repeat(4,1fr)}
}

View File

@ -1196,6 +1196,13 @@ body {
.player-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.player-info > div {
display: flex;
align-items: center;
gap: 0.3rem;
}
.player-name {
@ -1203,6 +1210,18 @@ body {
color: var(--text-primary);
}
.player-title {
font-weight: 500;
color: var(--text-secondary);
font-size: 0.9rem;
}
.player-username {
font-weight: 600;
color: var(--accent-color);
font-size: 0.85rem;
}
.player-level {
font-size: 0.8rem;
color: var(--primary-color);
@ -1381,6 +1400,20 @@ body {
gap: 0.5rem;
}
.player-stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
max-height: 400px;
overflow-y: auto;
}
@media (max-width: 768px) {
.player-stats-grid {
grid-template-columns: 1fr;
}
}
.stat {
display: flex;
justify-content: space-between;

View File

@ -0,0 +1,185 @@
/*
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}
}

View File

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

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