re-did the entire repo, and added many features and removed lingering features.
@ -1,451 +0,0 @@
|
||||
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const logger = require('./js/core/Logger');
|
||||
|
||||
console.log('[MAIN PROCESS] Electron main process starting...');
|
||||
console.log('[MAIN PROCESS] Node.js version:', process.version);
|
||||
console.log('[MAIN PROCESS] Electron version:', process.versions.electron);
|
||||
console.log('[MAIN PROCESS] Platform:', process.platform);
|
||||
console.log('[MAIN PROCESS] Current working directory:', process.cwd());
|
||||
|
||||
// Keep a global reference of the window object
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
console.log('[MAIN PROCESS] createWindow() called');
|
||||
|
||||
try {
|
||||
console.log('[MAIN PROCESS] Creating BrowserWindow...');
|
||||
// Create the browser window
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 832, // 800 + 32px for custom title bar
|
||||
minWidth: 1200,
|
||||
minHeight: 832,
|
||||
maxWidth: 1200,
|
||||
maxHeight: 832,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true,
|
||||
webSecurity: true
|
||||
},
|
||||
icon: path.join(__dirname, 'assets/icon.png'),
|
||||
show: false, // Don't show until ready-to-show
|
||||
title: 'Galaxy Strike Online'
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] BrowserWindow created successfully');
|
||||
console.log('[MAIN PROCESS] Loading index.html...');
|
||||
|
||||
// Load the index.html file
|
||||
mainWindow.loadFile('index.html');
|
||||
|
||||
console.log('[MAIN PROCESS] index.html loaded, setting up electronAPI...');
|
||||
|
||||
// Set up electronAPI after DOM is ready
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log('[MAIN PROCESS] DOM is ready, setting up electronAPI...');
|
||||
mainWindow.webContents.executeJavaScript(`
|
||||
console.log('[RENDERER] Setting up electronAPI...');
|
||||
window.electronAPI = {
|
||||
minimizeWindow: () => require('electron').ipcRenderer.send('minimize-window'),
|
||||
closeWindow: () => require('electron').ipcRenderer.send('close-window'),
|
||||
toggleFullscreen: () => require('electron').ipcRenderer.send('toggle-fullscreen'),
|
||||
log: (level, message, data) => require('electron').ipcRenderer.send('log-message', { level, message, data }),
|
||||
createSaveFolders: (saveSlots) => require('electron').ipcRenderer.invoke('create-save-folders', saveSlots),
|
||||
testFileAccess: (slotPath) => require('electron').ipcRenderer.invoke('test-file-access', slotPath),
|
||||
saveGame: (slot, saveData) => require('electron').ipcRenderer.invoke('save-game', slot, saveData),
|
||||
loadGame: (slot) => require('electron').ipcRenderer.invoke('load-game', slot),
|
||||
getPath: (name) => require('electron').ipcRenderer.invoke('get-path', name),
|
||||
deleteSaveFile: (slot) => require('electron').ipcRenderer.invoke('delete-save-file', slot)
|
||||
};
|
||||
console.log('[RENDERER] electronAPI setup completed');
|
||||
`).then(() => {
|
||||
console.log('[MAIN PROCESS] electronAPI setup completed');
|
||||
}).catch((error) => {
|
||||
console.error('[MAIN PROCESS] Failed to setup electronAPI:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Show window when ready
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
console.log('[MAIN PROCESS] Window ready-to-show event fired');
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// Open DevTools in development
|
||||
if (process.argv.includes('--dev')) {
|
||||
console.log('[MAIN PROCESS] Opening DevTools...');
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Handle window closed
|
||||
mainWindow.on('closed', () => {
|
||||
console.log('[MAIN PROCESS] Window closed event fired');
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Handle renderer process crashes
|
||||
mainWindow.webContents.on('render-process-gone', (event, details) => {
|
||||
console.error('[MAIN PROCESS] Renderer process crashed:', details);
|
||||
console.error('[MAIN PROCESS] Crash reason:', details.reason);
|
||||
console.error('[MAIN PROCESS] Exit code:', details.exitCode);
|
||||
});
|
||||
|
||||
// Handle renderer process unresponsive
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
console.warn('[MAIN PROCESS] Renderer process unresponsive');
|
||||
});
|
||||
|
||||
// Handle renderer process responsive again
|
||||
mainWindow.webContents.on('responsive', () => {
|
||||
console.log('[MAIN PROCESS] Renderer process responsive again');
|
||||
});
|
||||
|
||||
// Handle console messages from renderer
|
||||
mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||||
console.log(`[RENDERER CONSOLE] [${level}] ${message} (line: ${line}, source: ${sourceId})`);
|
||||
});
|
||||
|
||||
// Handle page load errors
|
||||
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
console.error('[MAIN PROCESS] Page failed to load:', errorCode, errorDescription, validatedURL);
|
||||
});
|
||||
|
||||
// Handle page load success
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
console.log('[MAIN PROCESS] Page finished loading');
|
||||
});
|
||||
|
||||
// Handle DOM ready
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
console.log('[MAIN PROCESS] DOM is ready');
|
||||
});
|
||||
|
||||
// Handle external links
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
console.log('[MAIN PROCESS] External link requested:', url);
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] createWindow() completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Error in createWindow():', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// IPC handlers for save operations
|
||||
ipcMain.handle('create-save-folders', async (event, saveSlots) => {
|
||||
console.log('[MAIN PROCESS] create-save-folders called with saveSlots:', saveSlots);
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
console.log('[MAIN PROCESS] userDataPath:', userDataPath);
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
console.log('[MAIN PROCESS] savesDir:', savesDir);
|
||||
|
||||
// Create main saves directory
|
||||
if (!fs.existsSync(savesDir)) {
|
||||
console.log('[MAIN PROCESS] Creating saves directory:', savesDir);
|
||||
fs.mkdirSync(savesDir, { recursive: true });
|
||||
console.log('[MAIN PROCESS] Saves directory created successfully');
|
||||
} else {
|
||||
console.log('[MAIN PROCESS] Saves directory already exists');
|
||||
}
|
||||
|
||||
const paths = {
|
||||
base: savesDir,
|
||||
slots: []
|
||||
};
|
||||
|
||||
// Create save slot directories
|
||||
for (let i = 1; i <= saveSlots; i++) {
|
||||
const slotDir = path.join(savesDir, `slot${i}`);
|
||||
console.log(`[MAIN PROCESS] Checking/creating slot ${i} directory:`, slotDir);
|
||||
if (!fs.existsSync(slotDir)) {
|
||||
console.log(`[MAIN PROCESS] Creating slot ${i} directory`);
|
||||
fs.mkdirSync(slotDir, { recursive: true });
|
||||
|
||||
// Create initial save info file
|
||||
const saveInfo = {
|
||||
slot: i,
|
||||
created: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: false
|
||||
};
|
||||
|
||||
const infoPath = path.join(slotDir, 'saveinfo.json');
|
||||
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
|
||||
console.log(`[MAIN PROCESS] Created save info for slot ${i}`);
|
||||
} else {
|
||||
console.log(`[MAIN PROCESS] Slot ${i} directory already exists`);
|
||||
}
|
||||
paths.slots.push(slotDir);
|
||||
}
|
||||
|
||||
console.log('[MAIN PROCESS] Save folders created successfully, returning paths:', paths);
|
||||
return { success: true, paths };
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Failed to create save folders:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('test-file-access', async (event, slotPath) => {
|
||||
try {
|
||||
const testFile = path.join(slotPath, 'access_test.txt');
|
||||
fs.writeFileSync(testFile, 'test');
|
||||
fs.unlinkSync(testFile);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-game', async (event, slot, saveData) => {
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
|
||||
// Save game data
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
fs.writeFileSync(saveFilePath, JSON.stringify(saveData, null, 2));
|
||||
|
||||
// Update save info
|
||||
const infoPath = path.join(slotDir, 'saveinfo.json');
|
||||
const saveInfo = {
|
||||
slot: slot,
|
||||
created: new Date().toISOString(),
|
||||
lastSaved: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: true,
|
||||
playTime: saveData.gameTime || 0
|
||||
};
|
||||
fs.writeFileSync(infoPath, JSON.stringify(saveInfo, null, 2));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to save game:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('load-game', async (event, slot) => {
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
|
||||
if (fs.existsSync(saveFilePath)) {
|
||||
const saveContent = fs.readFileSync(saveFilePath, 'utf8');
|
||||
const saveData = JSON.parse(saveContent);
|
||||
return { success: true, data: saveData };
|
||||
} else {
|
||||
return { success: false, error: 'Save file not found' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load game:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-path', async (event, name) => {
|
||||
try {
|
||||
return app.getPath(name);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-save-file', async (event, slot) => {
|
||||
console.log('[MAIN PROCESS] delete-save-file called for slot:', slot);
|
||||
try {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const savesDir = path.join(userDataPath, 'saves');
|
||||
const slotDir = path.join(savesDir, `slot${slot}`);
|
||||
const saveFilePath = path.join(slotDir, 'save.json');
|
||||
const infoFilePath = path.join(slotDir, 'saveinfo.json');
|
||||
|
||||
console.log('[MAIN PROCESS] Attempting to delete save files from:', slotDir);
|
||||
|
||||
let deletedFiles = [];
|
||||
|
||||
// Delete save file if it exists
|
||||
if (fs.existsSync(saveFilePath)) {
|
||||
console.log('[MAIN PROCESS] Deleting save file:', saveFilePath);
|
||||
fs.unlinkSync(saveFilePath);
|
||||
deletedFiles.push('save.json');
|
||||
}
|
||||
|
||||
// Delete save info file if it exists
|
||||
if (fs.existsSync(infoFilePath)) {
|
||||
console.log('[MAIN PROCESS] Deleting save info file:', infoFilePath);
|
||||
fs.unlinkSync(infoFilePath);
|
||||
deletedFiles.push('saveinfo.json');
|
||||
}
|
||||
|
||||
// Create empty save info file to indicate slot is empty
|
||||
const saveInfo = {
|
||||
slot: slot,
|
||||
created: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
exists: false,
|
||||
deleted: new Date().toISOString()
|
||||
};
|
||||
fs.writeFileSync(infoFilePath, JSON.stringify(saveInfo, null, 2));
|
||||
|
||||
console.log('[MAIN PROCESS] Successfully deleted save files for slot', slot, ':', deletedFiles);
|
||||
return { success: true, deletedFiles };
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Failed to delete save file:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// IPC handlers for window controls
|
||||
// Handle logging from renderer process
|
||||
ipcMain.on('log-message', async (event, { level, message, data }) => {
|
||||
try {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
await logger.error(message, data);
|
||||
break;
|
||||
case 'warn':
|
||||
await logger.warn(message, data);
|
||||
break;
|
||||
case 'info':
|
||||
await logger.info(message, data);
|
||||
break;
|
||||
case 'debug':
|
||||
await logger.debug(message, data);
|
||||
break;
|
||||
default:
|
||||
await logger.info(message, data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to log message from renderer:', error);
|
||||
// Fallback to console logging to prevent infinite loops
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('minimize-window', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-window', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-fullscreen', () => {
|
||||
if (mainWindow) {
|
||||
const isFullscreen = mainWindow.isFullScreen();
|
||||
if (isFullscreen) {
|
||||
mainWindow.setFullScreen(false);
|
||||
mainWindow.setSize(1200, 832);
|
||||
mainWindow.center();
|
||||
} else {
|
||||
mainWindow.setFullScreen(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished initialization
|
||||
app.whenReady().then(async () => {
|
||||
console.log('[MAIN PROCESS] Electron app ready, starting initialization...');
|
||||
|
||||
try {
|
||||
// Initialize logger with app data path
|
||||
console.log('[MAIN PROCESS] Initializing logger...');
|
||||
await logger.initialize(app.getPath('userData'));
|
||||
console.log('[MAIN PROCESS] Logger initialized');
|
||||
|
||||
await logger.info('Galaxy Strike Online application starting');
|
||||
console.log('[MAIN PROCESS] Logger info message sent');
|
||||
|
||||
console.log('[MAIN PROCESS] Creating main window...');
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
console.log('[MAIN PROCESS] Activate event fired');
|
||||
// On macOS it's common to re-create a window in the app when the dock icon is clicked
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
console.log('[MAIN PROCESS] No windows exist, creating new window');
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MAIN PROCESS] App initialization completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MAIN PROCESS] Error during app initialization:', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('[MAIN PROCESS] Error in app.whenReady():', error);
|
||||
console.error('[MAIN PROCESS] Error stack:', error.stack);
|
||||
});
|
||||
|
||||
// Quit when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it's common for applications and their menu bar to stay active
|
||||
if (process.platform !== 'darwin') {
|
||||
logger.info('Application shutting down');
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('[MAIN PROCESS] Uncaught Exception:', error);
|
||||
console.error('[MAIN PROCESS] Uncaught Exception stack:', error.stack);
|
||||
|
||||
try {
|
||||
if (logger && typeof logger.errorEvent === 'function') {
|
||||
await logger.errorEvent(error, 'Uncaught Exception in Main Process');
|
||||
}
|
||||
} catch (logError) {
|
||||
console.error('[MAIN PROCESS] Failed to log uncaught exception:', logError);
|
||||
}
|
||||
|
||||
console.error('[MAIN PROCESS] Application will continue running despite uncaught exception');
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[MAIN PROCESS] Unhandled Promise Rejection at:', promise, 'reason:', reason);
|
||||
console.error('[MAIN PROCESS] Rejection reason stack:', reason.stack);
|
||||
});
|
||||
|
||||
// Handle unhandled rejections
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
// Avoid logging the logging system's own errors to prevent infinite loops
|
||||
if (reason && reason.message && reason.message.includes('object could not be cloned')) {
|
||||
console.warn('IPC cloning error detected - this is expected during logger initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
await logger.error('Unhandled Rejection', { reason: reason.toString(), promise: promise.toString() });
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// Security: Prevent new window creation
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('new-window', (event, navigationUrl) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(navigationUrl);
|
||||
});
|
||||
});
|
||||
@ -1,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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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');
|
||||
@ -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');
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
120
Client/data/starbase-layout.json
Normal 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 }
|
||||
}
|
||||
1404
Client/index.html
@ -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', () => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
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 (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
|
||||
};
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,280 +1,110 @@
|
||||
/**
|
||||
* 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.selectedRecipe = null;
|
||||
|
||||
this.initializeRecipes();
|
||||
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
|
||||
});
|
||||
// ------------------------------------------------------------------ //
|
||||
// Initialisation — request recipes from the server
|
||||
// ------------------------------------------------------------------ //
|
||||
async initialize() {
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
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
|
||||
});
|
||||
console.log('[CRAFTING SYSTEM] Requesting recipes from server');
|
||||
|
||||
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
|
||||
});
|
||||
if (!window.game?.socket) {
|
||||
console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected');
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
_fetchRecipesFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = window.game.socket;
|
||||
|
||||
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
|
||||
});
|
||||
const timeout = setTimeout(() => {
|
||||
socket.off('recipes_data', handler);
|
||||
reject(new Error('Recipe data request timed out'));
|
||||
}, 10000);
|
||||
|
||||
// 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
|
||||
});
|
||||
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'));
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
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;
|
||||
@ -282,7 +112,6 @@ class CraftingSystem extends BaseSystem {
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Check for newly unlocked recipes
|
||||
this.checkRecipeUnlocks();
|
||||
}
|
||||
|
||||
@ -291,57 +120,45 @@ class CraftingSystem extends BaseSystem {
|
||||
if (!skillSystem) return;
|
||||
|
||||
for (const [id, recipe] of this.recipes) {
|
||||
if (!recipe.unlocked) {
|
||||
let canCraft = true;
|
||||
if (recipe.unlocked) continue;
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,94 +166,53 @@ class CraftingSystem extends BaseSystem {
|
||||
}
|
||||
|
||||
getMissingMaterials(recipeId) {
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
if (!recipe || !recipe.materials) return [];
|
||||
const recipe = this.recipes.get(recipeId);
|
||||
const inventory = this.game.systems.inventory;
|
||||
if (!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;
|
||||
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 (!recipe || !this.canCraftRecipe(recipeId)) return false;
|
||||
|
||||
if (!this.canCraftRecipe(recipeId)) {
|
||||
console.log(`[CRAFTING] Cannot craft recipe: ${recipe.name}`);
|
||||
return false;
|
||||
}
|
||||
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
|
||||
|
||||
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);
|
||||
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));
|
||||
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000));
|
||||
|
||||
// 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();
|
||||
this.game.systems.questSystem.onItemCrafted?.();
|
||||
}
|
||||
|
||||
console.log(`[CRAFTING] Successfully crafted: ${recipe.name}`);
|
||||
console.log(`[CRAFTING] Done: ${recipe.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -445,10 +221,11 @@ class CraftingSystem extends BaseSystem {
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
|
||||
getSelectedRecipe() {
|
||||
return this.selectedRecipe;
|
||||
}
|
||||
getSelectedRecipe() { return this.selectedRecipe; }
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// UI
|
||||
// ------------------------------------------------------------------ //
|
||||
updateUI() {
|
||||
this.updateRecipeList();
|
||||
this.updateCraftingDetails();
|
||||
@ -456,171 +233,145 @@ class CraftingSystem extends BaseSystem {
|
||||
}
|
||||
|
||||
updateRecipeList() {
|
||||
const recipeListElement = document.getElementById('recipeList');
|
||||
if (!recipeListElement) return;
|
||||
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);
|
||||
|
||||
recipeListElement.innerHTML = '';
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (recipes.length === 0) {
|
||||
recipeListElement.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
|
||||
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 el = document.createElement('div');
|
||||
el.className = 'recipe-item';
|
||||
el.dataset.recipeId = recipe.id;
|
||||
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
const missingMaterials = this.getMissingMaterials(recipe.id);
|
||||
const canCraft = this.canCraftRecipe(recipe.id);
|
||||
const missingMats = this.getMissingMaterials(recipe.id);
|
||||
const skillSystem = this.game.systems.skillSystem;
|
||||
let skillsMet = true;
|
||||
|
||||
// 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;
|
||||
}
|
||||
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');
|
||||
}
|
||||
if (!skillsMet) el.classList.add('locked');
|
||||
else if (!canCraft) el.classList.add('missing-materials');
|
||||
else el.classList.add('can-craft');
|
||||
|
||||
// Generate requirements text
|
||||
const requirementsText = recipe.requirements ?
|
||||
Object.entries(recipe.requirements).map(([skill, level]) => `${skill}: ${level}`).join(', ') : 'None';
|
||||
const reqText = recipe.requirements
|
||||
? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).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;
|
||||
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('');
|
||||
|
||||
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 = `
|
||||
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'}"
|
||||
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
|
||||
${canCraft ? 'Craft Item' : 'Cannot Craft'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
updateCraftingInfo() {
|
||||
@ -628,24 +379,19 @@ class CraftingSystem extends BaseSystem {
|
||||
if (!skillSystem) return;
|
||||
|
||||
const craftingLevel = skillSystem.getSkillLevel('crafting');
|
||||
const craftingExp = skillSystem.getSkillExperience('crafting');
|
||||
const expNeeded = skillSystem.getExperienceNeeded('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 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,6 +430,13 @@ class ItemSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* catalog getter — alias for shopItemsByCategory, used by Economy.updateShopUI
|
||||
*/
|
||||
get catalog() {
|
||||
return this.shopItemsByCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
|
||||
@ -1,463 +1,284 @@
|
||||
/**
|
||||
* 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() {
|
||||
}
|
||||
if (this._loaded || this._loading) return;
|
||||
this._loading = true;
|
||||
|
||||
// Skill management
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
levelUpSkill(category, skillId) {
|
||||
const skill = this.skills[category][skillId];
|
||||
|
||||
// Handle excess experience
|
||||
const excessExperience = skill.experience - skill.experienceToNext;
|
||||
const excess = skill.experience - skill.experienceToNext;
|
||||
|
||||
skill.currentLevel++;
|
||||
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
|
||||
skill.experience = Math.max(0, excess);
|
||||
|
||||
// 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 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; }
|
||||
|
||||
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) {
|
||||
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unlockSkill(category, skillId) {
|
||||
const skill = this.skills[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) { 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;
|
||||
for (const category of Object.values(this.skills)) {
|
||||
for (const skill of Object.values(category)) {
|
||||
if (!skill.unlocked || skill.currentLevel <= 0) continue;
|
||||
|
||||
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 [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;
|
||||
@ -467,38 +288,43 @@ return true;
|
||||
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 el = document.createElement('div');
|
||||
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
||||
|
||||
const progressPercent = skill.currentLevel > 0 ?
|
||||
(skill.experience / skill.experienceToNext) * 100 : 0;
|
||||
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');
|
||||
const iconClass = this.game.systems.textureManager
|
||||
? this.game.systems.textureManager.getIcon(skill.icon)
|
||||
: (skill.icon || 'fa-question');
|
||||
|
||||
skillElement.innerHTML = `
|
||||
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(); }
|
||||
}
|
||||
|
||||
1001
Client/js/systems/StarbaseWorld.js
Normal file
228
Galaxy-Strike-Online-main/.github/workflows/build-client.yml
vendored
Normal 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
@ -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-*
|
||||
19
Galaxy-Strike-Online-main/API/config/database.js
Normal file
@ -0,0 +1,19 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/galaxystrikeonline';
|
||||
|
||||
const conn = await mongoose.connect(mongoUri, {
|
||||
// Remove deprecated options for newer MongoDB versions
|
||||
});
|
||||
|
||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||
} catch (error) {
|
||||
logger.error('Database connection error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
131
Galaxy-Strike-Online-main/API/config/production.js
Normal 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
|
||||
};
|
||||
134
Galaxy-Strike-Online-main/API/middleware/errorHandler.js
Normal file
@ -0,0 +1,134 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Custom error classes
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(message) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationError extends AppError {
|
||||
constructor(message = 'Authentication failed') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationError extends AppError {
|
||||
constructor(message = 'Access denied') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message = 'Resource not found') {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Resource conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message = 'Database operation failed') {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
logger.error({
|
||||
error: err,
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
userId: req.userId
|
||||
}
|
||||
});
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message).join(', ');
|
||||
error = new ValidationError(message);
|
||||
}
|
||||
|
||||
// Mongoose duplicate key error
|
||||
if (err.code === 11000) {
|
||||
const field = Object.keys(err.keyValue)[0];
|
||||
const value = err.keyValue[field];
|
||||
error = new ConflictError(`${field} '${value}' already exists`);
|
||||
}
|
||||
|
||||
// Mongoose cast error
|
||||
if (err.name === 'CastError') {
|
||||
error = new ValidationError(`Invalid ${err.path}: ${err.value}`);
|
||||
}
|
||||
|
||||
// JWT errors
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
error = new AuthenticationError('Invalid token');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
error = new AuthenticationError('Token expired');
|
||||
}
|
||||
|
||||
// Default error
|
||||
if (!error.isOperational) {
|
||||
error = new AppError('Something went wrong', 500);
|
||||
}
|
||||
|
||||
res.status(error.statusCode || 500).json({
|
||||
status: error.status || 'error',
|
||||
message: error.message,
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
stack: error.stack,
|
||||
error: err
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// 404 handler
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
errorHandler,
|
||||
catchAsync,
|
||||
notFound
|
||||
};
|
||||
134
Galaxy-Strike-Online-main/API/models/GameServer.js
Normal 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);
|
||||
306
Galaxy-Strike-Online-main/API/models/Inventory.js
Normal 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);
|
||||
155
Galaxy-Strike-Online-main/API/models/Player.js
Normal 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);
|
||||
189
Galaxy-Strike-Online-main/API/models/Ship.js
Normal file
@ -0,0 +1,189 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const shipSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'Player'
|
||||
},
|
||||
|
||||
// Ship identification
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['Fighter', 'Cruiser', 'Battleship', 'Carrier', 'Explorer']
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
|
||||
// Ship stats
|
||||
stats: {
|
||||
health: { type: Number, required: true },
|
||||
maxHealth: { type: Number, required: true },
|
||||
attack: { type: Number, required: true },
|
||||
defense: { type: Number, required: true },
|
||||
speed: { type: Number, required: true },
|
||||
criticalChance: { type: Number, default: 0.05 },
|
||||
criticalDamage: { type: Number, default: 1.5 },
|
||||
hull: { type: Number, required: true }
|
||||
},
|
||||
|
||||
// Ship appearance
|
||||
texture: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// Ship progression
|
||||
experience: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
requiredExp: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
upgrades: [{
|
||||
type: String
|
||||
}],
|
||||
|
||||
// Ship status
|
||||
isEquipped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Shop information (if purchased)
|
||||
price: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ['common', 'uncommon', 'rare', 'epic', 'legendary'],
|
||||
default: 'common'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
acquiredAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for performance
|
||||
shipSchema.index({ userId: 1 });
|
||||
shipSchema.index({ id: 1 });
|
||||
shipSchema.index({ isEquipped: 1 });
|
||||
shipSchema.index({ isCurrent: 1 });
|
||||
|
||||
// Methods
|
||||
shipSchema.methods.addExperience = function(amount) {
|
||||
this.experience += amount;
|
||||
|
||||
// Level up logic
|
||||
while (this.experience >= this.requiredExp) {
|
||||
this.experience -= this.requiredExp;
|
||||
this.level += 1;
|
||||
this.requiredExp = this.level * 100;
|
||||
|
||||
// Increase stats on level up
|
||||
this.stats.maxHealth += 10;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
this.stats.attack += 2;
|
||||
this.stats.defense += 1;
|
||||
this.stats.speed += 1;
|
||||
}
|
||||
|
||||
return this.level;
|
||||
};
|
||||
|
||||
shipSchema.methods.takeDamage = function(damage) {
|
||||
const actualDamage = Math.max(0, damage - this.stats.defense);
|
||||
this.stats.health = Math.max(0, this.stats.health - actualDamage);
|
||||
|
||||
if (this.stats.health === 0) {
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
return actualDamage;
|
||||
};
|
||||
|
||||
shipSchema.methods.heal = function(amount) {
|
||||
const healAmount = Math.min(amount, this.stats.maxHealth - this.stats.health);
|
||||
this.stats.health += healAmount;
|
||||
this.isDestroyed = false;
|
||||
|
||||
return healAmount;
|
||||
};
|
||||
|
||||
shipSchema.methods.isAlive = function() {
|
||||
return this.stats.health > 0;
|
||||
};
|
||||
|
||||
shipSchema.methods.getStatSummary = function() {
|
||||
return {
|
||||
name: this.name,
|
||||
class: this.class,
|
||||
level: this.level,
|
||||
health: `${this.stats.health}/${this.stats.maxHealth}`,
|
||||
attack: this.stats.attack,
|
||||
defense: this.stats.defense,
|
||||
speed: this.stats.speed,
|
||||
criticalChance: `${(this.stats.criticalChance * 100).toFixed(1)}%`,
|
||||
criticalDamage: `${this.stats.criticalDamage}x`
|
||||
};
|
||||
};
|
||||
|
||||
shipSchema.methods.upgrade = function(upgradeType) {
|
||||
switch (upgradeType) {
|
||||
case 'health':
|
||||
this.stats.maxHealth += 20;
|
||||
this.stats.health = this.stats.maxHealth;
|
||||
break;
|
||||
case 'attack':
|
||||
this.stats.attack += 5;
|
||||
break;
|
||||
case 'defense':
|
||||
this.stats.defense += 3;
|
||||
break;
|
||||
case 'speed':
|
||||
this.stats.speed += 2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown upgrade type');
|
||||
}
|
||||
|
||||
if (!this.upgrades.includes(upgradeType)) {
|
||||
this.upgrades.push(upgradeType);
|
||||
}
|
||||
|
||||
this.lastUsed = new Date();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Ship', shipSchema);
|
||||
6068
Galaxy-Strike-Online-main/API/package-lock.json
generated
Normal file
39
Galaxy-Strike-Online-main/API/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
214
Galaxy-Strike-Online-main/API/routes/auth.js
Normal 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;
|
||||
419
Galaxy-Strike-Online-main/API/routes/servers.js
Normal 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;
|
||||
67
Galaxy-Strike-Online-main/API/scripts/createTestServer.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Create Test Server Script
|
||||
* Adds a test server to the database for testing the server browser
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GameServer = require('../models/GameServer');
|
||||
require('dotenv').config();
|
||||
|
||||
async function createTestServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log('Connected to database');
|
||||
|
||||
// Check if test server already exists
|
||||
const existingServer = await GameServer.findOne({ serverId: 'test_server_001' });
|
||||
if (existingServer) {
|
||||
console.log('Test server already exists, deleting it first...');
|
||||
await GameServer.deleteOne({ serverId: 'test_server_001' });
|
||||
}
|
||||
|
||||
// Create test server
|
||||
const testServer = new GameServer({
|
||||
serverId: 'test_server_001',
|
||||
name: 'Test Server - Galaxy Strike',
|
||||
type: 'public',
|
||||
region: 'us-east',
|
||||
maxPlayers: 10,
|
||||
currentPlayers: 2,
|
||||
owner: {
|
||||
userId: 'test_user_001',
|
||||
username: 'TestAdmin'
|
||||
},
|
||||
settings: {
|
||||
description: 'A test server for Galaxy Strike Online',
|
||||
tags: ['test', 'beginner', 'pve']
|
||||
},
|
||||
status: 'active',
|
||||
gameServerUrl: 'https://api.korvarix.com'
|
||||
});
|
||||
|
||||
await testServer.save();
|
||||
console.log('Test server created successfully!');
|
||||
console.log('Server details:', {
|
||||
id: testServer.serverId,
|
||||
name: testServer.name,
|
||||
type: testServer.type,
|
||||
region: testServer.region,
|
||||
currentPlayers: testServer.currentPlayers,
|
||||
maxPlayers: testServer.maxPlayers,
|
||||
status: testServer.status
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating test server:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (require.main === module) {
|
||||
createTestServer();
|
||||
}
|
||||
|
||||
module.exports = createTestServer;
|
||||
50
Galaxy-Strike-Online-main/API/scripts/migrate.js
Normal 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;
|
||||
71
Galaxy-Strike-Online-main/API/scripts/migratePasswords.js
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Password Migration Script
|
||||
* Updates existing users to have password fields
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const Player = require('../models/Player');
|
||||
const logger = require('../utils/logger');
|
||||
require('dotenv').config();
|
||||
|
||||
async function migratePasswords() {
|
||||
try {
|
||||
// Connect to database
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
logger.info('Connected to database for password migration');
|
||||
|
||||
// Find all users without passwords
|
||||
const usersWithoutPasswords = await Player.find({
|
||||
password: { $exists: false }
|
||||
});
|
||||
|
||||
logger.info(`Found ${usersWithoutPasswords.length} users without passwords`);
|
||||
|
||||
if (usersWithoutPasswords.length === 0) {
|
||||
logger.info('No users need password migration');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update each user with a default password
|
||||
for (const user of usersWithoutPasswords) {
|
||||
// Generate a default password (you might want to use a different approach)
|
||||
const defaultPassword = 'tempPassword123!';
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(defaultPassword, salt);
|
||||
|
||||
await Player.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
'stats.lastLogin': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Migrated user: ${user.email} with default password`);
|
||||
}
|
||||
|
||||
logger.info('Password migration completed successfully');
|
||||
|
||||
// Output the default password for users to change
|
||||
console.log('\n=== MIGRATION COMPLETE ===');
|
||||
console.log(`Updated ${usersWithoutPasswords.length} users`);
|
||||
console.log('Default password for all migrated users: tempPassword123!');
|
||||
console.log('Users should change their password after first login\n');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Password migration error:', error);
|
||||
console.error('Migration failed:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
if (require.main === module) {
|
||||
migratePasswords();
|
||||
}
|
||||
|
||||
module.exports = migratePasswords;
|
||||
196
Galaxy-Strike-Online-main/API/scripts/seed.js
Normal 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;
|
||||
234
Galaxy-Strike-Online-main/API/server.js
Normal 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 };
|
||||
272
Galaxy-Strike-Online-main/API/socket/socketHandlers.js
Normal 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;
|
||||
385
Galaxy-Strike-Online-main/API/systems/EconomySystem.js
Normal file
@ -0,0 +1,385 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class EconomySystem {
|
||||
constructor() {
|
||||
this.shopItems = {
|
||||
ships: [],
|
||||
weapons: [],
|
||||
armors: [],
|
||||
materials: [],
|
||||
consumables: []
|
||||
};
|
||||
|
||||
this.dailyRewards = {
|
||||
baseReward: 100,
|
||||
consecutiveBonus: 50,
|
||||
maxConsecutiveDays: 30
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
logger.info('Initializing Economy System...');
|
||||
|
||||
// Initialize shop items
|
||||
await this.initializeShopItems();
|
||||
|
||||
logger.info('Economy System initialized successfully');
|
||||
}
|
||||
|
||||
async initializeShopItems() {
|
||||
// Ships
|
||||
this.shopItems.ships = [
|
||||
// Starter Cruiser Variants
|
||||
{
|
||||
id: 'starter_cruiser_common',
|
||||
name: 'Starter Cruiser',
|
||||
type: 'ship',
|
||||
rarity: 'common',
|
||||
price: 5000,
|
||||
currency: 'credits',
|
||||
description: 'Reliable starter cruiser for new pilots',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 15, speed: 10, defense: 12, hull: 100 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_uncommon',
|
||||
name: 'Starter Cruiser II',
|
||||
type: 'ship',
|
||||
rarity: 'uncommon',
|
||||
price: 12000,
|
||||
currency: 'credits',
|
||||
description: 'Upgraded starter cruiser with enhanced systems',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 18, speed: 12, defense: 15, hull: 120 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_rare',
|
||||
name: 'Starter Cruiser III',
|
||||
type: 'ship',
|
||||
rarity: 'rare',
|
||||
price: 25000,
|
||||
currency: 'credits',
|
||||
description: 'Elite starter cruiser with advanced weaponry',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 22, speed: 14, defense: 18, hull: 140 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_epic',
|
||||
name: 'Starter Cruiser IV',
|
||||
type: 'ship',
|
||||
rarity: 'epic',
|
||||
price: 50000,
|
||||
currency: 'credits',
|
||||
description: 'Master starter cruiser with elite modifications',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 28, speed: 16, defense: 22, hull: 160 }
|
||||
},
|
||||
{
|
||||
id: 'starter_cruiser_legendary',
|
||||
name: 'Starter Cruiser V',
|
||||
type: 'ship',
|
||||
rarity: 'legendary',
|
||||
price: 100000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary starter cruiser with unparalleled performance',
|
||||
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||
stats: { attack: 35, speed: 18, defense: 28, hull: 180 }
|
||||
}
|
||||
];
|
||||
|
||||
// Weapons
|
||||
this.shopItems.weapons = [
|
||||
// Starter Blaster Variants
|
||||
{
|
||||
id: 'starter_blaster_common',
|
||||
name: 'Common Blaster',
|
||||
type: 'weapon',
|
||||
rarity: 'common',
|
||||
price: 1000,
|
||||
currency: 'credits',
|
||||
description: 'Basic blaster for new pilots',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 10, fireRate: 2, range: 5, energy: 5 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_uncommon',
|
||||
name: 'Starter Blaster II',
|
||||
type: 'weapon',
|
||||
rarity: 'uncommon',
|
||||
price: 2500,
|
||||
currency: 'credits',
|
||||
description: 'Improved blaster with better damage output',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 12, fireRate: 2.2, range: 5.5, energy: 6 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_rare',
|
||||
name: 'Starter Blaster III',
|
||||
type: 'weapon',
|
||||
rarity: 'rare',
|
||||
price: 5000,
|
||||
currency: 'credits',
|
||||
description: 'Advanced blaster with enhanced capabilities',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 15, fireRate: 2.5, range: 6, energy: 7 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_epic',
|
||||
name: 'Starter Blaster IV',
|
||||
type: 'weapon',
|
||||
rarity: 'epic',
|
||||
price: 10000,
|
||||
currency: 'credits',
|
||||
description: 'Elite blaster with superior performance',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 18, fireRate: 3, range: 6.5, energy: 8 }
|
||||
},
|
||||
{
|
||||
id: 'starter_blaster_legendary',
|
||||
name: 'Starter Blaster V',
|
||||
type: 'weapon',
|
||||
rarity: 'legendary',
|
||||
price: 20000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary starter blaster with ultimate power',
|
||||
texture: 'assets/textures/weapons/starter_blaster.png',
|
||||
stats: { damage: 22, fireRate: 4, range: 7, energy: 10 }
|
||||
}
|
||||
];
|
||||
|
||||
// Armors
|
||||
this.shopItems.armors = [
|
||||
// Basic Armor Variants
|
||||
{
|
||||
id: 'basic_armor_common',
|
||||
name: 'Basic Armor',
|
||||
type: 'armor',
|
||||
rarity: 'common',
|
||||
price: 1500,
|
||||
currency: 'credits',
|
||||
description: 'Light protection for beginners',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 5, durability: 20, weight: 2, energyShield: 0 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_uncommon',
|
||||
name: 'Basic Armor II',
|
||||
type: 'armor',
|
||||
rarity: 'uncommon',
|
||||
price: 4000,
|
||||
currency: 'credits',
|
||||
description: 'Improved basic armor with better durability',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 7, durability: 25, weight: 2.2, energyShield: 2 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_rare',
|
||||
name: 'Basic Armor III',
|
||||
type: 'armor',
|
||||
rarity: 'rare',
|
||||
price: 8000,
|
||||
currency: 'credits',
|
||||
description: 'Enhanced armor with energy shielding',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 10, durability: 30, weight: 2.5, energyShield: 5 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_epic',
|
||||
name: 'Basic Armor IV',
|
||||
type: 'armor',
|
||||
rarity: 'epic',
|
||||
price: 15000,
|
||||
currency: 'credits',
|
||||
description: 'Elite armor with advanced protection systems',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 15, durability: 35, weight: 3, energyShield: 10 }
|
||||
},
|
||||
{
|
||||
id: 'basic_armor_legendary',
|
||||
name: 'Basic Armor V',
|
||||
type: 'armor',
|
||||
rarity: 'legendary',
|
||||
price: 30000,
|
||||
currency: 'credits',
|
||||
description: 'Legendary armor with ultimate protection',
|
||||
texture: 'assets/textures/armors/basic_armor.png',
|
||||
stats: { defense: 20, durability: 40, weight: 3.5, energyShield: 15 }
|
||||
}
|
||||
];
|
||||
|
||||
// Materials
|
||||
this.shopItems.materials = [
|
||||
{
|
||||
id: 'iron_ore',
|
||||
name: 'Iron Ore',
|
||||
type: 'material',
|
||||
rarity: 'common',
|
||||
price: 50,
|
||||
currency: 'credits',
|
||||
description: 'Raw iron ore used for crafting basic weapons and armor',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'copper_wire',
|
||||
name: 'Copper Wire',
|
||||
type: 'material',
|
||||
rarity: 'common',
|
||||
price: 75,
|
||||
currency: 'credits',
|
||||
description: 'Copper wiring used in electronic components',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'energy_crystal',
|
||||
name: 'Energy Crystal',
|
||||
type: 'material',
|
||||
rarity: 'uncommon',
|
||||
price: 200,
|
||||
currency: 'credits',
|
||||
description: 'Crystallized energy used for powered equipment',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'rare_metal',
|
||||
name: 'Rare Metal',
|
||||
type: 'material',
|
||||
rarity: 'rare',
|
||||
price: 500,
|
||||
currency: 'credits',
|
||||
description: 'Rare metallic alloy used for high-end crafting',
|
||||
stackable: true
|
||||
},
|
||||
{
|
||||
id: 'advanced_components',
|
||||
name: 'Advanced Components',
|
||||
type: 'material',
|
||||
rarity: 'rare',
|
||||
price: 1000,
|
||||
currency: 'credits',
|
||||
description: 'Sophisticated electronic components for advanced ship systems',
|
||||
stackable: true
|
||||
}
|
||||
];
|
||||
|
||||
// Consumables
|
||||
this.shopItems.consumables = [
|
||||
{
|
||||
id: 'health_kit',
|
||||
name: 'Health Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
price: 100,
|
||||
currency: 'credits',
|
||||
description: 'A medical kit that restores health',
|
||||
consumable: true,
|
||||
effect: { health: 50 }
|
||||
},
|
||||
{
|
||||
id: 'energy_pack',
|
||||
name: 'Energy Pack',
|
||||
type: 'consumable',
|
||||
rarity: 'common',
|
||||
price: 150,
|
||||
currency: 'credits',
|
||||
description: 'A pack that restores energy',
|
||||
consumable: true,
|
||||
effect: { energy: 25 }
|
||||
},
|
||||
{
|
||||
id: 'repair_kit',
|
||||
name: 'Repair Kit',
|
||||
type: 'consumable',
|
||||
rarity: 'uncommon',
|
||||
price: 300,
|
||||
currency: 'credits',
|
||||
description: 'A kit that repairs ship damage',
|
||||
consumable: true,
|
||||
effect: { health: 100 }
|
||||
}
|
||||
];
|
||||
|
||||
logger.info(`Shop initialized with ${this.getTotalShopItems()} items`);
|
||||
}
|
||||
|
||||
getTotalShopItems() {
|
||||
return Object.values(this.shopItems).reduce((total, category) => total + category.length, 0);
|
||||
}
|
||||
|
||||
getShopItems(category = null) {
|
||||
if (category && this.shopItems[category]) {
|
||||
return this.shopItems[category];
|
||||
}
|
||||
return this.shopItems;
|
||||
}
|
||||
|
||||
getItem(itemId) {
|
||||
for (const category of Object.values(this.shopItems)) {
|
||||
const item = category.find(item => item.id === itemId);
|
||||
if (item) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
purchaseItem(userId, itemId, quantity = 1) {
|
||||
const item = this.getItem(itemId);
|
||||
if (!item) {
|
||||
throw new Error('Item not found in shop');
|
||||
}
|
||||
|
||||
const totalCost = item.price * quantity;
|
||||
|
||||
return {
|
||||
item,
|
||||
quantity,
|
||||
totalCost,
|
||||
currency: item.currency
|
||||
};
|
||||
}
|
||||
|
||||
calculateDailyReward(consecutiveDays) {
|
||||
const bonusMultiplier = Math.min(consecutiveDays - 1, this.dailyRewards.maxConsecutiveDays - 1);
|
||||
const bonusAmount = bonusMultiplier * this.dailyRewards.consecutiveBonus;
|
||||
const totalReward = this.dailyRewards.baseReward + bonusAmount;
|
||||
|
||||
return {
|
||||
baseReward: this.dailyRewards.baseReward,
|
||||
consecutiveBonus: bonusAmount,
|
||||
totalReward,
|
||||
consecutiveDays
|
||||
};
|
||||
}
|
||||
|
||||
getRandomShopItems(category, count = 6) {
|
||||
const items = this.shopItems[category] || [];
|
||||
const shuffled = [...items].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, Math.min(count, items.length));
|
||||
}
|
||||
|
||||
refreshShopInventory() {
|
||||
logger.info('Refreshing shop inventory...');
|
||||
// This would typically involve database operations
|
||||
// For now, we'll just log the refresh
|
||||
return true;
|
||||
}
|
||||
|
||||
getShopStats() {
|
||||
const stats = {
|
||||
totalItems: this.getTotalShopItems(),
|
||||
itemsByCategory: {},
|
||||
averagePriceByCategory: {}
|
||||
};
|
||||
|
||||
for (const [category, items] of Object.entries(this.shopItems)) {
|
||||
stats.itemsByCategory[category] = items.length;
|
||||
|
||||
if (items.length > 0) {
|
||||
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
|
||||
stats.averagePriceByCategory[category] = Math.round(totalPrice / items.length);
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EconomySystem;
|
||||
293
Galaxy-Strike-Online-main/API/systems/GameSystem.js
Normal file
@ -0,0 +1,293 @@
|
||||
const logger = require('../utils/logger');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
const Economy = require('./EconomySystem');
|
||||
|
||||
class GameSystem {
|
||||
constructor() {
|
||||
this.players = new Map();
|
||||
this.servers = new Map();
|
||||
this.economy = new Economy();
|
||||
}
|
||||
|
||||
async initializeGameSystems() {
|
||||
logger.info('Initializing server-side game systems...');
|
||||
|
||||
// Initialize economy system
|
||||
await this.economy.initialize();
|
||||
|
||||
logger.info('Game systems initialized successfully');
|
||||
}
|
||||
|
||||
// Player management
|
||||
async createPlayer(userId, playerData) {
|
||||
try {
|
||||
const player = new Player({
|
||||
userId,
|
||||
...playerData,
|
||||
createdAt: new Date(),
|
||||
lastLogin: new Date()
|
||||
});
|
||||
|
||||
await player.save();
|
||||
this.players.set(userId, player);
|
||||
|
||||
logger.info(`Created new player for user: ${userId}`);
|
||||
return player;
|
||||
} catch (error) {
|
||||
logger.error('Error creating player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadPlayer(userId) {
|
||||
try {
|
||||
let player = this.players.get(userId);
|
||||
|
||||
if (!player) {
|
||||
player = await Player.findOne({ userId }).populate('ships inventory');
|
||||
if (player) {
|
||||
this.players.set(userId, player);
|
||||
}
|
||||
}
|
||||
|
||||
return player;
|
||||
} catch (error) {
|
||||
logger.error('Error loading player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async savePlayer(userId) {
|
||||
try {
|
||||
const player = this.players.get(userId);
|
||||
if (player) {
|
||||
await player.save();
|
||||
logger.info(`Saved player data for user: ${userId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error saving player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Ship management
|
||||
async addShipToPlayer(userId, shipData) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
const ship = new Ship({
|
||||
...shipData,
|
||||
userId,
|
||||
acquiredAt: new Date()
|
||||
});
|
||||
|
||||
await ship.save();
|
||||
player.ships.push(ship._id);
|
||||
await player.save();
|
||||
|
||||
logger.info(`Added ship ${ship.name} to player ${userId}`);
|
||||
return ship;
|
||||
} catch (error) {
|
||||
logger.error('Error adding ship to player:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async equipShip(userId, shipId) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
const ship = await Ship.findOne({ _id: shipId, userId });
|
||||
if (!ship) {
|
||||
throw new Error('Ship not found');
|
||||
}
|
||||
|
||||
// Unequip current ship
|
||||
if (player.currentShip) {
|
||||
await Ship.findByIdAndUpdate(player.currentShip, { isEquipped: false });
|
||||
}
|
||||
|
||||
// Equip new ship
|
||||
ship.isEquipped = true;
|
||||
await ship.save();
|
||||
|
||||
player.currentShip = ship._id;
|
||||
await player.save();
|
||||
|
||||
logger.info(`Equipped ship ${ship.name} for player ${userId}`);
|
||||
return ship;
|
||||
} catch (error) {
|
||||
logger.error('Error equipping ship:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Server management
|
||||
async createServer(serverData) {
|
||||
try {
|
||||
const serverId = `server_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const server = {
|
||||
id: serverId,
|
||||
...serverData,
|
||||
createdAt: new Date(),
|
||||
players: [],
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this.servers.set(serverId, server);
|
||||
logger.info(`Created new server: ${serverId}`);
|
||||
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error creating server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async joinServer(serverId, userId) {
|
||||
try {
|
||||
const server = this.servers.get(serverId);
|
||||
if (!server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
|
||||
if (server.players.length >= server.maxPlayers) {
|
||||
throw new Error('Server is full');
|
||||
}
|
||||
|
||||
if (!server.players.includes(userId)) {
|
||||
server.players.push(userId);
|
||||
}
|
||||
|
||||
logger.info(`Player ${userId} joined server ${serverId}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error joining server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async leaveServer(serverId, userId) {
|
||||
try {
|
||||
const server = this.servers.get(serverId);
|
||||
if (!server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
|
||||
server.players = server.players.filter(id => id !== userId);
|
||||
|
||||
if (server.players.length === 0) {
|
||||
this.servers.delete(serverId);
|
||||
logger.info(`Server ${serverId} deleted (no players)`);
|
||||
}
|
||||
|
||||
logger.info(`Player ${userId} left server ${serverId}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
logger.error('Error leaving server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getServerList() {
|
||||
return Array.from(this.servers.values()).map(server => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
maxPlayers: server.maxPlayers,
|
||||
currentPlayers: server.players.length,
|
||||
status: server.status,
|
||||
region: server.region,
|
||||
createdAt: server.createdAt
|
||||
}));
|
||||
}
|
||||
|
||||
// Game actions
|
||||
async processGameAction(userId, actionData) {
|
||||
try {
|
||||
const player = await this.loadPlayer(userId);
|
||||
if (!player) {
|
||||
throw new Error('Player not found');
|
||||
}
|
||||
|
||||
switch (actionData.type) {
|
||||
case 'dungeon_enter':
|
||||
return await this.handleDungeonEnter(player, actionData);
|
||||
case 'ship_upgrade':
|
||||
return await this.handleShipUpgrade(player, actionData);
|
||||
case 'item_purchase':
|
||||
return await this.handleItemPurchase(player, actionData);
|
||||
case 'daily_reward':
|
||||
return await this.handleDailyReward(player, actionData);
|
||||
default:
|
||||
throw new Error('Unknown action type');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing game action:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async handleDungeonEnter(player, data) {
|
||||
// Dungeon logic will be implemented here
|
||||
logger.info(`Player ${player.userId} entering dungeon`);
|
||||
return { success: true, message: 'Dungeon entered' };
|
||||
}
|
||||
|
||||
async handleShipUpgrade(player, data) {
|
||||
// Ship upgrade logic will be implemented here
|
||||
logger.info(`Player ${player.userId} upgrading ship`);
|
||||
return { success: true, message: 'Ship upgraded' };
|
||||
}
|
||||
|
||||
async handleItemPurchase(player, data) {
|
||||
// Item purchase logic will be implemented here
|
||||
logger.info(`Player ${player.userId} purchasing item`);
|
||||
return { success: true, message: 'Item purchased' };
|
||||
}
|
||||
|
||||
async handleDailyReward(player, data) {
|
||||
// Daily reward logic will be implemented here
|
||||
logger.info(`Player ${player.userId} claiming daily reward`);
|
||||
return { success: true, message: 'Daily reward claimed' };
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let gameSystem = null;
|
||||
|
||||
async function initializeGameSystems() {
|
||||
if (!gameSystem) {
|
||||
gameSystem = new GameSystem();
|
||||
try {
|
||||
await gameSystem.initializeGameSystems();
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize game systems:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
function getGameSystem() {
|
||||
if (!gameSystem) {
|
||||
logger.warn('Game system not initialized. Call initializeGameSystems() first.');
|
||||
return null;
|
||||
}
|
||||
return gameSystem;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GameSystem,
|
||||
initializeGameSystems,
|
||||
getGameSystem
|
||||
};
|
||||
220
Galaxy-Strike-Online-main/API/tests/api.test.js
Normal file
@ -0,0 +1,220 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const app = require('../server');
|
||||
const Player = require('../models/Player');
|
||||
|
||||
describe('API Tests', () => {
|
||||
let token;
|
||||
let testUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Connect to test database
|
||||
const mongoUri = process.env.MONGODB_TEST_URI || 'mongodb://localhost:27017/galaxystrikeonline_test';
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up and close connection
|
||||
await Player.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up before each test
|
||||
await Player.deleteMany({});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('POST /api/auth/register - should register new user', async () => {
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
expect(response.body.user).toHaveProperty('email', userData.email);
|
||||
});
|
||||
|
||||
test('POST /api/auth/login - should login existing user', async () => {
|
||||
// First register a user
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
// Then login
|
||||
const loginData = {
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
token = response.body.token;
|
||||
testUser = response.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/auth/verify - should verify token', async () => {
|
||||
// First login to get token
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const registerResponse = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData);
|
||||
|
||||
token = registerResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('valid', true);
|
||||
expect(response.body.user).toHaveProperty('username', userData.username);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for game tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/game/player - should get player data', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/player')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('userId');
|
||||
expect(response.body).toHaveProperty('stats');
|
||||
expect(response.body).toHaveProperty('attributes');
|
||||
});
|
||||
|
||||
test('GET /api/game/ships - should get player ships', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/ships')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('ships');
|
||||
expect(Array.isArray(response.body.ships)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/game/inventory - should get player inventory', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/game/inventory')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('items');
|
||||
expect(response.body).toHaveProperty('summary');
|
||||
});
|
||||
|
||||
test('POST /api/game/daily-reward - should claim daily reward', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/game/daily-reward')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server API', () => {
|
||||
beforeEach(async () => {
|
||||
// Create and login a user for server tests
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
});
|
||||
|
||||
token = loginResponse.body.token;
|
||||
testUser = loginResponse.body.user;
|
||||
});
|
||||
|
||||
test('GET /api/servers - should get server list', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/servers')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('servers');
|
||||
expect(response.body).toHaveProperty('totalServers');
|
||||
expect(Array.isArray(response.body.servers)).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /api/servers/create - should create new server', async () => {
|
||||
const serverData = {
|
||||
name: 'Test Server',
|
||||
type: 'public',
|
||||
maxPlayers: 10,
|
||||
region: 'us-east'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/servers/create')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send(serverData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.server).toHaveProperty('name', serverData.name);
|
||||
expect(response.body.server).toHaveProperty('type', serverData.type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check', () => {
|
||||
test('GET /health - should return health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('status', 'OK');
|
||||
expect(response.body).toHaveProperty('timestamp');
|
||||
expect(response.body).toHaveProperty('uptime');
|
||||
});
|
||||
});
|
||||
});
|
||||
27
Galaxy-Strike-Online-main/API/utils/logger.js
Normal 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;
|
||||
120
Galaxy-Strike-Online-main/Client/data/starbase-layout.json
Normal 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 }
|
||||
}
|
||||
3793
Galaxy-Strike-Online-main/Client/index.html
Normal 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
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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,
|
||||
905
Galaxy-Strike-Online-main/Client/js/core/Economy.js
Normal 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;
|
||||
}
|
||||
753
Galaxy-Strike-Online-main/Client/js/core/GameEngine.js
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
125
Galaxy-Strike-Online-main/Client/js/data/GameData.js
Normal 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
|
||||
};
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
403
Galaxy-Strike-Online-main/Client/js/systems/CraftingSystem.js
Normal 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;
|
||||
}
|
||||
832
Galaxy-Strike-Online-main/Client/js/systems/DungeonSystem.js
Normal 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;
|
||||
}
|
||||
@ -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) {
|
||||
468
Galaxy-Strike-Online-main/Client/js/systems/ItemSystem.js
Normal 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;
|
||||
}
|
||||
@ -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 = '';
|
||||
|
||||
@ -61,6 +61,22 @@ class ShipSystem {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ship image URL from server or local
|
||||
*/
|
||||
getShipImageUrl(ship) {
|
||||
if (!ship) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||
|
||||
// For multiplayer, get from server
|
||||
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||
return `${serverUrl}/images/ships/${ship.id}.png`;
|
||||
}
|
||||
|
||||
// For singleplayer, use local path
|
||||
return ship.image || ship.texture || 'assets/textures/ships/starter_cruiser.png';
|
||||
}
|
||||
|
||||
createShipCard(ship) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `ship-card ${ship.status === 'active' ? 'active' : ''}`;
|
||||
@ -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';
|
||||
407
Galaxy-Strike-Online-main/Client/js/systems/SkillSystem.js
Normal 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(); }
|
||||
}
|
||||
1001
Galaxy-Strike-Online-main/Client/js/systems/StarbaseWorld.js
Normal 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
|
||||
|
||||
@ -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
|
||||
1
Galaxy-Strike-Online-main/Client/locales/de.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"de","name":"de","status":"placeholder - community translation needed"}}
|
||||
58
Galaxy-Strike-Online-main/Client/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/es.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"es","name":"es","status":"placeholder - community translation needed"}}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/fr.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"fr","name":"fr","status":"placeholder - community translation needed"}}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/ja.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"ja","name":"ja","status":"placeholder - community translation needed"}}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/ko.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"ko","name":"ko","status":"placeholder - community translation needed"}}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/pt.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"pt","name":"pt","status":"placeholder - community translation needed"}}
|
||||
1
Galaxy-Strike-Online-main/Client/locales/zh.json
Normal file
@ -0,0 +1 @@
|
||||
{"_meta":{"lang":"zh","name":"zh","status":"placeholder - community translation needed"}}
|
||||
216
Galaxy-Strike-Online-main/Client/styles/components.css
Normal 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)}}
|
||||
}
|
||||
@ -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);
|
||||
606
Galaxy-Strike-Online-main/Client/styles/main.css
Normal 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)}
|
||||
}
|
||||
@ -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;
|
||||
185
Galaxy-Strike-Online-main/Client/styles/tables.css
Normal 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}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |