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 }
|
||||||
|
}
|
||||||
1408
Client/index.html
@ -70,13 +70,13 @@ class GameInitializer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE THE URL - Override any undefined issues
|
// Resolve server URL: use serverData.url if set, otherwise gameServerUrl, then dev default
|
||||||
const FORCED_URL = 'https://dev.gameserver.galaxystrike.online';
|
const FORCED_URL = (this.serverData && this.serverData.url)
|
||||||
console.log('[GAME INITIALIZER] FORCING URL to:', FORCED_URL);
|
? this.serverData.url
|
||||||
console.log('[GAME INITIALIZER] Original this.gameServerUrl:', this.gameServerUrl);
|
: (this.gameServerUrl || 'https://dev.gameserver.galaxystrike.online');
|
||||||
console.log('[GAME INITIALIZER] Using remote development server');
|
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, {
|
this.socket = io(FORCED_URL, {
|
||||||
auth: {
|
auth: {
|
||||||
token: this.authToken,
|
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
|
// Socket event handlers
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
|
|||||||
@ -636,24 +636,11 @@ class Economy {
|
|||||||
|
|
||||||
console.log('[ECONOMY] updateShopUI called');
|
console.log('[ECONOMY] updateShopUI called');
|
||||||
|
|
||||||
if (this.game.multiplayerMode && this.game.itemSystem && this.game.itemSystem.catalog) {
|
if (this.game.multiplayerMode && this.game.itemSystem) {
|
||||||
console.log('[ECONOMY] Multiplayer mode:', true);
|
// Support both .catalog getter (new) and .shopItemsByCategory (legacy)
|
||||||
console.log('[ECONOMY] ItemSystem available:', !!this.game.itemSystem);
|
const shopItems = this.game.itemSystem.catalog || this.game.itemSystem.shopItemsByCategory || {};
|
||||||
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
|
|
||||||
const activeCategory = this.game.itemSystem.activeCategory || 'ships';
|
const activeCategory = this.game.itemSystem.activeCategory || 'ships';
|
||||||
console.log('[ECONOMY] Active shop category:', activeCategory);
|
|
||||||
|
|
||||||
// Filter items for active category
|
|
||||||
const categoryItems = shopItems[activeCategory] || [];
|
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);
|
this.renderShopItems(categoryItems);
|
||||||
} else {
|
} else {
|
||||||
// Singleplayer mode - use local shop data
|
// Singleplayer mode - use local shop data
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Galaxy Strike Online - Game Data
|
* 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
|
// Game configuration
|
||||||
@ -13,7 +14,7 @@ const GAME_CONFIG = {
|
|||||||
maxNotifications: 5
|
maxNotifications: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player defaults
|
// Player defaults (used only for initial UI state before server data arrives)
|
||||||
const PLAYER_DEFAULTS = {
|
const PLAYER_DEFAULTS = {
|
||||||
level: 1,
|
level: 1,
|
||||||
experience: 0,
|
experience: 0,
|
||||||
@ -31,522 +32,81 @@ const PLAYER_DEFAULTS = {
|
|||||||
criticalDamage: 1.5
|
criticalDamage: 1.5
|
||||||
};
|
};
|
||||||
|
|
||||||
// Experience requirements
|
// Experience requirements (client-side display only; server is authoritative)
|
||||||
const EXPERIENCE_TABLE = [];
|
const EXPERIENCE_TABLE = [];
|
||||||
for (let i = 1; i <= 100; i++) {
|
for (let i = 1; i <= 100; i++) {
|
||||||
EXPERIENCE_TABLE[i] = Math.floor(100 * Math.pow(1.5, i - 1));
|
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 = {
|
const ITEM_RARITIES = {
|
||||||
common: {
|
common: { name: 'Common', color: '#888888', multiplier: 1.0 },
|
||||||
name: 'Common',
|
uncommon: { name: 'Uncommon', color: '#00ff00', multiplier: 1.2 },
|
||||||
color: '#888888',
|
rare: { name: 'Rare', color: '#0088ff', multiplier: 1.5 },
|
||||||
multiplier: 1.0,
|
epic: { name: 'Epic', color: '#8833ff', multiplier: 2.0 },
|
||||||
dropChance: 0.60
|
legendary: { name: 'Legendary', color: '#ff8800', multiplier: 3.0 }
|
||||||
},
|
|
||||||
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
|
// Game messages and notifications
|
||||||
const GAME_MESSAGES = {
|
const GAME_MESSAGES = {
|
||||||
welcome: 'Welcome to Galaxy Strike Online, Commander!',
|
welcome: 'Welcome to Galaxy Strike Online, Commander!',
|
||||||
levelUp: 'Level Up! You are now level {level}!',
|
levelUp: 'Level Up! You are now level {level}!',
|
||||||
questCompleted: 'Quest completed: {questName}!',
|
questCompleted: 'Quest completed: {questName}!',
|
||||||
dungeonCompleted: 'Dungeon completed! Time: {time}',
|
dungeonCompleted: 'Dungeon completed! Time: {time}',
|
||||||
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
|
achievementUnlocked: 'Achievement Unlocked: {achievementName}!',
|
||||||
insufficientCredits: 'Not enough credits!',
|
insufficientCredits: 'Not enough credits!',
|
||||||
insufficientGems: 'Not enough gems!',
|
insufficientGems: 'Not enough gems!',
|
||||||
insufficientEnergy: 'Not enough energy!',
|
insufficientEnergy: 'Not enough energy!',
|
||||||
inventoryFull: 'Inventory is full!',
|
inventoryFull: 'Inventory is full!',
|
||||||
skillPointsAvailable: 'You have {points} skill points available!',
|
skillPointsAvailable: 'You have {points} skill points available!',
|
||||||
dailyReward: 'Daily reward claimed! Day {day}',
|
dailyReward: 'Daily reward claimed! Day {day}',
|
||||||
offlineRewards: 'Welcome back! You were offline for {time}'
|
offlineRewards: 'Welcome back! You were offline for {time}'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const GameUtils = {
|
const GameUtils = {
|
||||||
// Get random item from array
|
|
||||||
getRandomItem(array) {
|
getRandomItem(array) {
|
||||||
return array[Math.floor(Math.random() * array.length)];
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get random integer between min and max (inclusive)
|
|
||||||
getRandomInt(min, max) {
|
getRandomInt(min, max) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get random float between min and max
|
|
||||||
getRandomFloat(min, max) {
|
getRandomFloat(min, max) {
|
||||||
return Math.random() * (max - min) + min;
|
return Math.random() * (max - min) + min;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if chance succeeds
|
|
||||||
checkChance(chance) {
|
checkChance(chance) {
|
||||||
return Math.random() < chance;
|
return Math.random() < chance;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Format large numbers with suffixes
|
|
||||||
formatNumber(num) {
|
formatNumber(num) {
|
||||||
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
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();
|
return Math.floor(num).toString();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Format time in milliseconds to readable string
|
|
||||||
formatTime(milliseconds) {
|
formatTime(milliseconds) {
|
||||||
const seconds = Math.floor(milliseconds / 1000);
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
if (days > 0) return `${days}d ${hours % 24}h`;
|
if (days > 0) return `${days}d ${hours % 24}h`;
|
||||||
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||||
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||||
return `${seconds}s`;
|
return `${seconds}s`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Calculate experience needed for level
|
|
||||||
getExperienceForLevel(level) {
|
getExperienceForLevel(level) {
|
||||||
return EXPERIENCE_TABLE[level] || 0;
|
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) {
|
deepClone(obj) {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Generate unique ID
|
|
||||||
generateId() {
|
generateId() {
|
||||||
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
||||||
}
|
}
|
||||||
@ -559,11 +119,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
PLAYER_DEFAULTS,
|
PLAYER_DEFAULTS,
|
||||||
EXPERIENCE_TABLE,
|
EXPERIENCE_TABLE,
|
||||||
ITEM_RARITIES,
|
ITEM_RARITIES,
|
||||||
ENEMY_TEMPLATES,
|
|
||||||
DUNGEON_CONFIGS,
|
|
||||||
SKILL_DEFINITIONS,
|
|
||||||
SHOP_ITEMS,
|
|
||||||
ACHIEVEMENTS,
|
|
||||||
GAME_MESSAGES,
|
GAME_MESSAGES,
|
||||||
GameUtils
|
GameUtils
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1026,8 +1026,15 @@ class BaseSystem {
|
|||||||
} else if (view === 'ships') {
|
} else if (view === 'ships') {
|
||||||
this.updateShipGallery();
|
this.updateShipGallery();
|
||||||
} else if (view === 'starbases') {
|
} else if (view === 'starbases') {
|
||||||
this.updateStarbaseList();
|
// Boot the isometric starbase world instead of the old list UI
|
||||||
this.updateStarbasePurchaseList();
|
if (typeof _startStarbaseWorld === 'function') {
|
||||||
|
_startStarbaseWorld();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Leaving starbases — stop the world loop
|
||||||
|
if (typeof _stopStarbaseWorld === 'function') {
|
||||||
|
_stopStarbaseWorld();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,651 +1,397 @@
|
|||||||
/**
|
/**
|
||||||
* Galaxy Strike Online - Crafting System
|
* Galaxy Strike Online - Client Crafting System
|
||||||
* Handles item crafting, recipes, and crafting skill progression
|
* Recipe definitions are loaded from the server; this file handles
|
||||||
|
* local crafting logic, requirement checking, and UI rendering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class CraftingSystem extends BaseSystem {
|
class CraftingSystem extends BaseSystem {
|
||||||
constructor(gameEngine) {
|
constructor(gameEngine) {
|
||||||
super(gameEngine);
|
super(gameEngine);
|
||||||
|
|
||||||
this.recipes = new Map();
|
this.recipes = new Map(); // recipeId -> recipe object
|
||||||
this.currentCategory = 'weapons';
|
this.currentCategory = 'weapons';
|
||||||
this.selectedRecipe = null;
|
this.selectedRecipe = null;
|
||||||
|
|
||||||
this.initializeRecipes();
|
this._loaded = false;
|
||||||
|
this._loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeRecipes() {
|
// ------------------------------------------------------------------ //
|
||||||
// Weapon Recipes
|
// Initialisation — request recipes from the server
|
||||||
this.addRecipe('basic_blaster', {
|
// ------------------------------------------------------------------ //
|
||||||
name: 'Basic Blaster',
|
async initialize() {
|
||||||
category: 'weapons',
|
if (this._loaded || this._loading) return;
|
||||||
description: 'A simple energy blaster for beginners',
|
this._loading = true;
|
||||||
requirements: {
|
|
||||||
weapon_crafting: 1,
|
console.log('[CRAFTING SYSTEM] Requesting recipes from server');
|
||||||
crafting: 1
|
|
||||||
},
|
if (!window.game?.socket) {
|
||||||
materials: [
|
console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected');
|
||||||
{ id: 'iron_ore', quantity: 5 },
|
this._loading = false;
|
||||||
{ id: 'energy_crystal', quantity: 2 }
|
return;
|
||||||
],
|
}
|
||||||
results: [
|
|
||||||
{ id: 'basic_blaster', quantity: 1 }
|
try {
|
||||||
],
|
const recipes = await this._fetchRecipesFromServer();
|
||||||
experience: 10,
|
this._applyServerRecipes(recipes);
|
||||||
craftingTime: 3000 // 3 seconds
|
this._loaded = true;
|
||||||
});
|
console.log(`[CRAFTING SYSTEM] Loaded ${this.recipes.size} recipes from server`);
|
||||||
|
} catch (err) {
|
||||||
this.addRecipe('enhanced_blaster', {
|
console.error('[CRAFTING SYSTEM] Failed to load recipes from server:', err);
|
||||||
name: 'Enhanced Blaster',
|
} finally {
|
||||||
category: 'weapons',
|
this._loading = false;
|
||||||
description: 'An improved blaster with better damage output',
|
}
|
||||||
requirements: {
|
}
|
||||||
weapon_crafting: 3,
|
|
||||||
crafting: 5
|
_fetchRecipesFromServer() {
|
||||||
},
|
return new Promise((resolve, reject) => {
|
||||||
materials: [
|
const socket = window.game.socket;
|
||||||
{ id: 'iron_ore', quantity: 10 },
|
|
||||||
{ id: 'energy_crystal', quantity: 5 },
|
const timeout = setTimeout(() => {
|
||||||
{ id: 'copper_wire', quantity: 3 }
|
socket.off('recipes_data', handler);
|
||||||
],
|
reject(new Error('Recipe data request timed out'));
|
||||||
results: [
|
}, 10000);
|
||||||
{ id: 'enhanced_blaster', quantity: 1 }
|
|
||||||
],
|
const handler = (data) => {
|
||||||
experience: 25,
|
clearTimeout(timeout);
|
||||||
craftingTime: 5000 // 5 seconds
|
socket.off('recipes_data', handler);
|
||||||
});
|
if (data && (Array.isArray(data) || typeof data === 'object')) {
|
||||||
|
resolve(data);
|
||||||
this.addRecipe('laser_sniper_rifle', {
|
} else {
|
||||||
name: 'Laser Sniper Rifle',
|
reject(new Error('Invalid recipe data from server'));
|
||||||
category: 'weapons',
|
}
|
||||||
description: 'A long-range precision laser weapon',
|
};
|
||||||
requirements: {
|
|
||||||
weapon_crafting: 3,
|
socket.on('recipes_data', handler);
|
||||||
crafting: 5
|
socket.emit('get_recipes');
|
||||||
},
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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) {
|
addRecipe(id, recipe) {
|
||||||
recipe.id = id;
|
recipe.id = id;
|
||||||
recipe.unlocked = false;
|
recipe.unlocked = false;
|
||||||
this.recipes.set(id, recipe);
|
this.recipes.set(id, recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
// Check for newly unlocked recipes
|
|
||||||
this.checkRecipeUnlocks();
|
this.checkRecipeUnlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkRecipeUnlocks() {
|
checkRecipeUnlocks() {
|
||||||
const skillSystem = this.game.systems.skillSystem;
|
const skillSystem = this.game.systems.skillSystem;
|
||||||
if (!skillSystem) return;
|
if (!skillSystem) return;
|
||||||
|
|
||||||
for (const [id, recipe] of this.recipes) {
|
for (const [id, recipe] of this.recipes) {
|
||||||
if (!recipe.unlocked) {
|
if (recipe.unlocked) continue;
|
||||||
let canCraft = true;
|
|
||||||
|
let canUnlock = true;
|
||||||
// Check skill requirements
|
if (recipe.requirements) {
|
||||||
if (recipe.requirements) {
|
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
if (skillSystem.getSkillLevel(skillName) < requiredLevel) {
|
||||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
canUnlock = false;
|
||||||
if (skillLevel < requiredLevel) {
|
break;
|
||||||
canCraft = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (canCraft) {
|
|
||||||
recipe.unlocked = true;
|
if (canUnlock) {
|
||||||
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
|
recipe.unlocked = true;
|
||||||
}
|
console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecipesByCategory(category) {
|
getRecipesByCategory(category) {
|
||||||
return Array.from(this.recipes.values())
|
return Array.from(this.recipes.values())
|
||||||
.filter(recipe => recipe.category === category);
|
.filter(r => r.category === category || r.type === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
canCraftRecipe(recipeId) {
|
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;
|
if (!recipe) return false;
|
||||||
|
|
||||||
// Check skill requirements
|
if (recipe.requirements && skillSystem) {
|
||||||
if (recipe.requirements) {
|
|
||||||
const skillSystem = this.game.systems.skillSystem;
|
|
||||||
if (!skillSystem) return false;
|
|
||||||
|
|
||||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
||||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false;
|
||||||
if (skillLevel < requiredLevel) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check materials
|
if (recipe.materials && inventory) {
|
||||||
if (recipe.materials) {
|
for (const mat of recipe.materials) {
|
||||||
for (const material of recipe.materials) {
|
if (!inventory.hasItem(mat.id, mat.quantity)) return false;
|
||||||
const inventory = this.game.systems.inventory;
|
|
||||||
if (!inventory || !inventory.hasItem(material.id, material.quantity)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMissingMaterials(recipeId) {
|
getMissingMaterials(recipeId) {
|
||||||
const recipe = this.recipes.get(recipeId);
|
const recipe = this.recipes.get(recipeId);
|
||||||
if (!recipe || !recipe.materials) return [];
|
|
||||||
|
|
||||||
const missing = [];
|
|
||||||
const inventory = this.game.systems.inventory;
|
const inventory = this.game.systems.inventory;
|
||||||
|
if (!recipe?.materials) return [];
|
||||||
console.log(`[CRAFTING DEBUG] Checking materials for recipe: ${recipe.name}`);
|
|
||||||
console.log(`[CRAFTING DEBUG] Inventory system:`, inventory);
|
const missing = [];
|
||||||
|
for (const mat of recipe.materials) {
|
||||||
for (const material of recipe.materials) {
|
let current = 0;
|
||||||
let currentCount = 0;
|
if (inventory?.getItemCount) {
|
||||||
|
try { current = inventory.getItemCount(mat.id) || 0; } catch (_) {}
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
const required = mat.quantity || 0;
|
||||||
// Ensure required quantity is also a valid number
|
if (current < required) {
|
||||||
const requiredQuantity = typeof material.quantity === 'number' && !isNaN(material.quantity) ? material.quantity : 0;
|
missing.push({ id: mat.id, required, current, missing: required - current });
|
||||||
|
|
||||||
// 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;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async craftRecipe(recipeId) {
|
async craftRecipe(recipeId) {
|
||||||
const recipe = this.recipes.get(recipeId);
|
const recipe = this.recipes.get(recipeId);
|
||||||
if (!recipe) {
|
if (!recipe || !this.canCraftRecipe(recipeId)) return false;
|
||||||
console.error(`[CRAFTING] Recipe not found: ${recipeId}`);
|
|
||||||
return false;
|
console.log(`[CRAFTING] Crafting: ${recipe.name}`);
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (recipe.materials) {
|
||||||
for (const material of recipe.materials) {
|
for (const mat of recipe.materials) {
|
||||||
this.game.systems.inventory.removeItem(material.id, material.quantity);
|
this.game.systems.inventory.removeItem(mat.id, mat.quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add crafting experience
|
if (recipe.experience && this.game.systems.skillSystem) {
|
||||||
if (recipe.experience) {
|
|
||||||
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
|
this.game.systems.skillSystem.awardCraftingExperience(recipe.experience);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for crafting time
|
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000));
|
||||||
await new Promise(resolve => setTimeout(resolve, recipe.craftingTime));
|
|
||||||
|
|
||||||
// Add results to inventory
|
|
||||||
if (recipe.results) {
|
if (recipe.results) {
|
||||||
for (const result of recipe.results) {
|
for (const result of recipe.results) {
|
||||||
this.game.systems.inventory.addItem(result.id, result.quantity);
|
this.game.systems.inventory.addItem(result.id, result.quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update quest progress
|
|
||||||
if (this.game.systems.questSystem) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectRecipe(recipeId) {
|
selectRecipe(recipeId) {
|
||||||
this.selectedRecipe = this.recipes.get(recipeId);
|
this.selectedRecipe = this.recipes.get(recipeId);
|
||||||
return this.selectedRecipe;
|
return this.selectedRecipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedRecipe() {
|
getSelectedRecipe() { return this.selectedRecipe; }
|
||||||
return this.selectedRecipe;
|
|
||||||
}
|
// ------------------------------------------------------------------ //
|
||||||
|
// UI
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
updateUI() {
|
updateUI() {
|
||||||
this.updateRecipeList();
|
this.updateRecipeList();
|
||||||
this.updateCraftingDetails();
|
this.updateCraftingDetails();
|
||||||
this.updateCraftingInfo();
|
this.updateCraftingInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRecipeList() {
|
updateRecipeList() {
|
||||||
const recipeListElement = document.getElementById('recipeList');
|
const listEl = document.getElementById('recipeList');
|
||||||
if (!recipeListElement) return;
|
if (!listEl) return;
|
||||||
|
|
||||||
const recipes = this.getRecipesByCategory(this.currentCategory);
|
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>';
|
||||||
recipeListElement.innerHTML = '';
|
|
||||||
|
|
||||||
if (recipes.length === 0) {
|
|
||||||
recipeListElement.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
|
|
||||||
return;
|
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 => {
|
recipes.forEach(recipe => {
|
||||||
const recipeElement = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
recipeElement.className = 'recipe-item';
|
el.className = 'recipe-item';
|
||||||
recipeElement.dataset.recipeId = recipe.id;
|
el.dataset.recipeId = recipe.id;
|
||||||
|
|
||||||
const canCraft = this.canCraftRecipe(recipe.id);
|
const canCraft = this.canCraftRecipe(recipe.id);
|
||||||
const missingMaterials = this.getMissingMaterials(recipe.id);
|
const missingMats = this.getMissingMaterials(recipe.id);
|
||||||
|
const skillSystem = this.game.systems.skillSystem;
|
||||||
// Check if recipe is unlocked (skill requirements met)
|
let skillsMet = true;
|
||||||
const skillSystem = this.game.systems.skillSystem;
|
|
||||||
let skillRequirementsMet = true;
|
|
||||||
if (recipe.requirements && skillSystem) {
|
if (recipe.requirements && skillSystem) {
|
||||||
for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) {
|
for (const [skill, level] of Object.entries(recipe.requirements)) {
|
||||||
const skillLevel = skillSystem.getSkillLevel(skillName);
|
if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; }
|
||||||
if (skillLevel < requiredLevel) {
|
|
||||||
skillRequirementsMet = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply styling based on status
|
if (!skillsMet) el.classList.add('locked');
|
||||||
if (!skillRequirementsMet) {
|
else if (!canCraft) el.classList.add('missing-materials');
|
||||||
recipeElement.classList.add('locked');
|
else el.classList.add('can-craft');
|
||||||
} else if (!canCraft) {
|
|
||||||
recipeElement.classList.add('missing-materials');
|
const reqText = recipe.requirements
|
||||||
} else {
|
? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).join(', ')
|
||||||
recipeElement.classList.add('can-craft');
|
: 'None';
|
||||||
}
|
|
||||||
|
const matsHtml = recipe.materials.map(mat => {
|
||||||
// Generate requirements text
|
const mis = missingMats.find(m => m.id === mat.id);
|
||||||
const requirementsText = recipe.requirements ?
|
const cur = mis ? mis.current : (this.game.systems.inventory?.getItemCount(mat.id) || 0);
|
||||||
Object.entries(recipe.requirements).map(([skill, level]) => `${skill}: ${level}`).join(', ') : 'None';
|
const cls = mis ? 'material-item missing' : 'material-item';
|
||||||
|
return `<div class="${cls}">
|
||||||
// Generate materials with missing status
|
<span class="material-name">${mat.id}</span>
|
||||||
const materialsHtml = recipe.materials ? recipe.materials.map(mat => {
|
<span class="material-quantity">${cur}/${mat.quantity}</span>
|
||||||
const missing = missingMaterials.find(m => m.id === mat.id);
|
</div>`;
|
||||||
const currentCount = missing ? missing.current : 0;
|
}).join('');
|
||||||
const requiredCount = mat.quantity || 0;
|
|
||||||
|
el.innerHTML = `
|
||||||
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">
|
<div class="recipe-header">
|
||||||
<h4>${recipe.name}</h4>
|
<h4>${recipe.name}</h4>
|
||||||
<span class="recipe-level">Level ${requirementsText}</span>
|
<span class="recipe-level">Level ${reqText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="recipe-description">${recipe.description}</div>
|
<div class="recipe-description">${recipe.description || ''}</div>
|
||||||
<div class="recipe-materials">
|
<div class="recipe-materials">${matsHtml}</div>
|
||||||
${materialsHtml}
|
${missingMats.length > 0 ? `
|
||||||
</div>
|
|
||||||
${missingMaterials.length > 0 ? `
|
|
||||||
<div class="missing-materials-text">
|
<div class="missing-materials-text">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
Missing: ${missingMaterials.map(m => `${m.missing}x ${m.id}`).join(', ')}
|
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
|
||||||
</div>
|
</div>` : ''}
|
||||||
` : ''}
|
|
||||||
<div class="recipe-time">
|
<div class="recipe-time">
|
||||||
<i class="fas fa-clock"></i>
|
<i class="fas fa-clock"></i>
|
||||||
<span>${recipe.craftingTime / 1000}s</span>
|
<span>${(recipe.craftingTime || 0) / 1000}s</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
recipeElement.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
this.selectRecipe(recipe.id);
|
this.selectRecipe(recipe.id);
|
||||||
this.updateCraftingDetails();
|
this.updateCraftingDetails();
|
||||||
});
|
});
|
||||||
|
|
||||||
recipeListElement.appendChild(recipeElement);
|
listEl.appendChild(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCraftingDetails() {
|
updateCraftingDetails() {
|
||||||
const detailsElement = document.getElementById('craftingDetails');
|
const detailsEl = document.getElementById('craftingDetails');
|
||||||
if (!detailsElement) return;
|
if (!detailsEl) return;
|
||||||
|
|
||||||
if (!this.selectedRecipe) {
|
if (!this.selectedRecipe) {
|
||||||
detailsElement.innerHTML = `
|
detailsEl.innerHTML = `
|
||||||
<div class="selected-recipe">
|
<div class="selected-recipe">
|
||||||
<h3>Select a Recipe</h3>
|
<h3>Select a Recipe</h3>
|
||||||
<p>Choose a recipe from the list to see details and craft items.</p>
|
<p>Choose a recipe from the list to see details and craft items.</p>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipe = this.selectedRecipe;
|
const recipe = this.selectedRecipe;
|
||||||
const canCraft = this.canCraftRecipe(recipe.id);
|
const canCraft = this.canCraftRecipe(recipe.id);
|
||||||
|
|
||||||
detailsElement.innerHTML = `
|
detailsEl.innerHTML = `
|
||||||
<div class="selected-recipe">
|
<div class="selected-recipe">
|
||||||
<h3>${recipe.name}</h3>
|
<h3>${recipe.name}</h3>
|
||||||
<p class="recipe-description">${recipe.description}</p>
|
<p class="recipe-description">${recipe.description || ''}</p>
|
||||||
|
|
||||||
<div class="recipe-requirements">
|
<div class="recipe-requirements">
|
||||||
<h4>Requirements:</h4>
|
<h4>Requirements:</h4>
|
||||||
${recipe.requirements ? Object.entries(recipe.requirements).map(([skill, level]) =>
|
${recipe.requirements
|
||||||
`<div class="requirement-item">
|
? Object.entries(recipe.requirements).map(([s, l]) =>
|
||||||
<span class="skill-name">${skill}</span>
|
`<div class="requirement-item">
|
||||||
<span class="skill-level">Level ${level}</span>
|
<span class="skill-name">${s}</span>
|
||||||
</div>`
|
<span class="skill-level">Level ${l}</span>
|
||||||
).join('') : '<p>No special requirements</p>'}
|
</div>`).join('')
|
||||||
|
: '<p>No special requirements</p>'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recipe-materials-needed">
|
<div class="recipe-materials-needed">
|
||||||
<h4>Materials Needed:</h4>
|
<h4>Materials Needed:</h4>
|
||||||
${recipe.materials ? recipe.materials.map(mat =>
|
${recipe.materials.map(mat =>
|
||||||
`<div class="material-needed">
|
`<div class="material-needed">
|
||||||
<span class="material-name">${mat.id}</span>
|
<span class="material-name">${mat.id}</span>
|
||||||
<span class="material-needed">x${mat.quantity}</span>
|
<span class="material-needed">x${mat.quantity}</span>
|
||||||
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
|
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
|
||||||
</div>`
|
</div>`).join('')}
|
||||||
).join('') : '<p>No materials needed</p>'}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recipe-results">
|
<div class="recipe-results">
|
||||||
<h4>Results:</h4>
|
<h4>Results:</h4>
|
||||||
${recipe.results ? recipe.results.map(result =>
|
${recipe.results.map(r =>
|
||||||
`<div class="result-item">
|
`<div class="result-item">
|
||||||
<span class="result-name">${result.id}</span>
|
<span class="result-name">${r.id}</span>
|
||||||
<span class="result-quantity">x${result.quantity}</span>
|
<span class="result-quantity">x${r.quantity}</span>
|
||||||
</div>`
|
</div>`).join('')}
|
||||||
).join('') : ''}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recipe-info">
|
<div class="recipe-info">
|
||||||
<div class="experience-reward">
|
<div class="experience-reward">
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star"></i>
|
||||||
<span>${recipe.experience} XP</span>
|
<span>${recipe.experience || 0} XP</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="crafting-time">
|
<div class="crafting-time">
|
||||||
<i class="fas fa-clock"></i>
|
<i class="fas fa-clock"></i>
|
||||||
<span>${recipe.craftingTime / 1000} seconds</span>
|
<span>${(recipe.craftingTime || 0) / 1000} seconds</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
|
||||||
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
|
|
||||||
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
|
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
|
||||||
${canCraft ? 'Craft Item' : 'Cannot Craft'}
|
${canCraft ? 'Craft Item' : 'Cannot Craft'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCraftingInfo() {
|
updateCraftingInfo() {
|
||||||
const skillSystem = this.game.systems.skillSystem;
|
const skillSystem = this.game.systems.skillSystem;
|
||||||
if (!skillSystem) return;
|
if (!skillSystem) return;
|
||||||
|
|
||||||
const craftingLevel = skillSystem.getSkillLevel('crafting');
|
const craftingLevel = skillSystem.getSkillLevel('crafting');
|
||||||
const craftingExp = skillSystem.getSkillExperience('crafting');
|
const craftingExp = skillSystem.getSkillExperience('crafting');
|
||||||
const expNeeded = skillSystem.getExperienceNeeded('crafting');
|
const expNeeded = skillSystem.getExperienceNeeded('crafting');
|
||||||
|
|
||||||
const levelElement = document.getElementById('craftingLevel');
|
const levelEl = document.getElementById('craftingLevel');
|
||||||
const expElement = document.getElementById('craftingExp');
|
const expEl = document.getElementById('craftingExp');
|
||||||
|
if (levelEl) levelEl.textContent = craftingLevel;
|
||||||
if (levelElement) levelElement.textContent = craftingLevel;
|
if (expEl) expEl.textContent = `${craftingExp}/${expNeeded}`;
|
||||||
if (expElement) expElement.textContent = `${craftingExp}/${expNeeded}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switchCategory(category) {
|
switchCategory(category) {
|
||||||
this.currentCategory = category;
|
this.currentCategory = category;
|
||||||
this.selectedRecipe = null;
|
this.selectedRecipe = null;
|
||||||
|
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||||
// Update UI only if in multiplayer mode or game is actively running
|
|
||||||
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
|
|
||||||
|
|
||||||
if (shouldUpdateUI) {
|
|
||||||
this.updateUI();
|
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
|
* Get system statistics
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,504 +1,330 @@
|
|||||||
/**
|
/**
|
||||||
* Galaxy Strike Online - Skill System
|
* Galaxy Strike Online - Client Skill System
|
||||||
* Manages skills, progression, and specialization
|
* Skill definitions are loaded from the server; this file handles
|
||||||
|
* local progression tracking, UI rendering, and skill-point spending.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class SkillSystem {
|
class SkillSystem {
|
||||||
constructor(gameEngine) {
|
constructor(gameEngine) {
|
||||||
this.game = gameEngine;
|
this.game = gameEngine;
|
||||||
|
|
||||||
// Skill categories
|
// Populated after server responds to 'get_skills'
|
||||||
|
this.skills = { combat: {}, science: {}, crafting: {} };
|
||||||
|
|
||||||
this.categories = {
|
this.categories = {
|
||||||
combat: 'Combat',
|
combat: 'Combat',
|
||||||
science: 'Science',
|
science: 'Science',
|
||||||
crafting: 'Crafting'
|
crafting: 'Crafting'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skill definitions
|
this.experienceRates = { combat: 1.0, science: 0.8, crafting: 0.6 };
|
||||||
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 = {};
|
this.activeBuffs = {};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
this._loaded = false;
|
||||||
|
this._loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Initialisation — request skill definitions from the server
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
async initialize() {
|
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) {
|
addSkillExperience(category, skillId, amount) {
|
||||||
const skill = this.skills[category]?.[skillId];
|
const skill = this.skills[category]?.[skillId];
|
||||||
if (!skill || skill.currentLevel >= skill.maxLevel) {
|
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
skill.experience += amount;
|
skill.experience += amount;
|
||||||
|
|
||||||
// Check for level up
|
|
||||||
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
|
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
|
||||||
this.levelUpSkill(category, skillId);
|
this.levelUpSkill(category, skillId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applySkillEffects();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unlockSkill(category, skillId) {
|
levelUpSkill(category, skillId) {
|
||||||
const skill = this.skills[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;
|
const player = this.game.systems.player;
|
||||||
|
|
||||||
if (!skill) {
|
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
|
||||||
this.game.showNotification('Skill not found', 'error', 3000);
|
if (!skill.unlocked) { this.game.showNotification('Skill is locked', 'error', 3000); return false; }
|
||||||
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;
|
||||||
if (skill.unlocked) {
|
}
|
||||||
this.game.showNotification('Skill is already unlocked', 'warning', 3000);
|
|
||||||
return false;
|
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) {
|
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
|
||||||
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
|
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.stats.skillPoints < 2) {
|
if (player.stats.skillPoints < 2) {
|
||||||
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
|
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlock skill
|
|
||||||
player.stats.skillPoints -= 2;
|
player.stats.skillPoints -= 2;
|
||||||
skill.unlocked = true;
|
skill.unlocked = true;
|
||||||
skill.currentLevel = 1;
|
skill.currentLevel = 1;
|
||||||
|
|
||||||
this.applySkillEffects();
|
this.applySkillEffects();
|
||||||
|
|
||||||
// Update UI to refresh skill points display only if in multiplayer mode or game is actively running
|
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
|
||||||
const shouldUpdateUI = window.smartSaveManager?.isMultiplayer || this.game?.isRunning;
|
|
||||||
|
|
||||||
if (shouldUpdateUI) {
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
|
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
applySkillEffects() {
|
applySkillEffects() {
|
||||||
const player = this.game.systems.player;
|
const player = this.game.systems.player;
|
||||||
|
|
||||||
// Reset to base stats first
|
|
||||||
this.resetToBaseStats();
|
this.resetToBaseStats();
|
||||||
|
|
||||||
// Apply all skill effects
|
for (const category of Object.values(this.skills)) {
|
||||||
Object.values(this.skills).forEach(skill => {
|
for (const skill of Object.values(category)) {
|
||||||
if (skill.level > 0) {
|
if (!skill.unlocked || skill.currentLevel <= 0) continue;
|
||||||
const skillData = this.skillData[skill.id];
|
|
||||||
if (skillData && skillData.effects) {
|
for (const [effect, value] of Object.entries(skill.effects || {})) {
|
||||||
Object.entries(skillData.effects).forEach(([effect, value]) => {
|
const total = value * skill.currentLevel;
|
||||||
const totalEffect = value * skill.level;
|
switch (effect) {
|
||||||
|
case 'attack': player.attributes.attack += total; break;
|
||||||
switch (effect) {
|
case 'defense': player.attributes.defense += total; break;
|
||||||
case 'attack':
|
case 'speed': player.attributes.speed += total; break;
|
||||||
player.attributes.attack += totalEffect;
|
case 'health':
|
||||||
break;
|
case 'maxHealth': player.attributes.maxHealth += total; break;
|
||||||
case 'defense':
|
case 'maxEnergy': player.attributes.maxEnergy += total; break;
|
||||||
player.attributes.defense += totalEffect;
|
case 'criticalChance': player.attributes.criticalChance += total; break;
|
||||||
break;
|
case 'criticalDamage': player.attributes.criticalDamage += total; break;
|
||||||
case 'speed':
|
default:
|
||||||
player.attributes.speed += totalEffect;
|
if (!this.activeBuffs[effect]) this.activeBuffs[effect] = 0;
|
||||||
break;
|
this.activeBuffs[effect] += total;
|
||||||
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();
|
player.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetToBaseStats() {
|
resetToBaseStats() {
|
||||||
const player = this.game.systems.player;
|
const player = this.game.systems.player;
|
||||||
|
const lvl = player.stats.level || 1;
|
||||||
// Reset to base values (would need to store base stats separately)
|
Object.assign(player.attributes, {
|
||||||
// For now, we'll use initial values
|
attack: 10 + (lvl - 1) * 2,
|
||||||
const baseStats = {
|
defense: 5 + (lvl - 1) * 1,
|
||||||
attack: 10 + (player.stats.level - 1) * 2,
|
speed: 10,
|
||||||
defense: 5 + (player.stats.level - 1) * 1,
|
maxHealth: 100 + (lvl - 1) * 10,
|
||||||
speed: 10,
|
maxEnergy: 100 + (lvl - 1) * 5,
|
||||||
maxHealth: 100 + (player.stats.level - 1) * 10,
|
|
||||||
maxEnergy: 100 + (player.stats.level - 1) * 5,
|
|
||||||
criticalChance: 0.05,
|
criticalChance: 0.05,
|
||||||
criticalDamage: 1.5
|
criticalDamage: 1.5
|
||||||
};
|
});
|
||||||
|
|
||||||
Object.assign(player.attributes, baseStats);
|
|
||||||
this.activeBuffs = {};
|
this.activeBuffs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill experience from actions
|
// ------------------------------------------------------------------ //
|
||||||
|
// Combat / science / crafting XP helpers
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
awardCombatExperience(amount) {
|
awardCombatExperience(amount) {
|
||||||
this.addSkillExperience('combat', 'weapons_mastery', amount);
|
this.addSkillExperience('combat', 'weapons_mastery', amount);
|
||||||
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
|
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
awardScienceExperience(amount) {
|
awardScienceExperience(amount) {
|
||||||
this.addSkillExperience('science', 'energy_manipulation', 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) {
|
awardCraftingExperience(amount) {
|
||||||
this.addSkillExperience('crafting', 'weapon_crafting', amount);
|
this.addSkillExperience('crafting', 'weapons_crafting', amount);
|
||||||
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
|
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill checks
|
// ------------------------------------------------------------------ //
|
||||||
|
// Queries
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
getSkillLevel(category, skillId) {
|
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;
|
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) {
|
hasSkill(category, skillId, minimumLevel = 1) {
|
||||||
const skill = this.skills[category]?.[skillId];
|
const skill = this.skills[category]?.[skillId];
|
||||||
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
|
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkillBonus(effect) {
|
getSkillBonus(effect) {
|
||||||
return this.activeBuffs[effect] || 0;
|
return this.activeBuffs[effect] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI updates
|
// ------------------------------------------------------------------ //
|
||||||
|
// UI
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
updateUI() {
|
updateUI() {
|
||||||
this.updateSkillsGrid();
|
this.updateSkillsGrid();
|
||||||
this.updateSkillPointsDisplay();
|
this.updateSkillPointsDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSkillsGrid() {
|
updateSkillsGrid() {
|
||||||
const skillsGridElement = document.getElementById('skillsGrid');
|
const grid = document.getElementById('skillsGrid');
|
||||||
if (!skillsGridElement) return;
|
if (!grid) return;
|
||||||
|
|
||||||
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
|
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
|
||||||
const skills = this.skills[activeCategory] || {};
|
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]) => {
|
Object.entries(skills).forEach(([skillId, skill]) => {
|
||||||
const skillElement = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
skillElement.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
|
||||||
|
|
||||||
const progressPercent = skill.currentLevel > 0 ?
|
const progressPercent = skill.currentLevel > 0
|
||||||
(skill.experience / skill.experienceToNext) * 100 : 0;
|
? (skill.experience / skill.experienceToNext) * 100
|
||||||
|
: 0;
|
||||||
// Use texture manager for icon fallback
|
|
||||||
const iconClass = this.game.systems.textureManager ?
|
const iconClass = this.game.systems.textureManager
|
||||||
this.game.systems.textureManager.getIcon(skill.icon) :
|
? this.game.systems.textureManager.getIcon(skill.icon)
|
||||||
(skill.icon || 'fa-question');
|
: (skill.icon || 'fa-question');
|
||||||
|
|
||||||
skillElement.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="skill-header">
|
<div class="skill-header">
|
||||||
<div class="skill-icon">
|
<div class="skill-icon"><i class="fas ${iconClass}"></i></div>
|
||||||
<i class="fas ${iconClass}"></i>
|
|
||||||
</div>
|
|
||||||
<div class="skill-info">
|
<div class="skill-info">
|
||||||
<div class="skill-name">${skill.name}</div>
|
<div class="skill-name">${skill.name}</div>
|
||||||
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
|
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
|
||||||
@ -508,89 +334,74 @@ return true;
|
|||||||
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
|
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
|
||||||
<div class="skill-progress">
|
<div class="skill-progress">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" style="width: ${progressPercent}%"></div>
|
<div class="progress-fill" style="width:${progressPercent}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span>${skill.experience}/${skill.experienceToNext} XP</span>
|
<span>${skill.experience}/${skill.experienceToNext} XP</span>
|
||||||
</div>
|
</div>
|
||||||
` : skill.currentLevel >= skill.maxLevel ? `
|
` : skill.currentLevel >= skill.maxLevel ? `
|
||||||
<div class="skill-max-level">
|
<div class="skill-max-level"><span>MAX LEVEL</span></div>
|
||||||
<span>MAX LEVEL</span>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="skill-actions">
|
<div class="skill-actions">
|
||||||
${!skill.unlocked ? `
|
${!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)
|
Unlock (2 Points)
|
||||||
</button>
|
</button>
|
||||||
` : skill.currentLevel < skill.maxLevel ? `
|
` : 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)
|
Upgrade (1 Point)
|
||||||
</button>
|
</button>
|
||||||
` : `
|
` : `<span class="max-level">MAX LEVEL</span>`}
|
||||||
<span class="max-level">MAX LEVEL</span>
|
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
${skill.requiredLevel && !skill.unlocked ? `
|
${skill.requiredLevel && !skill.unlocked ? `
|
||||||
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
|
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
skillsGridElement.appendChild(skillElement);
|
grid.appendChild(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSkillPointsDisplay() {
|
updateSkillPointsDisplay() {
|
||||||
const player = this.game.systems.player;
|
const player = this.game.systems.player;
|
||||||
// Update skill points display if element exists
|
document.querySelectorAll('.skill-points').forEach(el => {
|
||||||
const skillPointsElements = document.querySelectorAll('.skill-points');
|
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
|
||||||
skillPointsElements.forEach(element => {
|
|
||||||
element.textContent = `Skill Points: ${player.stats.skillPoints}`;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save/Load
|
// ------------------------------------------------------------------ //
|
||||||
|
// Save / Load
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
save() {
|
save() {
|
||||||
return {
|
return { skills: this.skills, activeBuffs: this.activeBuffs };
|
||||||
skills: this.skills,
|
|
||||||
activeBuffs: this.activeBuffs
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load(data) {
|
load(data) {
|
||||||
if (data.skills) {
|
if (data.skills) {
|
||||||
// Deep merge to preserve structure
|
|
||||||
for (const [category, skills] of Object.entries(data.skills)) {
|
for (const [category, skills] of Object.entries(data.skills)) {
|
||||||
if (this.skills[category]) {
|
if (!this.skills[category]) this.skills[category] = {};
|
||||||
for (const [skillId, skillData] of Object.entries(skills)) {
|
for (const [skillId, skillData] of Object.entries(skills)) {
|
||||||
if (this.skills[category][skillId]) {
|
if (this.skills[category][skillId]) {
|
||||||
Object.assign(this.skills[category][skillId], skillData);
|
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();
|
this.applySkillEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.skillPoints = 0;
|
this.activeBuffs = {};
|
||||||
this.unlockedSkills = [];
|
for (const category of Object.values(this.skills)) {
|
||||||
this.activeBuffs = [];
|
for (const skill of Object.values(category)) {
|
||||||
// Skills are already defined in constructor, just reset levels
|
skill.currentLevel = 0;
|
||||||
Object.values(this.skills).forEach(category => {
|
skill.experience = 0;
|
||||||
Object.values(category).forEach(skill => {
|
}
|
||||||
skill.currentLevel = 0;
|
}
|
||||||
skill.experience = 0;
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
clear() { this.reset(); }
|
||||||
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.currentUser = null;
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.apiBaseUrl = 'https://api.korvarix.com/api'; // API Server
|
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);
|
console.log('[GAME INITIALIZER] Constructor - gameServerUrl set to:', this.gameServerUrl);
|
||||||
}
|
}
|
||||||
@ -70,12 +70,13 @@ class GameInitializer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE THE URL - Override any undefined issues
|
// Resolve server URL: use serverData.url if set, otherwise gameServerUrl, then dev default
|
||||||
const FORCED_URL = 'https://dev.gameserver.galaxystrike.online';
|
const FORCED_URL = (this.serverData && this.serverData.url)
|
||||||
console.log('[GAME INITIALIZER] FORCING URL to:', FORCED_URL);
|
? this.serverData.url
|
||||||
console.log('[GAME INITIALIZER] Original this.gameServerUrl:', this.gameServerUrl);
|
: (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, {
|
this.socket = io(FORCED_URL, {
|
||||||
auth: {
|
auth: {
|
||||||
token: this.authToken,
|
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
|
// Socket event handlers
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
@ -146,9 +147,117 @@ class GameInitializer {
|
|||||||
console.log('[GAME INITIALIZER] Game data saved:', data);
|
console.log('[GAME INITIALIZER] Game data saved:', data);
|
||||||
this.onGameDataSaved(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() {
|
onSocketConnected() {
|
||||||
|
// Expose socket globally for systems that need it
|
||||||
|
if (window.game) {
|
||||||
|
window.game.socket = this.socket;
|
||||||
|
}
|
||||||
|
|
||||||
// Join the server room
|
// Join the server room
|
||||||
this.socket.emit('joinServer', {
|
this.socket.emit('joinServer', {
|
||||||
serverId: this.serverData.id,
|
serverId: this.serverData.id,
|
||||||
@ -232,6 +341,17 @@ class GameInitializer {
|
|||||||
if (data.success && data.playerData) {
|
if (data.success && data.playerData) {
|
||||||
// Store server player data from authentication (this is our primary source)
|
// Store server player data from authentication (this is our primary source)
|
||||||
this.serverPlayerData = data.playerData;
|
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);
|
console.log('[GAME INITIALIZER] Using authentication data as primary source:', this.serverPlayerData);
|
||||||
|
|
||||||
// NOW create GameEngine AFTER authentication is successful
|
// NOW create GameEngine AFTER authentication is successful
|
||||||
@ -245,22 +365,12 @@ class GameInitializer {
|
|||||||
window.smartSaveManager.setMultiplayerMode(true, this);
|
window.smartSaveManager.setMultiplayerMode(true, this);
|
||||||
console.log('[GAME INITIALIZER] SmartSaveManager set to multiplayer mode');
|
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);
|
window.smartSaveManager.applyServerDataToGame(data.playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Apply authentication data to game if game is running
|
// NOTE: Don't apply to GameEngine here - it doesn't exist yet!
|
||||||
if (window.game && window.game.loadServerPlayerData) {
|
// The data will be applied in createGameEngineForMultiplayer() after the game is created.
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showNotification(`Welcome back! Level ${data.playerData.stats?.level || 1}`, 'success');
|
this.showNotification(`Welcome back! Level ${data.playerData.stats?.level || 1}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
@ -276,11 +386,22 @@ class GameInitializer {
|
|||||||
// Create GameEngine instance
|
// Create GameEngine instance
|
||||||
window.game = new GameEngine();
|
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
|
// Initialize the game engine
|
||||||
console.log('[GAME INITIALIZER] About to call window.game.init()');
|
console.log('[GAME INITIALIZER] About to call window.game.init()');
|
||||||
const initPromise = window.game.init();
|
const initPromise = window.game.init();
|
||||||
console.log('[GAME INITIALIZER] GameEngine.init() returned:', typeof initPromise, initPromise);
|
console.log('[GAME INITIALIZER] GameEngine.init() returned:', typeof initPromise, initPromise);
|
||||||
|
|
||||||
|
// Apply server data and refresh UI after initialization is complete
|
||||||
initPromise.then(() => {
|
initPromise.then(() => {
|
||||||
console.log('[GAME INITIALIZER] GameEngine initialized successfully for multiplayer');
|
console.log('[GAME INITIALIZER] GameEngine initialized successfully for multiplayer');
|
||||||
|
|
||||||
@ -290,13 +411,23 @@ class GameInitializer {
|
|||||||
window.game.loadServerPlayerData(this.serverPlayerData);
|
window.game.loadServerPlayerData(this.serverPlayerData);
|
||||||
console.log('[GAME INITIALIZER] Server player data applied to GameEngine');
|
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
|
// Force UI refresh
|
||||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||||
console.log('[GAME INITIALIZER] Forcing UI refresh after data application');
|
console.log('[GAME INITIALIZER] Forcing UI refresh after data application');
|
||||||
window.game.systems.ui.forceRefreshAllUI();
|
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 {
|
} else {
|
||||||
console.warn('[GAME INITIALIZER] No server player data or loadServerPlayerData method available');
|
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
|
// Start the game
|
||||||
@ -322,11 +453,11 @@ class GameInitializer {
|
|||||||
console.log('[GAME INITIALIZER] Data content:', data.data);
|
console.log('[GAME INITIALIZER] Data content:', data.data);
|
||||||
console.log('[GAME INITIALIZER] Data keys:', data.data ? Object.keys(data.data) : 'No data object');
|
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) {
|
// Only process if we don't already have good data from authentication
|
||||||
// Store server player data
|
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;
|
this.serverPlayerData = data.data;
|
||||||
|
|
||||||
console.log('[GAME INITIALIZER] Applying server data to game systems');
|
|
||||||
// Apply server data to game if game is running
|
// Apply server data to game if game is running
|
||||||
if (window.game && window.game.loadServerPlayerData) {
|
if (window.game && window.game.loadServerPlayerData) {
|
||||||
window.game.loadServerPlayerData(data.data);
|
window.game.loadServerPlayerData(data.data);
|
||||||
@ -335,17 +466,9 @@ class GameInitializer {
|
|||||||
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
if (window.game.systems && window.game.systems.ui && window.game.systems.ui.forceRefreshAllUI) {
|
||||||
window.game.systems.ui.forceRefreshAllUI();
|
window.game.systems.ui.forceRefreshAllUI();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('[GAME INITIALIZER] No game or loadServerPlayerData method available');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[GAME INITIALIZER] Ignoring empty game data - no player data to load');
|
console.log('[GAME INITIALIZER] Ignoring gameDataLoaded - already have data from authentication or data is empty');
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +537,7 @@ class GameInitializer {
|
|||||||
|
|
||||||
// Configure game for multiplayer mode
|
// Configure game for multiplayer mode
|
||||||
console.log('[GAME INITIALIZER] Configuring 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
|
window.game.gameInitializer = this; // Store reference for server polling
|
||||||
|
|
||||||
// DISABLE game logic in multiplayer - server is authoritative
|
// DISABLE game logic in multiplayer - server is authoritative
|
||||||
@ -482,8 +605,24 @@ class GameInitializer {
|
|||||||
updateUIForMultiplayerMode() {
|
updateUIForMultiplayerMode() {
|
||||||
// Update UI elements to show multiplayer mode
|
// Update UI elements to show multiplayer mode
|
||||||
const playerName = document.getElementById('playerName');
|
const playerName = document.getElementById('playerName');
|
||||||
if (playerName && this.currentUser) {
|
const playerTitle = document.getElementById('playerTitle');
|
||||||
playerName.textContent = this.currentUser.username;
|
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
|
// Show multiplayer-specific UI elements
|
||||||
@ -666,13 +805,223 @@ class GameInitializer {
|
|||||||
username: this.currentUser.username
|
username: this.currentUser.username
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('[GAME INITIALIZER] Cannot authenticate - missing socket or user data');
|
// Try to get from localStorage as fallback
|
||||||
if (!this.socket) {
|
const storedUser = localStorage.getItem('currentUser');
|
||||||
console.warn('[GAME INITIALIZER] Socket is null/undefined');
|
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.currentUser = null;
|
||||||
this.serverPlayerData = 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
|
// Create global instance
|
||||||
@ -11,40 +11,15 @@ function integrateWithGameEngine() {
|
|||||||
// Store original save method
|
// Store original save method
|
||||||
const originalSave = window.game.save;
|
const originalSave = window.game.save;
|
||||||
|
|
||||||
// Override save method
|
// Override game save method
|
||||||
window.game.save = async function() {
|
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) {
|
||||||
if (window.smartSaveManager && window.smartSaveManager.isMultiplayer) {
|
await window.smartSaveManager.save();
|
||||||
console.log('[SAVE INTEGRATION] Multiplayer mode - client save disabled, server is authoritative');
|
} else {
|
||||||
this.showNotification('Server manages your game data', 'info', 2000);
|
// Fallback to original save if SmartSaveManager not available
|
||||||
return true;
|
return await originalSave.call(this);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -225,7 +200,7 @@ function addSaveModeUI() {
|
|||||||
indicator.id = 'save-mode-indicator';
|
indicator.id = 'save-mode-indicator';
|
||||||
indicator.style.cssText = `
|
indicator.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
color: white;
|
color: white;
|
||||||
@ -14,9 +14,11 @@ class SmartSaveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setMultiplayerMode(isMultiplayer, gameInitializer = null) {
|
setMultiplayerMode(isMultiplayer, gameInitializer = null) {
|
||||||
|
const oldMode = this.isMultiplayer;
|
||||||
this.isMultiplayer = isMultiplayer;
|
this.isMultiplayer = isMultiplayer;
|
||||||
this.gameInitializer = gameInitializer;
|
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`);
|
console.log(`[SMART SAVE] Set to ${isMultiplayer ? 'multiplayer' : 'singleplayer'} mode`);
|
||||||
|
|
||||||
if (isMultiplayer && gameInitializer) {
|
if (isMultiplayer && gameInitializer) {
|
||||||
@ -136,9 +138,52 @@ class SmartSaveManager {
|
|||||||
|
|
||||||
this.serverPlayerData = serverData;
|
this.serverPlayerData = serverData;
|
||||||
|
|
||||||
// Apply to game if game is running
|
// Apply to game if game is running (try both methods)
|
||||||
if (window.game && window.game.loadPlayerData) {
|
if (window.game) {
|
||||||
window.game.loadPlayerData(serverData);
|
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
|
// Store for game engine
|
||||||
@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
class DebugLogger {
|
class DebugLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.debugEnabled = true; // Always enabled
|
// Completely disable debug logging to prevent console flooding
|
||||||
|
this.debugEnabled = false;
|
||||||
|
|
||||||
this.startTime = performance.now();
|
this.startTime = performance.now();
|
||||||
this.stepTimers = new Map();
|
this.stepTimers = new Map();
|
||||||
this.debugLogs = []; // Store logs in memory
|
this.debugLogs = []; // Store logs in memory
|
||||||
@ -15,10 +17,15 @@ class DebugLogger {
|
|||||||
this.logger = window.logger || null;
|
this.logger = window.logger || null;
|
||||||
|
|
||||||
// Log initialization
|
// Log initialization
|
||||||
this.log('=== DEBUG SESSION STARTED ===');
|
if (this.debugEnabled) {
|
||||||
|
this.log('=== DEBUG SESSION STARTED ===');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async log(message, data = null) {
|
async log(message, data = null) {
|
||||||
|
// Skip logging if debug is disabled
|
||||||
|
if (!this.debugEnabled) return;
|
||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const stackTrace = new Error().stack;
|
const stackTrace = new Error().stack;
|
||||||
|
|
||||||
@ -55,13 +62,13 @@ class DebugLogger {
|
|||||||
this.debugLogs.shift();
|
this.debugLogs.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always log to console
|
// Skip console logging to prevent flooding
|
||||||
console.log(`[DEBUG] ${message}`, data || '');
|
// console.log(`[DEBUG] ${message}`, data || '');
|
||||||
|
|
||||||
// Log performance data to console
|
// Skip performance logging to prevent flooding
|
||||||
if (performanceData.memory) {
|
// if (performanceData.memory) {
|
||||||
console.log(`[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory.used}/${performanceData.memory.total}`);
|
// console.log(`[PERF] ${performanceData.elapsed} | Memory: ${performanceData.memory.used}/${performanceData.memory.total}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Use existing logger if available
|
// Use existing logger if available
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
@ -79,6 +86,9 @@ class DebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startStep(stepName) {
|
async startStep(stepName) {
|
||||||
|
// Skip logging if debug is disabled
|
||||||
|
if (!this.debugEnabled) return;
|
||||||
|
|
||||||
this.stepTimers.set(stepName, performance.now());
|
this.stepTimers.set(stepName, performance.now());
|
||||||
await this.log(`STEP START: ${stepName}`, {
|
await this.log(`STEP START: ${stepName}`, {
|
||||||
type: 'step_start',
|
type: 'step_start',
|
||||||
@ -88,6 +98,9 @@ class DebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async endStep(stepName, data = null) {
|
async endStep(stepName, data = null) {
|
||||||
|
// Skip logging if debug is disabled
|
||||||
|
if (!this.debugEnabled) return;
|
||||||
|
|
||||||
const startTime = this.stepTimers.get(stepName);
|
const startTime = this.stepTimers.get(stepName);
|
||||||
const duration = startTime ? (performance.now() - startTime).toFixed(2) : 'N/A';
|
const duration = startTime ? (performance.now() - startTime).toFixed(2) : 'N/A';
|
||||||
|
|
||||||
@ -101,6 +114,9 @@ class DebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logStep(stepName, data = null) {
|
async logStep(stepName, data = null) {
|
||||||
|
// Skip logging if debug is disabled
|
||||||
|
if (!this.debugEnabled) return;
|
||||||
|
|
||||||
await this.log(`STEP: ${stepName}`, {
|
await this.log(`STEP: ${stepName}`, {
|
||||||
type: 'step',
|
type: 'step',
|
||||||
step: stepName,
|
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
|
maxSlots: this.maxSlots
|
||||||
});
|
});
|
||||||
|
|
||||||
const startingItems = [
|
// In multiplayer mode, starting items should come from server
|
||||||
{
|
if (window.smartSaveManager?.isMultiplayer) {
|
||||||
id: 'starter_blaster_common',
|
console.log('[INVENTORY] Multiplayer mode - starting items will be provided by server');
|
||||||
name: 'Common Blaster',
|
if (debugLogger) debugLogger.logStep('Skipping starting items in multiplayer mode');
|
||||||
type: 'weapon',
|
if (debugLogger) debugLogger.endStep('Inventory.addStartingItems', {
|
||||||
rarity: 'common',
|
finalItemCount: this.items.length,
|
||||||
quantity: 1,
|
itemsAdded: 0
|
||||||
stats: { attack: 5, criticalChance: 0.02 },
|
});
|
||||||
description: 'A reliable basic blaster for new pilots',
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equip basic armor
|
// Singleplayer mode - no hardcoded starting items available
|
||||||
const armorItem = this.items.find(item => item.id === 'basic_armor');
|
console.log('[INVENTORY] Singleplayer mode - no hardcoded starting items available');
|
||||||
if (armorItem) {
|
if (debugLogger) debugLogger.logStep('No starting items available in singleplayer mode');
|
||||||
console.log('[INVENTORY] Equipping basic armor');
|
|
||||||
this.equipItem(armorItem.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-stack starting items
|
|
||||||
if (debugLogger) debugLogger.logStep('Auto-stacking starting items');
|
|
||||||
this.autoStackItems();
|
|
||||||
|
|
||||||
if (debugLogger) debugLogger.endStep('Inventory.addStartingItems', {
|
if (debugLogger) debugLogger.endStep('Inventory.addStartingItems', {
|
||||||
finalItemCount: this.items.length,
|
finalItemCount: this.items.length,
|
||||||
itemsAdded: startingItems.length
|
itemsAdded: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,9 +537,9 @@ class Player {
|
|||||||
upgrades: []
|
upgrades: []
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('=== DEBUG: Character Reset ===');
|
// console.log('=== DEBUG: Character Reset ===');
|
||||||
console.log('Player health reset to:', this.attributes.health, '/', this.attributes.maxHealth);
|
// 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('Ship health reset to:', this.ship.health, '/', this.ship.maxHealth);
|
||||||
|
|
||||||
// Reset skills
|
// Reset skills
|
||||||
this.skills = {};
|
this.skills = {};
|
||||||
@ -692,16 +692,43 @@ class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePlayTime(deltaTime) {
|
updatePlayTime(deltaTime) {
|
||||||
|
// DISABLED: Reduce console spam for quest debugging
|
||||||
|
/*
|
||||||
console.log('[PLAYER] updatePlayTime called with deltaTime:', deltaTime, 'ms');
|
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');
|
console.log('[PLAYER] Before update - playTime:', this.stats.playTime, 'ms');
|
||||||
|
*/
|
||||||
|
|
||||||
// Use real computer time delta
|
// Use real computer time delta
|
||||||
this.stats.playTime += deltaTime;
|
this.stats.playTime += deltaTime;
|
||||||
|
|
||||||
|
// DISABLED: Reduce console spam for quest debugging
|
||||||
|
/*
|
||||||
console.log('[PLAYER] After update - playTime:', this.stats.playTime, 'ms');
|
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 seconds:', this.stats.playTime / 1000, 'seconds');
|
||||||
console.log('[PLAYER] PlayTime in minutes:', this.stats.playTime / 60000, 'minutes');
|
console.log('[PLAYER] PlayTime in minutes:', this.stats.playTime / 60000, 'minutes');
|
||||||
console.log('[PLAYER] PlayTime in hours:', this.stats.playTime / 3600000, 'hours');
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI updates
|
// UI updates
|
||||||
@ -717,10 +744,15 @@ class Player {
|
|||||||
|
|
||||||
// Update player info
|
// Update player info
|
||||||
const playerNameElement = document.getElementById('playerName');
|
const playerNameElement = document.getElementById('playerName');
|
||||||
|
const playerTitleElement = document.getElementById('playerTitle');
|
||||||
const playerLevelElement = document.getElementById('playerLevel');
|
const playerLevelElement = document.getElementById('playerLevel');
|
||||||
|
|
||||||
if (playerNameElement) {
|
if (playerNameElement) {
|
||||||
playerNameElement.textContent = `${this.info.name} - ${this.info.title}`;
|
playerNameElement.textContent = this.info.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerTitleElement) {
|
||||||
|
playerTitleElement.textContent = ` - ${this.info.title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerLevelElement) {
|
if (playerLevelElement) {
|
||||||
@ -767,6 +799,7 @@ class Player {
|
|||||||
if (debugLogger) debugLogger.logStep('Player UI update completed', {
|
if (debugLogger) debugLogger.logStep('Player UI update completed', {
|
||||||
elementsUpdated: {
|
elementsUpdated: {
|
||||||
playerName: !!playerNameElement,
|
playerName: !!playerNameElement,
|
||||||
|
playerTitle: !!playerTitleElement,
|
||||||
playerLevel: !!playerLevelElement,
|
playerLevel: !!playerLevelElement,
|
||||||
totalKills: !!totalKillsElement,
|
totalKills: !!totalKillsElement,
|
||||||
dungeonsCleared: !!dungeonsClearedElement,
|
dungeonsCleared: !!dungeonsClearedElement,
|
||||||
@ -819,9 +852,29 @@ class Player {
|
|||||||
try {
|
try {
|
||||||
if (data.stats) {
|
if (data.stats) {
|
||||||
console.log('[PLAYER] Loading stats:', 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 };
|
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] Level after stats load:', this.stats.level);
|
||||||
|
console.log('[PLAYER] PlayTime after stats load:', this.stats.playTime);
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('Player stats loaded', {
|
if (debugLogger) debugLogger.logStep('Player stats loaded', {
|
||||||
oldLevel: oldStats.level,
|
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') {
|
} else if (view === 'ships') {
|
||||||
this.updateShipGallery();
|
this.updateShipGallery();
|
||||||
} else if (view === 'starbases') {
|
} else if (view === 'starbases') {
|
||||||
this.updateStarbaseList();
|
// Boot the isometric starbase world instead of the old list UI
|
||||||
this.updateStarbasePurchaseList();
|
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.lastActiveTime = Date.now();
|
||||||
this.accumulatedTime = 0; // Track time for resource generation
|
this.accumulatedTime = 0; // Track time for resource generation
|
||||||
|
|
||||||
// Idle production rates
|
// Idle production rates (online rates)
|
||||||
this.productionRates = {
|
this.productionRates = {
|
||||||
credits: 10, // credits per second (increased for better gameplay)
|
credits: 0.1, // 1 credit every 10 seconds (0.1 per second)
|
||||||
experience: 1, // experience per second (increased for better progression)
|
experience: 0, // no auto experience - only from dungeons
|
||||||
energy: 0.5 // energy regeneration per second
|
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
|
// Offline rewards
|
||||||
@ -144,6 +151,20 @@ class IdleSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
claimOfflineRewards() {
|
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 &&
|
if (this.offlineRewards.credits === 0 &&
|
||||||
this.offlineRewards.experience === 0 &&
|
this.offlineRewards.experience === 0 &&
|
||||||
this.offlineRewards.items.length === 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;
|
this.game = gameEngine;
|
||||||
|
|
||||||
|
// Server time synchronization
|
||||||
|
this.serverTimeOffset = 0; // Difference between server and client time
|
||||||
|
this.lastServerTimeSync = 0;
|
||||||
|
|
||||||
// Quest types
|
// Quest types
|
||||||
this.questTypes = {
|
this.questTypes = {
|
||||||
main: 'Main Story',
|
main: 'Main Story',
|
||||||
@ -35,682 +39,56 @@ class QuestSystem {
|
|||||||
questStatus: Object.keys(this.questStatus)
|
questStatus: Object.keys(this.questStatus)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Main story quests
|
// Main story quests - populated by server
|
||||||
this.mainQuests = [
|
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' }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// All possible daily quests (20 total)
|
// Daily quests - populated by server
|
||||||
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)
|
|
||||||
this.dailyQuests = [];
|
this.dailyQuests = [];
|
||||||
this.selectedDailyQuests = [];
|
|
||||||
|
|
||||||
// Currently active weekly quests (5 random from allWeeklyQuests)
|
|
||||||
this.weeklyQuests = [];
|
this.weeklyQuests = [];
|
||||||
this.selectedWeeklyQuests = [];
|
|
||||||
|
|
||||||
// Current active quests
|
|
||||||
this.activeQuests = [];
|
this.activeQuests = [];
|
||||||
this.completedQuests = [];
|
this.completedQuests = [];
|
||||||
this.failedQuests = [];
|
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
|
// Initialize daily quests with safety check
|
||||||
try {
|
try {
|
||||||
@ -788,14 +166,15 @@ class QuestSystem {
|
|||||||
this.maxProceduralQuests = 3;
|
this.maxProceduralQuests = 3;
|
||||||
this.proceduralQuestRefresh = 30 * 60 * 1000; // 30 minutes
|
this.proceduralQuestRefresh = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
// Statistics
|
// Initialize stats
|
||||||
this.stats = {
|
this.stats = {
|
||||||
questsCompleted: 0,
|
questsCompleted: 0,
|
||||||
|
questsFailed: 0,
|
||||||
dailyQuestsCompleted: 0,
|
dailyQuestsCompleted: 0,
|
||||||
weeklyQuestsCompleted: 0,
|
weeklyQuestsCompleted: 0,
|
||||||
totalRewardsEarned: { credits: 0, experience: 0, gems: 0 },
|
totalRewardsEarned: { credits: 0, experience: 0, gems: 0 },
|
||||||
lastDailyReset: Date.now(),
|
lastDailyReset: this.getServerTime(),
|
||||||
lastWeeklyReset: Date.now()
|
lastWeeklyReset: this.getServerTime()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize daily quests
|
// 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() {
|
async initialize() {
|
||||||
const debugLogger = window.debugLogger;
|
const debugLogger = window.debugLogger;
|
||||||
|
|
||||||
@ -1119,7 +535,7 @@ class QuestSystem {
|
|||||||
|
|
||||||
// Complete quest
|
// Complete quest
|
||||||
quest.status = 'completed';
|
quest.status = 'completed';
|
||||||
quest.completedAt = Date.now();
|
quest.completedAt = this.getServerTime();
|
||||||
this.completedQuests.push(quest);
|
this.completedQuests.push(quest);
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('Quest marked as completed', {
|
if (debugLogger) debugLogger.logStep('Quest marked as completed', {
|
||||||
@ -1131,7 +547,7 @@ class QuestSystem {
|
|||||||
|
|
||||||
// Save completed daily quests to history
|
// Save completed daily quests to history
|
||||||
if (quest.type === 'daily') {
|
if (quest.type === 'daily') {
|
||||||
const questCopy = { ...quest, completedAt: Date.now() };
|
const questCopy = { ...quest, completedAt: this.getServerTime() };
|
||||||
this.completedDailyQuests.push(questCopy);
|
this.completedDailyQuests.push(questCopy);
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('Daily quest added to history', {
|
if (debugLogger) debugLogger.logStep('Daily quest added to history', {
|
||||||
@ -1143,7 +559,7 @@ class QuestSystem {
|
|||||||
|
|
||||||
// Save completed weekly quests to history
|
// Save completed weekly quests to history
|
||||||
if (quest.type === 'weekly') {
|
if (quest.type === 'weekly') {
|
||||||
const questCopy = { ...quest, completedAt: Date.now() };
|
const questCopy = { ...quest, completedAt: this.getServerTime() };
|
||||||
this.completedWeeklyQuests.push(questCopy);
|
this.completedWeeklyQuests.push(questCopy);
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('Weekly quest added to history', {
|
if (debugLogger) debugLogger.logStep('Weekly quest added to history', {
|
||||||
@ -1542,7 +958,7 @@ class QuestSystem {
|
|||||||
this.completedQuests = [];
|
this.completedQuests = [];
|
||||||
this.dailyQuests = [];
|
this.dailyQuests = [];
|
||||||
this.selectedDailyQuests = [];
|
this.selectedDailyQuests = [];
|
||||||
this.lastDailyReset = Date.now();
|
this.lastDailyReset = this.getServerTime();
|
||||||
|
|
||||||
// Reset main quest statuses
|
// Reset main quest statuses
|
||||||
this.mainQuests.forEach(quest => {
|
this.mainQuests.forEach(quest => {
|
||||||
@ -1575,7 +991,7 @@ class QuestSystem {
|
|||||||
|
|
||||||
checkDailyReset() {
|
checkDailyReset() {
|
||||||
const debugLogger = window.debugLogger;
|
const debugLogger = window.debugLogger;
|
||||||
const now = Date.now();
|
const now = this.getServerTime();
|
||||||
const lastReset = this.lastDailyReset;
|
const lastReset = this.lastDailyReset;
|
||||||
const daysSinceReset = Math.floor((now - lastReset) / (24 * 60 * 60 * 1000));
|
const daysSinceReset = Math.floor((now - lastReset) / (24 * 60 * 60 * 1000));
|
||||||
|
|
||||||
@ -1646,10 +1062,10 @@ class QuestSystem {
|
|||||||
|
|
||||||
updateDailyCountdown() {
|
updateDailyCountdown() {
|
||||||
// Always update countdown so it's ready when user switches to daily tab
|
// Always update countdown so it's ready when user switches to daily tab
|
||||||
const now = new Date();
|
const now = this.getServerDate();
|
||||||
const tomorrow = new Date(now);
|
const tomorrow = new Date();
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
tomorrow.setUTCDate(now.getDate() + 1);
|
||||||
tomorrow.setHours(0, 0, 0, 0); // Set to midnight
|
tomorrow.setUTCHours(0, 0, 0, 0); // Set to midnight UTC
|
||||||
|
|
||||||
const timeUntilReset = tomorrow - now;
|
const timeUntilReset = tomorrow - now;
|
||||||
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
|
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
|
||||||
@ -1714,11 +1130,11 @@ class QuestSystem {
|
|||||||
|
|
||||||
checkWeeklyReset() {
|
checkWeeklyReset() {
|
||||||
const debugLogger = window.debugLogger;
|
const debugLogger = window.debugLogger;
|
||||||
const now = Date.now();
|
const now = this.getServerTime();
|
||||||
const lastReset = this.lastWeeklyReset || 0;
|
const lastReset = this.lastWeeklyReset || 0;
|
||||||
|
|
||||||
// Calculate if we need to reset based on Saturday midnight
|
// Calculate if we need to reset based on Saturday midnight (server time)
|
||||||
const currentDateTime = new Date();
|
const currentDateTime = this.getServerDate();
|
||||||
const dayOfWeek = currentDateTime.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
const dayOfWeek = currentDateTime.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||||
const currentHour = currentDateTime.getHours();
|
const currentHour = currentDateTime.getHours();
|
||||||
const currentMinute = currentDateTime.getMinutes();
|
const currentMinute = currentDateTime.getMinutes();
|
||||||
@ -1814,7 +1230,7 @@ class QuestSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateWeeklyCountdown() {
|
updateWeeklyCountdown() {
|
||||||
const now = new Date();
|
const now = this.getServerDate();
|
||||||
const dayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
const dayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||||
const currentHour = now.getHours();
|
const currentHour = now.getHours();
|
||||||
const currentMinute = now.getMinutes();
|
const currentMinute = now.getMinutes();
|
||||||
@ -1848,19 +1264,19 @@ class QuestSystem {
|
|||||||
|
|
||||||
console.log('[QUEST SYSTEM] Days until Saturday:', daysUntilSaturday);
|
console.log('[QUEST SYSTEM] Days until Saturday:', daysUntilSaturday);
|
||||||
|
|
||||||
// Create the target reset time (Saturday midnight)
|
// Create the target reset time (Saturday midnight UTC)
|
||||||
const nextSaturday = new Date(now);
|
const nextSaturday = new Date();
|
||||||
nextSaturday.setDate(now.getDate() + daysUntilSaturday);
|
nextSaturday.setUTCDate(now.getDate() + daysUntilSaturday);
|
||||||
nextSaturday.setHours(0, 0, 0, 0); // Set to midnight
|
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();
|
let timeUntilReset = nextSaturday.getTime() - now.getTime();
|
||||||
|
|
||||||
// Ensure timeUntilReset is positive (handle edge cases)
|
// Ensure timeUntilReset is positive (handle edge cases)
|
||||||
if (timeUntilReset <= 0) {
|
if (timeUntilReset <= 0) {
|
||||||
console.log('[QUEST SYSTEM] Time until reset is negative or zero, adding 7 days');
|
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();
|
timeUntilReset = nextSaturday.getTime() - now.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2065,8 +1481,58 @@ class QuestSystem {
|
|||||||
return completed.sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0));
|
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
|
// UI updates
|
||||||
updateUI() {
|
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.updateQuestList();
|
||||||
this.updateQuestStats();
|
this.updateQuestStats();
|
||||||
}
|
}
|
||||||
@ -2084,6 +1550,7 @@ class QuestSystem {
|
|||||||
|
|
||||||
const quests = this.getQuestsByType(activeType);
|
const quests = this.getQuestsByType(activeType);
|
||||||
console.log(`[QUEST SYSTEM] Getting quests for type: ${activeType}, found: ${quests.length} quests`);
|
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 = '';
|
questListElement.innerHTML = '';
|
||||||
|
|
||||||
@ -60,6 +60,22 @@ class ShipSystem {
|
|||||||
shipGrid.appendChild(shipCard);
|
shipGrid.appendChild(shipCard);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ship image URL from server or local
|
||||||
|
*/
|
||||||
|
getShipImageUrl(ship) {
|
||||||
|
if (!ship) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
|
||||||
|
|
||||||
|
// For multiplayer, get from server
|
||||||
|
if (window.smartSaveManager?.isMultiplayer && window.game?.socket) {
|
||||||
|
const serverUrl = window.game.socket.io?.uri?.replace('/socket.io', '') || process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online';
|
||||||
|
return `${serverUrl}/images/ships/${ship.id}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For singleplayer, use local path
|
||||||
|
return ship.image || ship.texture || 'assets/textures/ships/starter_cruiser.png';
|
||||||
|
}
|
||||||
|
|
||||||
createShipCard(ship) {
|
createShipCard(ship) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@ -68,17 +84,13 @@ class ShipSystem {
|
|||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="ship-card-header">
|
<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-info">
|
||||||
<div class="ship-card-rarity ${ship.rarity.toLowerCase()}">${ship.rarity}</div>
|
<div class="ship-card-rarity ${ship.rarity.toLowerCase()}">${ship.rarity}</div>
|
||||||
</div>
|
</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;
|
return card;
|
||||||
@ -106,10 +118,22 @@ class ShipSystem {
|
|||||||
const ship = player.ship;
|
const ship = player.ship;
|
||||||
|
|
||||||
if (elements.currentShipImage) {
|
if (elements.currentShipImage) {
|
||||||
// Use the ship's texture if available, otherwise fallback
|
// Use server image for multiplayer, local for singleplayer
|
||||||
const imagePath = ship.texture || `assets/textures/ships/starter_cruiser.png`;
|
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.src = imagePath;
|
||||||
elements.currentShipImage.alt = ship.name;
|
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.currentShipName) elements.currentShipName.textContent = ship.name;
|
||||||
if (elements.currentShipClass) elements.currentShipClass.textContent = ship.class || 'Unknown';
|
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.currentUser = null;
|
||||||
this.servers = []; // Renamed from serverList to avoid conflict with DOM element
|
this.servers = []; // Renamed from serverList to avoid conflict with DOM element
|
||||||
this.apiBaseUrl = 'https://api.korvarix.com/api'; // API Server URL
|
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.localGameServerUrl = 'http://localhost:3002'; // Local Game Server URL
|
||||||
this.isLocalMode = false; // Track if we're in local mode
|
this.isLocalMode = false; // Track if we're in local mode
|
||||||
|
|
||||||
@ -43,16 +43,19 @@ class UIManager {
|
|||||||
const gameInterface = document.getElementById('gameInterface');
|
const gameInterface = document.getElementById('gameInterface');
|
||||||
const navButtons = document.querySelectorAll('.nav-btn');
|
const navButtons = document.querySelectorAll('.nav-btn');
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('DOM Check', {
|
console.log('[UI MANAGER] DOM Check:', {
|
||||||
gameInterfaceExists: !!gameInterface,
|
gameInterfaceExists: !!gameInterface,
|
||||||
gameInterfaceHidden: gameInterface?.classList.contains('hidden'),
|
gameInterfaceHidden: gameInterface?.classList.contains('hidden'),
|
||||||
navButtonsFound: navButtons.length,
|
navButtonsFound: navButtons.length,
|
||||||
documentReady: document.readyState
|
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();
|
this.proceedWithInitialization();
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[UI MANAGER] Waiting for navigation buttons...');
|
||||||
setTimeout(waitForDOM, 100);
|
setTimeout(waitForDOM, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -117,10 +120,6 @@ class UIManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (debugLogger) debugLogger.endStep('UIManager.setupNavigation', {
|
if (debugLogger) debugLogger.endStep('UIManager.setupNavigation', {
|
||||||
navButtonsSetup: navButtons.length,
|
|
||||||
skillCatButtonsSetup: skillCatButtons.length,
|
|
||||||
shopCatButtonsSetup: shopCatButtons.length,
|
|
||||||
questTabButtonsSetup: questTabButtons.length,
|
|
||||||
craftingCatButtonsSetup: craftingCatButtons.length
|
craftingCatButtonsSetup: craftingCatButtons.length
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -140,10 +139,11 @@ class UIManager {
|
|||||||
if (navButtons.length === 0) {
|
if (navButtons.length === 0) {
|
||||||
console.warn('[UIManager] No navigation buttons found!');
|
console.warn('[UIManager] No navigation buttons found!');
|
||||||
if (debugLogger) debugLogger.logStep('No navigation buttons found');
|
if (debugLogger) debugLogger.logStep('No navigation buttons found');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navButtons.forEach((btn, index) => {
|
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
|
// Check if button already has a listener to prevent duplicates
|
||||||
if (btn.getAttribute('data-has-listener') === 'true') {
|
if (btn.getAttribute('data-has-listener') === 'true') {
|
||||||
console.log(`[UIManager] Button ${btn.dataset.tab} already has listener, skipping`);
|
console.log(`[UIManager] Button ${btn.dataset.tab} already has listener, skipping`);
|
||||||
@ -151,16 +151,30 @@ class UIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
btn.addEventListener('click', (e) => {
|
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;
|
const tab = btn.dataset.tab;
|
||||||
if (tab) {
|
if (tab) {
|
||||||
this.switchTab(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');
|
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
|
// Set up UI update event listener from GameEngine
|
||||||
@ -765,7 +779,7 @@ class UIManager {
|
|||||||
|
|
||||||
// Stop economy system timers
|
// Stop economy system timers
|
||||||
if (this.game.systems.economy) {
|
if (this.game.systems.economy) {
|
||||||
this.game.systems.economy.stopShopTimers();
|
this.game.systems.economy.stopShopRefreshTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMultiplayer) {
|
if (isMultiplayer) {
|
||||||
@ -1037,7 +1051,7 @@ class UIManager {
|
|||||||
const oldTab = this.currentTab;
|
const oldTab = this.currentTab;
|
||||||
|
|
||||||
// Update navigation buttons
|
// Update navigation buttons
|
||||||
const navButtons = document.querySelectorAll('.nav-btn');
|
const navButtons = document.querySelectorAll('.nav-btn, .bottom-nav-btn, .nav-drawer-btn');
|
||||||
let navButtonsUpdated = 0;
|
let navButtonsUpdated = 0;
|
||||||
|
|
||||||
navButtons.forEach(btn => {
|
navButtons.forEach(btn => {
|
||||||
@ -1292,8 +1306,14 @@ class UIManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Call economy system update
|
||||||
this.game.systems.economy.updateUI();
|
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', {
|
if (debugLogger) debugLogger.endStep('UIManager.switchShopCategory', {
|
||||||
success: true,
|
success: true,
|
||||||
category: category,
|
category: category,
|
||||||
@ -1313,13 +1333,7 @@ class UIManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchQuestType(type) {
|
updateQuestTabs(type) {
|
||||||
const debugLogger = window.debugLogger;
|
|
||||||
|
|
||||||
if (debugLogger) debugLogger.startStep('UIManager.switchQuestType', {
|
|
||||||
type: type
|
|
||||||
});
|
|
||||||
|
|
||||||
const questTabButtons = document.querySelectorAll('.quest-tab-btn');
|
const questTabButtons = document.querySelectorAll('.quest-tab-btn');
|
||||||
let buttonsUpdated = 0;
|
let buttonsUpdated = 0;
|
||||||
|
|
||||||
@ -1339,7 +1353,13 @@ class UIManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
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', {
|
if (debugLogger) debugLogger.endStep('UIManager.switchQuestType', {
|
||||||
success: true,
|
success: true,
|
||||||
@ -1611,20 +1631,20 @@ class UIManager {
|
|||||||
|
|
||||||
// In multiplayer mode, ensure we're using server data
|
// In multiplayer mode, ensure we're using server data
|
||||||
if (window.smartSaveManager?.isMultiplayer) {
|
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 {
|
try {
|
||||||
// Safety checks - return early if systems aren't available
|
// Safety checks - return early if systems aren't available
|
||||||
if (!this.game || !this.game.systems) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.game.systems.player || !this.game.systems.economy) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1633,18 +1653,18 @@ class UIManager {
|
|||||||
|
|
||||||
// Additional safety checks for player properties
|
// Additional safety checks for player properties
|
||||||
if (!player.stats || !player.attributes || !player.ship) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugLogger) debugLogger.startStep('UIManager.updateResourceDisplay', {
|
// if (debugLogger) debugLogger.startStep('UIManager.updateResourceDisplay', {
|
||||||
gameSystemsAvailable: !!(this.game && this.game.systems),
|
// gameSystemsAvailable: !!(this.game && this.game.systems),
|
||||||
playerSystemAvailable: !!(this.game && this.game.systems && this.game.systems.player),
|
// playerSystemAvailable: !!(this.game && this.game.systems && this.game.systems.player),
|
||||||
economySystemAvailable: !!(this.game && this.game.systems && this.game.systems.economy),
|
// economySystemAvailable: !!(this.game && this.game.systems && this.game.systems.economy),
|
||||||
playerStatsAvailable: !!player.stats,
|
// playerStatsAvailable: !!player.stats,
|
||||||
playerAttributesAvailable: !!player.attributes,
|
// playerAttributesAvailable: !!player.attributes,
|
||||||
playerShipAvailable: !!player.ship
|
// playerShipAvailable: !!player.ship
|
||||||
});
|
// });
|
||||||
|
|
||||||
let elementsUpdated = 0;
|
let elementsUpdated = 0;
|
||||||
let elementsNotFound = 0;
|
let elementsNotFound = 0;
|
||||||
@ -1654,7 +1674,7 @@ class UIManager {
|
|||||||
if (playerLevelElement) {
|
if (playerLevelElement) {
|
||||||
const oldLevel = playerLevelElement.textContent;
|
const oldLevel = playerLevelElement.textContent;
|
||||||
const playerLevel = player.stats.level || 1;
|
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}`;
|
playerLevelElement.textContent = `Lv. ${playerLevel}`;
|
||||||
elementsUpdated++;
|
elementsUpdated++;
|
||||||
|
|
||||||
@ -1665,7 +1685,7 @@ class UIManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
elementsNotFound++;
|
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
|
// Update ship level with safety checks
|
||||||
@ -1692,7 +1712,9 @@ class UIManager {
|
|||||||
if (creditsElement) {
|
if (creditsElement) {
|
||||||
const oldCredits = creditsElement.textContent;
|
const oldCredits = creditsElement.textContent;
|
||||||
const creditsAmount = economy.credits || 0;
|
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);
|
creditsElement.textContent = this.game.formatNumber(creditsAmount);
|
||||||
elementsUpdated++;
|
elementsUpdated++;
|
||||||
|
|
||||||
@ -1712,6 +1734,9 @@ class UIManager {
|
|||||||
if (gemsElement) {
|
if (gemsElement) {
|
||||||
const oldGems = gemsElement.textContent;
|
const oldGems = gemsElement.textContent;
|
||||||
const gemsAmount = economy.gems || 0;
|
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);
|
gemsElement.textContent = this.game.formatNumber(gemsAmount);
|
||||||
elementsUpdated++;
|
elementsUpdated++;
|
||||||
|
|
||||||
@ -1730,10 +1755,13 @@ class UIManager {
|
|||||||
const energyElement = document.getElementById('energy');
|
const energyElement = document.getElementById('energy');
|
||||||
if (energyElement) {
|
if (energyElement) {
|
||||||
const oldEnergy = energyElement.textContent;
|
const oldEnergy = energyElement.textContent;
|
||||||
// Ensure we're using the correct energy values, not credits
|
const currentEnergy = player.attributes.currentEnergy || player.attributes.energy || 0;
|
||||||
const currentEnergy = Math.floor(player.attributes.energy || 0);
|
const maxEnergy = player.attributes.maxEnergy || 100;
|
||||||
const maxEnergy = Math.floor(player.attributes.maxEnergy || 100);
|
const newEnergy = `${currentEnergy}/${maxEnergy}`;
|
||||||
energyElement.textContent = `${currentEnergy}/${maxEnergy}`;
|
if (oldEnergy !== newEnergy) {
|
||||||
|
console.log('[UI MANAGER] Energy updated:', newEnergy);
|
||||||
|
}
|
||||||
|
energyElement.textContent = newEnergy;
|
||||||
elementsUpdated++;
|
elementsUpdated++;
|
||||||
|
|
||||||
if (debugLogger) debugLogger.logStep('Energy updated', {
|
if (debugLogger) debugLogger.logStep('Energy updated', {
|
||||||
@ -1749,6 +1777,65 @@ class UIManager {
|
|||||||
if (debugLogger) debugLogger.log('Energy element not found');
|
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', {
|
if (debugLogger) debugLogger.endStep('UIManager.updateResourceDisplay', {
|
||||||
elementsUpdated: elementsUpdated,
|
elementsUpdated: elementsUpdated,
|
||||||
elementsNotFound: elementsNotFound,
|
elementsNotFound: elementsNotFound,
|
||||||
@ -1767,33 +1854,132 @@ class UIManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI() {
|
formatPlayTime(milliseconds) {
|
||||||
const debugLogger = window.debugLogger;
|
// Format play time showing only the two most relevant units
|
||||||
|
const totalSeconds = Math.floor(milliseconds / 1000);
|
||||||
|
|
||||||
if (debugLogger) debugLogger.startStep('UIManager.updateUI', {
|
if (totalSeconds < 60) {
|
||||||
currentTab: this.currentTab,
|
// Less than 1 minute: show seconds only
|
||||||
modalOpen: this.modalOpen
|
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
|
// Update resource display only if in multiplayer mode or game is actively running
|
||||||
if (this.shouldUpdateUI()) {
|
if (this.shouldUpdateUI()) {
|
||||||
if (debugLogger) debugLogger.logStep('Updating resource display');
|
// if (debugLogger) debugLogger.logStep('Updating resource display');
|
||||||
this.updateResourceDisplay();
|
this.updateResourceDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tab content only if in multiplayer mode or game is actively running
|
// Update tab content only if in multiplayer mode or game is actively running
|
||||||
if (this.shouldUpdateUI()) {
|
if (this.shouldUpdateUI()) {
|
||||||
if (debugLogger) debugLogger.logStep('Updating tab content', {
|
// if (debugLogger) debugLogger.logStep('Updating tab content', {
|
||||||
currentTab: this.currentTab
|
// currentTab: this.currentTab
|
||||||
});
|
// });
|
||||||
this.updateTabContent(this.currentTab);
|
this.updateTabContent(this.currentTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugLogger) debugLogger.endStep('UIManager.updateUI', {
|
// if (debugLogger) debugLogger.endStep('UIManager.updateUI', {
|
||||||
resourceDisplayUpdated: true,
|
// resourceDisplayUpdated: true,
|
||||||
tabContentUpdated: true,
|
// tabContentUpdated: true,
|
||||||
currentTab: this.currentTab
|
// currentTab: this.currentTab
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menus
|
// Menus
|
||||||
@ -1945,8 +2131,8 @@ class UIManager {
|
|||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset economy
|
// Reset economy - only reset if not in multiplayer mode
|
||||||
if (this.game.systems.economy) {
|
if (this.game.systems.economy && !window.smartSaveManager?.isMultiplayer) {
|
||||||
this.game.systems.economy.credits = 1000;
|
this.game.systems.economy.credits = 1000;
|
||||||
this.game.systems.economy.gems = 10;
|
this.game.systems.economy.gems = 10;
|
||||||
}
|
}
|
||||||
@ -2065,8 +2251,8 @@ class UIManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear economy data
|
// Clear economy data - only reset if not in multiplayer mode
|
||||||
if (this.game.systems.economy) {
|
if (this.game.systems.economy && !window.smartSaveManager?.isMultiplayer) {
|
||||||
this.game.systems.economy.credits = 1000;
|
this.game.systems.economy.credits = 1000;
|
||||||
this.game.systems.economy.gems = 10;
|
this.game.systems.economy.gems = 10;
|
||||||
}
|
}
|
||||||
@ -2451,6 +2637,94 @@ class UIManager {
|
|||||||
</button>
|
</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
|
// 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);
|
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 Items */
|
||||||
.shop-item {
|
.shop-item {
|
||||||
background: var(--bg-tertiary);
|
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 {
|
.player-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-name {
|
.player-name {
|
||||||
@ -1203,6 +1210,18 @@ body {
|
|||||||
color: var(--text-primary);
|
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 {
|
.player-level {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@ -1381,6 +1400,20 @@ body {
|
|||||||
gap: 0.5rem;
|
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 {
|
.stat {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
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 |