/** * Game Server - Based on Client LocalServer Infrastructure * Handles real-time multiplayer game instances and gameplay */ const express = require('express'); const http = require('http'); const errorReporter = require('./utils/ErrorReporter'); const socketIo = require('socket.io'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const mongoose = require('mongoose'); const fs = require('fs'); const path = require('path'); require('dotenv').config(); const logger = require('./utils/logger'); const connectDB = require('./config/database'); const PlayerData = require('./models/PlayerData'); // Import server systems for player initialization const QuestSystem = require('./systems/QuestSystem'); const SkillSystem = require('./systems/SkillSystem'); const DungeonSystem = require('./systems/DungeonSystem'); const CraftingSystem = require('./systems/CraftingSystem'); const IdleSystem = require('./systems/IdleSystem'); const ItemSystem = require('./systems/ItemSystem'); const ContentLoader = require('./systems/ContentLoader'); const FleetSystem = require('./systems/FleetSystem'); const { ResourceSystem, RESOURCE_CONFIG, RESOURCE_TYPES } = require('./systems/ResourceSystem'); const MissionSystem = require('./systems/MissionSystem'); const { AllianceSystem } = require('./systems/AllianceSystem'); const { MarketSystem } = require('./systems/MarketSystem'); const SocialSystem = require('./systems/SocialSystem'); const { ReputationSystem } = require('./systems/ReputationSystem'); const GalaxyEventSystem = require('./systems/GalaxyEventSystem'); const { SeasonSystem } = require('./systems/SeasonSystem'); const GalaxySystem = require('./systems/GalaxySystem'); const { ResearchSystem } = require('./systems/ResearchSystem'); // ── Load all JSON game content before systems are initialised ── const contentLoader = new ContentLoader(); contentLoader._loadSkills(); contentLoader._loadEnemies(); contentLoader._loadDungeons(); contentLoader._loadItems(); contentLoader._loadRecipes(); contentLoader._loadQuests(); contentLoader._loaded = true; console.log(`[SERVER] Content loaded — ${contentLoader.skills.size} skills, ${contentLoader.items.size} items, ${contentLoader.recipes.size} recipes, ${contentLoader.quests.size} quests, ${contentLoader.dungeons.size} dungeons, ${contentLoader.enemies.size} enemies`); // Initialize server systems — each receives contentLoader as its data source const questSystem = new QuestSystem(contentLoader); const skillSystem = new SkillSystem(contentLoader); const dungeonSystem = new DungeonSystem(contentLoader); const craftingSystem = new CraftingSystem(contentLoader); const idleSystem = new IdleSystem(); const itemSystem = new ItemSystem(contentLoader); const fleetSystem = new FleetSystem(contentLoader); const resourceSystem = new ResourceSystem(); const missionSystem = new MissionSystem(); const allianceSystem = new AllianceSystem(); const marketSystem = new MarketSystem(); const socialSystem = new SocialSystem(); const reputationSystem = new ReputationSystem(); const galaxyEventSystem = new GalaxyEventSystem(); const seasonSystem = new SeasonSystem(); const galaxySystem = new GalaxySystem(); const researchSystem = new ResearchSystem(); // Set server URL for ItemSystem const SERVER_URL = process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online'; itemSystem.setServerUrl(SERVER_URL); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: [ "http://localhost:3000", "http://127.0.0.1:3000", "file://", "https://dev.galaxystrike.online", "https://galaxystrike.online" ], methods: ["GET", "POST"], credentials: true } }); // Pass io instance to systems that need it dungeonSystem.setIO(io); // Game state const gameInstances = new Map(); const connectedClients = new Map(); let pvpChallenges = new Map(); // GDD §9.4 — active PvP challenge invites // ── PvP battle simulation helper ───────────────────────────────────────────── function _simulatePvpBattle(challenger, defender) { const rounds = []; let cHp = Math.max(1, (challenger.ship?.health || 1000) + (challenger.level || 1) * 50); let dHp = Math.max(1, (defender.ship?.health || 1000) + (defender.level || 1) * 50); const cAtk = Math.max(1, (challenger.ship?.attack || 100) + (challenger.level || 1) * 10); const dAtk = Math.max(1, (defender.ship?.attack || 100) + (defender.level || 1) * 10); let round = 0; while (cHp > 0 && dHp > 0 && round < 20) { round++; const cDmg = Math.floor(cAtk * (0.85 + Math.random() * 0.3)); const dDmg = Math.floor(dAtk * (0.85 + Math.random() * 0.3)); dHp -= cDmg; cHp -= dDmg; rounds.push({ round, challengerDmg: cDmg, defenderDmg: dDmg, challengerHp: Math.max(0, cHp), defenderHp: Math.max(0, dHp) }); } return { winner: cHp >= dHp ? 'challenger' : 'defender', rounds }; } // ── Ship module slot helper ──────────────────────────────────────────────────── function _getShipSlots(ship) { const base = ['weapon_1', 'weapon_2', 'armor_1', 'engine', 'shield']; // Higher-rarity ships get extra slots const rarity = ship?.rarity || 'common'; if (rarity === 'rare') return [...base, 'special_1']; if (rarity === 'epic') return [...base, 'special_1', 'special_2']; if (rarity === 'legendary') return [...base, 'special_1', 'special_2', 'special_3']; return base; } // Server-side shop item lookup using ItemSystem function findShopItem(itemId) { return itemSystem.findShopItem(itemId); } // Middleware app.use(cors({ origin: [ "http://localhost:3000", "http://127.0.0.1:3000", "file://", "https://dev.galaxystrike.online", "https://galaxystrike.online" ], credentials: true })); app.use(express.json({ limit: '10mb' })); app.use(errorReporter.requestMiddleware()); // Health + metrics endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', ...errorReporter.getMetrics() }); }); // Serve static files from the client directory app.use(express.static(path.join(__dirname, '../Client'))); // Serve ships from server-side storage app.use('/images/ships', (req, res, next) => { const requestedPath = req.path; const serverImagePath = path.join(__dirname, 'assets/images/ships', requestedPath); console.log('[IMAGE SERVER] Ship requested:', requestedPath); if (fs.existsSync(serverImagePath)) { res.sendFile(serverImagePath); } else { const placeholderPath = path.join(__dirname, 'assets/images/ui/placeholder.png'); res.sendFile(placeholderPath); } }); // Serve weapons from server-side storage app.use('/images/weapons', (req, res, next) => { const requestedPath = req.path; const serverImagePath = path.join(__dirname, 'assets/images/weapons', requestedPath); console.log('[IMAGE SERVER] Weapon requested:', requestedPath); if (fs.existsSync(serverImagePath)) { res.sendFile(serverImagePath); } else { const placeholderPath = path.join(__dirname, 'assets/images/ui/placeholder.png'); res.sendFile(placeholderPath); } }); // Serve armors from server-side storage app.use('/images/armors', (req, res, next) => { const requestedPath = req.path; const serverImagePath = path.join(__dirname, 'assets/images/armors', requestedPath); console.log('[IMAGE SERVER] Armor requested:', requestedPath); if (fs.existsSync(serverImagePath)) { res.sendFile(serverImagePath); } else { const placeholderPath = path.join(__dirname, 'assets/images/ui/placeholder.png'); res.sendFile(placeholderPath); } }); // Serve other items (materials, consumables, cosmetics) from server-side storage app.use('/images/items', (req, res, next) => { const requestedPath = req.path; const serverImagePath = path.join(__dirname, 'assets/images/items', requestedPath); console.log('[IMAGE SERVER] Item requested:', requestedPath); if (fs.existsSync(serverImagePath)) { res.sendFile(serverImagePath); } else { const placeholderPath = path.join(__dirname, 'assets/images/ui/placeholder.png'); res.sendFile(placeholderPath); } }); // Serve UI elements and icons from server-side storage app.use('/images/ui', (req, res, next) => { const requestedPath = req.path; const serverImagePath = path.join(__dirname, 'assets/images/ui', requestedPath); if (fs.existsSync(serverImagePath)) { res.sendFile(serverImagePath); } else { res.status(404).send('UI asset not found'); } }); app.use(express.urlencoded({ extended: true })); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), uptime: process.uptime(), mode: 'game-server', activeInstances: gameInstances.size, connectedClients: connectedClients.size }); }); // API version endpoint app.get('/api/ssc/version', (req, res) => { res.status(200).json({ version: '1.0.0', service: 'galaxystrikeonline-game-server', timestamp: new Date().toISOString(), mode: 'multiplayer' }); }); // Shop API endpoints app.get('/api/shop/items', (req, res) => { try { res.status(200).json(itemSystem.buildShopResponse()); } catch (error) { console.error('[GAME SERVER] Error fetching shop items:', error); res.status(500).json({ success: false, error: 'Failed to fetch shop items' }); } }); app.get('/api/shop/items/:category', (req, res) => { try { const { category } = req.params; res.status(200).json({ success: true, category, items: itemSystem.getRandomItemsByCategory(category), timestamp: new Date().toISOString() }); } catch (error) { console.error('[GAME SERVER] Error fetching category items:', error); res.status(500).json({ success: false, error: 'Failed to fetch category items' }); } }); app.get('/api/items/:itemId', (req, res) => { try { const { itemId } = req.params; // Find item across all categories const allItems = itemSystem.getAllItems(); let item = null; for (const [category, items] of Object.entries(allItems)) { item = items.find(i => i.id === itemId); if (item) break; } if (!item) { return res.status(404).json({ success: false, error: 'Item not found' }); } res.status(200).json({ success: true, item: item, timestamp: new Date().toISOString() }); } catch (error) { console.error('[GAME SERVER] Error fetching item details:', error); res.status(500).json({ success: false, error: 'Failed to fetch item details' }); } }); // Game data endpoints (similar to LocalServer) app.post('/api/game/player/:id/save', (req, res) => { const playerId = req.params.id; const playerData = req.body; // Store player data in game instance if (connectedClients.has(playerId)) { connectedClients.get(playerId).playerData = playerData; // Broadcast to other players in same instance const instanceId = connectedClients.get(playerId).instanceId; if (gameInstances.has(instanceId)) { io.to(instanceId).emit('playerDataUpdated', { playerId, timestamp: Date.now() }); } } res.status(200).json({ success: true, message: 'Player data saved to game server' }); }); app.get('/api/game/player/:id', (req, res) => { const playerId = req.params.id; if (connectedClients.has(playerId)) { const playerData = connectedClients.get(playerId).playerData; res.status(200).json({ success: true, player: playerData }); } else { res.status(404).json({ success: false, error: 'Player not connected to game server' }); } }); // Game system routes app.use('/api/crafting', require('./routes/crafting')); app.use('/api/quests', require('./routes/quests')); app.use('/api/skills', require('./routes/skills')); app.use('/api/ships', require('./routes/ships')); app.use('/api/idle', require('./routes/idle')); app.use('/api/dungeons', require('./routes/dungeons')); app.use('/api/base', require('./routes/base')); // Socket.IO handlers (based on LocalServer) io.on('connection', (socket) => { console.log('[GAME SERVER] === NEW CLIENT CONNECTION ==='); console.log('[GAME SERVER] Client connected:', socket.id); // ── Rate limiter: GDD §18.4 — max 20 events/sec per socket ────────── { const RL_MAX = 20, RL_WIN = 1000; let rlCount = 0, rlReset = Date.now() + RL_WIN; const _origOn = socket.on.bind(socket); const SYSTEM_EVENTS = new Set(['connect','disconnect','error','reconnect']); socket.on = function(event, handler) { if (SYSTEM_EVENTS.has(event)) return _origOn(event, handler); return _origOn(event, function(...args) { const now = Date.now(); if (now > rlReset) { rlCount = 0; rlReset = now + RL_WIN; } if (++rlCount > RL_MAX) { console.warn(`[RATE_LIMIT] ${socket.id} dropped '${event}' (${rlCount}/s)`); return; } handler.apply(this, args); }); }; } connectedClients.set(socket.id, { connectedAt: Date.now(), playerData: null, instanceId: null }); console.log('[GAME SERVER] Waiting for authentication from:', socket.id); // Update player count on API server when new player connects updatePlayerCountOnAPI(); // Add timeout for authentication const authTimeout = setTimeout(() => { const clientData = connectedClients.get(socket.id); if (clientData && !clientData.userId) { console.log('[GAME SERVER] Authentication timeout for:', socket.id); socket.emit('authenticated', { success: false, error: 'Authentication timeout' }); socket.disconnect(); } }, 10000); // 10 seconds timeout // Clear timeout when authenticated socket.on('authenticated', () => { clearTimeout(authTimeout); }); // Log all incoming events for debugging socket.onAny((eventName, ...args) => { if (eventName !== 'ping' && eventName !== 'pong') { console.log(`[GAME SERVER] Event received: ${eventName} from ${socket.id}`, args); } }); // Authentication (similar to LocalServer) socket.on('authenticate', async (data) => { console.log('[GAME SERVER] Authenticating client:', socket.id, data); try { // Check database connection first if (mongoose.connection.readyState !== 1) { console.error('[GAME SERVER] Database not connected, authentication failed'); socket.emit('authenticated', { success: false, error: 'Database not available' }); return; } console.log('[GAME SERVER] Database is connected, proceeding with authentication'); // Load player data from database const playerData = await loadPlayerData(data.userId || socket.id, data.username || 'Game Player'); if (playerData) { console.log('[GAME SERVER] Player data loaded successfully:', playerData.username); // Store player data in client connection const clientData = connectedClients.get(socket.id); if (clientData) { clientData.playerData = playerData; clientData.userId = playerData.userId; clientData.username = playerData.username; } else { console.error('[GAME SERVER] No client data found for socket:', socket.id); } // Join server playerData.joinServer(`devgame-server-${PORT}`); // Update last login time + daily login streak gems (GDD §3 v3.2) if (!playerData.stats) playerData.stats = {}; const now = new Date(); const lastLogin = playerData.stats.lastLogin ? new Date(playerData.stats.lastLogin) : null; let streakGems = 0; if (lastLogin) { const hoursSince = (now - lastLogin) / 3600000; if (hoursSince >= 20 && hoursSince <= 48) { // Consecutive day login — increment streak playerData.stats.loginStreak = Math.min(30, (playerData.stats.loginStreak || 0) + 1); } else if (hoursSince > 48) { // Streak broken playerData.stats.loginStreak = 1; } } else { playerData.stats.loginStreak = 1; } // Gem reward: day 1=0, day 3=1, day 7=3, day 14=5, day 30=10 const streak = playerData.stats.loginStreak || 1; if (streak >= 30) streakGems = 10; else if (streak >= 14) streakGems = 5; else if (streak >= 7) streakGems = 3; else if (streak >= 3) streakGems = 1; if (streakGems > 0) { playerData.stats.gems = (playerData.stats.gems || 0) + streakGems; } playerData.stats.lastLogin = now.toISOString(); // Ensure all required fields exist (for existing players) if (playerData.stats.totalExperience === undefined) playerData.stats.totalExperience = playerData.stats.experience || 0; if (playerData.stats.gems === undefined || playerData.stats.gems === 0) playerData.stats.gems = 50; // Restore gems if wiped if (playerData.stats.skillPoints === undefined) playerData.stats.skillPoints = 0; if (playerData.stats.totalKills === undefined) playerData.stats.totalKills = 0; if (playerData.stats.questsCompleted === undefined) playerData.stats.questsCompleted = 0; // Ensure idle system production rates are initialized idleSystem.initializePlayerData(playerData.userId); console.log('[GAME SERVER] Idle system initialized for player:', playerData.username); await savePlayerData(playerData.userId, playerData); // Auto-collect any completed fleet missions (GDD §8.3) try { if (playerData.fleetMissions?.length > 0) { const mResults = missionSystem.collectMissions(playerData, resourceSystem); if (mResults.length > 0) { await savePlayerData(playerData.userId, playerData); mResults.forEach(r => socket.emit('mission_completed', r)); } } } catch(me) { console.error('[AUTH MISSION COLLECT]', me.message); } // Calculate pending offline rewards to show on dashboard const pendingOffline = idleSystem.calculateOfflineRewards(playerData.userId); if (pendingOffline.offlineTime > 0) { playerData.stats.offlineTime = pendingOffline.offlineTime; playerData.stats.offlineCredits = pendingOffline.rewards?.credits || 0; } else { playerData.stats.offlineTime = 0; playerData.stats.offlineCredits = 0; } // In production, validate with API server socket.emit('authenticated', { success: true, user: { id: playerData.userId, username: playerData.username, token: 'game-token-' + Date.now() }, playerData: { ...playerData.toObject(), serverTimestamp: Date.now(), serverTimezone: 'UTC' }, loginStreak: playerData.stats.loginStreak || 1, streakGemsAwarded: streakGems || 0, }); console.log(`[GAME SERVER] ${playerData.username} authenticated with Level ${playerData.stats.level}`); } else { console.error('[GAME SERVER] Failed to load player data'); socket.emit('authenticated', { success: false, error: 'Failed to load player data' }); } } catch (error) { console.error('[GAME SERVER] Authentication error:', error); console.error('[GAME INITIALIZER] Error stack:', error.stack); socket.emit('authenticated', { success: false, error: 'Authentication failed: ' + error.message }); } }); // Game data events (similar to LocalServer) socket.on('saveGameData', async (data) => { console.log('[GAME SERVER] Saving game data for:', socket.id); const clientData = connectedClients.get(socket.id); if (clientData && clientData.userId) { try { // Validate the data before saving if (!data || typeof data !== 'object') { console.warn('[GAME SERVER] Invalid game data received, skipping save'); socket.emit('gameDataSaved', { success: false, error: 'Invalid game data' }); return; } // Update player data with new game data const updatedPlayerData = { ...clientData.playerData, ...data }; clientData.playerData = updatedPlayerData; // Save to database const success = await savePlayerData(clientData.userId, updatedPlayerData); socket.emit('gameDataSaved', { success: success, message: success ? 'Game saved to server!' : 'Failed to save to server' }); console.log(`[GAME SERVER] Saved game data for ${clientData.username}`); } catch (error) { console.error('[GAME SERVER] Error saving game data:', error); socket.emit('gameDataSaved', { success: false, error: 'Failed to save data' }); } } else { console.warn('[GAME SERVER] No client data or user ID found for saveGameData'); socket.emit('gameDataSaved', { success: false, error: 'Player not authenticated' }); } }); socket.on('loadGameData', (data) => { console.log('[GAME SERVER] Loading game data for:', socket.id); const playerData = connectedClients.get(socket.id)?.playerData || {}; socket.emit('gameDataLoaded', { success: true, data: playerData }); }); // Test idle system manually socket.on('testIdleRewards', (data) => { console.log('[GAME SERVER] Testing idle rewards for:', socket.id); const clientData = connectedClients.get(socket.id); if (clientData && clientData.userId) { const onlineRewards = idleSystem.generateOnlineIdleRewards(clientData.userId, 10000); console.log('[GAME SERVER] Test idle rewards:', onlineRewards); socket.emit('testIdleRewards', { rewards: onlineRewards, productionRates: idleSystem.playerProductionRates.get(clientData.userId) }); } else { socket.emit('testIdleRewards', { error: 'Not authenticated' }); } }); // Shop and item system events socket.on('ping', (data) => { console.log('[GAME SERVER] Ping received from:', socket.id, data); socket.emit('pong', { timestamp: Date.now(), received: data.timestamp, serverTime: new Date().toISOString() }); }); socket.on('getShopItems', () => { try { socket.emit('shopItemsReceived', itemSystem.buildShopResponse()); } catch (error) { console.error('[GAME SERVER] getShopItems error:', error); socket.emit('shopItemsReceived', { success: false, error: 'Failed to load shop items' }); } }); socket.on('getShopCategory', (data) => { try { const { category } = data || {}; if (!category) { socket.emit('shopCategoryReceived', { success: false, error: 'Category required' }); return; } socket.emit('shopCategoryReceived', { success: true, category, items: itemSystem.getRandomItemsByCategory(category), timestamp: new Date().toISOString() }); } catch (error) { console.error('[GAME SERVER] getShopCategory error:', error); socket.emit('shopCategoryReceived', { success: false, error: 'Failed to load category items' }); } }); socket.on('getItemDetails', (data) => { const { itemId } = data || {}; if (!itemId) { socket.emit('itemDetailsReceived', { success: false, error: 'Item ID required' }); return; } socket.emit('itemDetailsReceived', itemSystem.buildItemDetailResponse(itemId)); }); // Game-specific events socket.on('joinGameInstance', (data) => { const { instanceId } = data; if (!gameInstances.has(instanceId)) { gameInstances.set(instanceId, { id: instanceId, players: new Set(), createdAt: Date.now() }); } const instance = gameInstances.get(instanceId); instance.players.add(socket.id); connectedClients.get(socket.id).instanceId = instanceId; socket.join(instanceId); socket.emit('joinedGameInstance', { instanceId, playerCount: instance.players.size }); // Notify other players socket.to(instanceId).emit('playerJoinedInstance', { playerId: socket.id, playerCount: instance.players.size }); }); socket.on('leaveGameInstance', (data) => { const clientData = connectedClients.get(socket.id); if (clientData && clientData.instanceId) { const instance = gameInstances.get(clientData.instanceId); if (instance) { instance.players.delete(socket.id); socket.leave(clientData.instanceId); // Clean up empty instances if (instance.players.size === 0) { gameInstances.delete(clientData.instanceId); } // Notify other players socket.to(clientData.instanceId).emit('playerLeftInstance', { playerId: socket.id, playerCount: instance.players.size }); } clientData.instanceId = null; } }); socket.on('gameAction', (data) => { const clientData = connectedClients.get(socket.id); if (clientData && clientData.instanceId) { // Broadcast game action to other players in same instance socket.to(clientData.instanceId).emit('gameAction', { playerId: socket.id, action: data, timestamp: Date.now() }); } }); // Idle rewards events socket.on('claimOfflineRewards', async (data) => { console.log('[GAME SERVER] Claiming offline rewards for:', socket.id); const clientData = connectedClients.get(socket.id); if (!clientData || !clientData.userId) { socket.emit('offlineRewardsClaimed', { success: false, error: 'Not authenticated' }); return; } try { const playerData = await loadPlayerData(clientData.userId, clientData.username || 'Player'); const offlineRewards = idleSystem.calculateOfflineRewards(clientData.userId); if (offlineRewards.offlineTime > 0 && offlineRewards.rewards.credits > 0) { // Apply rewards to player playerData.stats.credits = (playerData.stats.credits || 0) + offlineRewards.rewards.credits; playerData.stats.experience = (playerData.stats.experience || 0) + offlineRewards.rewards.experience; // Update idle system data if (!playerData.idleSystem) playerData.idleSystem = {}; playerData.idleSystem.lastActive = new Date().toISOString(); playerData.idleSystem.totalOfflineTime += offlineRewards.offlineTime; playerData.idleSystem.totalIdleCredits += offlineRewards.rewards.credits; await savePlayerData(clientData.userId, playerData); socket.emit('offlineRewardsClaimed', { success: true, rewards: offlineRewards.rewards, offlineTime: offlineRewards.offlineTime }); console.log(`[GAME SERVER] Offline rewards claimed for ${clientData.username}: ${offlineRewards.rewards.credits} credits`); } else { socket.emit('offlineRewardsClaimed', { success: true, rewards: { credits: 0, experience: 0, energy: 0 }, offlineTime: 0 }); } } catch (error) { console.error('[GAME SERVER] Error claiming offline rewards:', error); socket.emit('offlineRewardsClaimed', { success: false, error: 'Failed to claim rewards' }); } }); // Shop purchase events socket.on('purchaseItem', async (data) => { const clientData = connectedClients.get(socket.id); if (!clientData?.userId) { socket.emit('purchaseCompleted', { success: false, error: 'Not authenticated' }); return; } try { const { itemId, quantity = 1 } = data || {}; if (!itemId) { socket.emit('purchaseCompleted', { success: false, error: 'Item ID required' }); return; } const playerData = await loadPlayerData(clientData.userId, clientData.username || 'Player'); if (!playerData) { socket.emit('purchaseCompleted', { success: false, error: 'Failed to load player data' }); return; } // Server-side validation: item must exist and be in the shop const item = itemSystem.findShopItem(itemId); if (!item) { socket.emit('purchaseCompleted', { success: false, error: 'Item not found in shop' }); return; } // Block re-purchase of already-owned cosmetics and decorations if (item.type === 'cosmetic' && playerData.ownedCosmetics?.includes(item.id)) { socket.emit('purchaseCompleted', { success: false, error: 'You already own this cosmetic' }); return; } if (item.type === 'decoration') { const sb = playerData.starbase || {}; if (item.subtype === 'wallpaper' && sb.ownedWallpapers?.includes(item.id)) { socket.emit('purchaseCompleted', { success: false, error: 'You already own this wallpaper' }); return; } if (item.subtype === 'room_unlock' && item.roomId && sb.unlockedRooms?.includes(item.roomId)) { socket.emit('purchaseCompleted', { success: false, error: 'You already own this room' }); return; } } // Validate affordability const validation = itemSystem.validatePurchase(item, playerData.stats, quantity); if (!validation.valid) { socket.emit('purchaseCompleted', { success: false, error: validation.error }); return; } // Apply purchase (deducts currency + grants item in playerData) const purchaseSummary = itemSystem.applyPurchase(item, playerData, quantity); await savePlayerData(clientData.userId, playerData); clientData.playerData = playerData; socket.emit('purchaseCompleted', { success: true, item: itemSystem.buildItemDetailResponse(item.id).item, quantity, totalCost: purchaseSummary.totalCost, currency: purchaseSummary.currency, newBalance: purchaseSummary.newBalance }); console.log(`[GAME SERVER] Purchase: ${clientData.username} bought ${item.name} x${quantity} for ${purchaseSummary.totalCost} ${purchaseSummary.currency}`); broadcastEconomyUpdate(socket.id); } catch (error) { console.error('[GAME SERVER] purchaseItem error:', error); socket.emit('purchaseCompleted', { success: false, error: 'Purchase failed: ' + error.message }); } }); // ── Starbase customisation ────────────────────────────────────────────── socket.on('get_starbase_data', async () => { const client = connectedClients.get(socket.id); if (!client?.userId) { socket.emit('starbase_data', { success: false }); return; } try { const pd = await loadPlayerData(client.userId, client.username || 'Player'); const sb = pd?.starbase || { wallpaper: null, ownedWallpapers: [], unlockedRooms: [] }; const catalog = itemSystem.getAllItems().filter(i => i.type === 'decoration'); socket.emit('starbase_data', { success: true, starbase: sb, catalog }); } catch (err) { console.error('[GAME SERVER] get_starbase_data error:', err); socket.emit('starbase_data', { success: false, error: err.message }); } }); socket.on('set_wallpaper', async (data) => { const { wallpaperId, roomId } = data || {}; const client = connectedClients.get(socket.id); if (!client?.userId) { socket.emit('wallpaper_set', { success: false, error: 'Not authenticated' }); return; } try { const pd = await loadPlayerData(client.userId, client.username || 'Player'); if (!pd) { socket.emit('wallpaper_set', { success: false, error: 'Player not found' }); return; } if (!pd.starbase) pd.starbase = { wallpaper: null, ownedWallpapers: [], unlockedRooms: [] }; if (wallpaperId === null) { if (roomId) { if (!pd.starbase.roomWallpapers) pd.starbase.roomWallpapers = {}; delete pd.starbase.roomWallpapers[roomId]; } else { pd.starbase.wallpaper = null; } } else { if (!pd.starbase.ownedWallpapers?.includes(wallpaperId)) { socket.emit('wallpaper_set', { success: false, error: 'Wallpaper not owned' }); return; } if (roomId) { if (!pd.starbase.roomWallpapers) pd.starbase.roomWallpapers = {}; pd.starbase.roomWallpapers[roomId] = wallpaperId; } else { pd.starbase.wallpaper = wallpaperId; } } await savePlayerData(client.userId, pd); client.playerData = pd; socket.emit('wallpaper_set', { success: true, starbase: pd.starbase }); } catch (err) { console.error('[GAME SERVER] set_wallpaper error:', err); socket.emit('wallpaper_set', { success: false, error: err.message }); } }); socket.on('get_skills', () => { console.log('[GAME SERVER] Sending skill definitions to:', socket.id); try { const skills = skillSystem.getAllSkills(); socket.emit('skills_data', skills); } catch (error) { console.error('[GAME SERVER] Error sending skills:', error); socket.emit('skills_data', []); } }); socket.on('get_recipes', () => { console.log('[GAME SERVER] Sending crafting recipes to:', socket.id); try { const recipes = craftingSystem.getAllRecipes(); socket.emit('recipes_data', recipes); } catch (error) { console.error('[GAME SERVER] Error sending recipes:', error); socket.emit('recipes_data', []); } }); // ── CRAFTING: craft_item (GDD §11) — timed queue ──────────────────────── // Materials are consumed immediately; item arrives after craft_time_seconds. // Skill level reduces time: time = base * (1 - 0.01 * craftingLevel) capped at 50% reduction. socket.on('craft_item', async ({ recipeId } = {}) => { if (!socket.userId) return socket.emit('craft_result', { success: false, error: 'Not authenticated' }); if (!recipeId) return socket.emit('craft_result', { success: false, error: 'No recipe specified' }); try { const playerData = await PlayerData.findOne({ userId: socket.userId }); if (!playerData) return socket.emit('craft_result', { success: false, error: 'Player not found' }); const recipe = craftingSystem.getRecipe(recipeId); if (!recipe) return socket.emit('craft_result', { success: false, error: 'Recipe not found' }); // Check max queue size (5 simultaneous crafts) const crafting = playerData.crafting || {}; const queue = Array.isArray(crafting.queue) ? crafting.queue : []; if (queue.length >= 5) return socket.emit('craft_result', { success: false, error: 'Crafting queue full (max 5)' }); // Server-side material check const check = craftingSystem.checkMaterials(recipeId, playerData.inventory || { items: [] }); if (!check.canCraft) return socket.emit('craft_result', { success: false, error: 'Missing materials', missing: check.missing }); // Consume materials now const inputs = recipe.recipe?.inputs || recipe.inputs || {}; const inv = playerData.inventory || { items: [] }; for (const [itemId, qty] of Object.entries(inputs)) { craftingSystem._removeItems(inv, itemId, qty); } playerData.inventory = inv; // Calculate craft time with skill reduction const skillLevel = crafting.skill || 1; const reduction = Math.min(0.5, skillLevel * 0.01); // 1% per level, cap 50% const baseTimeSec = recipe.recipe?.craft_time_seconds || recipe.recipe?.alloy_time_seconds || recipe.recipe?.smelt_time_seconds || recipe.recipe?.process_time_seconds || recipe.recipe?.cook_time_seconds || recipe.recipe?.harvest_time_seconds || 30; // default 30s const timeSec = Math.max(5, Math.round(baseTimeSec * (1 - reduction))); const completesAt = Date.now() + timeSec * 1000; // Queue the craft const craftJobId = `craft_${Date.now()}_${Math.random().toString(36).slice(2,6)}`; const outputs = recipe.recipe?.output || recipe.output || {}; queue.push({ jobId: craftJobId, recipeId, outputs, xpGain: recipe.craft?.xp || recipe.xp || 10, startedAt: Date.now(), completesAt, timeSec, }); // Update skill XP on start (small preview XP — full XP on collect) crafting.queue = queue; crafting.skill = crafting.skill || 1; crafting.experience = crafting.experience || 0; playerData.crafting = crafting; playerData.markModified('inventory'); playerData.markModified('crafting'); await playerData.save(); socket.emit('craft_result', { success: true, queued: true, jobId: craftJobId, recipeId, timeSec, completesAt, queueLength: queue.length, }); socket.emit('inventory_update', { items: playerData.inventory?.items || [], maxSize: playerData.inventory?.maxSize || 50, }); socket.emit('craft_queue_update', { queue }); console.log(`[CRAFTING] ${socket.username} queued ${recipeId} — completes in ${timeSec}s`); } catch (err) { console.error('[CRAFTING] craft_item error:', err); socket.emit('craft_result', { success: false, error: 'Server error' }); } }); // ── CRAFTING: collect_craft — collect a completed craft job ───────────── socket.on('collect_craft', async ({ jobId } = {}) => { if (!socket.userId || !jobId) return; try { const playerData = await PlayerData.findOne({ userId: socket.userId }); if (!playerData) return; const crafting = playerData.crafting || {}; const queue = Array.isArray(crafting.queue) ? crafting.queue : []; const idx = queue.findIndex(j => j.jobId === jobId); if (idx < 0) return socket.emit('collect_craft_result', { success: false, error: 'Job not found' }); const job = queue[idx]; if (Date.now() < job.completesAt) return socket.emit('collect_craft_result', { success: false, error: 'Not ready yet', remainingMs: job.completesAt - Date.now() }); // Add outputs to inventory const inv = playerData.inventory || { items: [] }; const outputItems = []; for (const [itemId, qty] of Object.entries(job.outputs || {})) { const item = { id: itemId, itemId, quantity: qty, obtainedAt: Date.now(), source: 'crafting' }; (inv.items || inv).push(item); outputItems.push(item); } playerData.inventory = inv; // Award XP and skill up crafting.experience = (crafting.experience || 0) + job.xpGain; const newSkill = Math.min(50, Math.floor(crafting.experience / 200) + 1); crafting.skill = newSkill; crafting.totalCrafted = (crafting.totalCrafted || 0) + 1; queue.splice(idx, 1); crafting.queue = queue; playerData.crafting = crafting; playerData.markModified('inventory'); playerData.markModified('crafting'); await playerData.save(); socket.emit('collect_craft_result', { success: true, jobId, output: outputItems, xpGained: job.xpGain, craftingLevel: newSkill, craftingXp: crafting.experience, }); socket.emit('inventory_update', { items: inv.items || [], maxSize: playerData.inventory?.maxSize || 50 }); socket.emit('craft_queue_update', { queue }); } catch (err) { socket.emit('collect_craft_result', { success: false, error: 'Server error' }); } }); // ── CRAFTING: get_craft_queue — fetch current queue state ─────────────── socket.on('get_craft_queue', async () => { if (!socket.userId) return; try { const playerData = await PlayerData.findOne({ userId: socket.userId }); if (!playerData) return; const queue = Array.isArray(playerData.crafting?.queue) ? playerData.crafting.queue : []; socket.emit('craft_queue_update', { queue, skill: playerData.crafting?.skill || 1, xp: playerData.crafting?.experience || 0 }); } catch (err) {} }); // ── CRAFTING: check_craft (can player craft a recipe?) ────────────────── socket.on('check_craft', async ({ recipeId } = {}) => { if (!socket.userId || !recipeId) return; try { const playerData = await PlayerData.findOne({ userId: socket.userId }); if (!playerData) return; const recipe = craftingSystem.getRecipe(recipeId); const result = craftingSystem.checkMaterials(recipeId, playerData.inventory || { items: [] }); const skillLevel = playerData.crafting?.skill || 1; const reduction = Math.min(0.5, skillLevel * 0.01); const baseTime = recipe?.recipe?.craft_time_seconds || recipe?.recipe?.alloy_time_seconds || recipe?.recipe?.smelt_time_seconds || 30; const timeSec = Math.max(5, Math.round(baseTime * (1 - reduction))); socket.emit('craft_check_result', { recipeId, ...result, timeSec, skillLevel }); } catch (err) { console.error('[CRAFTING] check_craft error:', err); } }); socket.on('get_dungeons', () => { console.log('[GAME SERVER] Sending dungeons data to:', socket.id); const dungeons = dungeonSystem.getDungeonsGroupedByDifficulty(); socket.emit('dungeons_data', dungeons); }); socket.on('get_enemy_templates', () => { // Client expects { [id]: enemyTemplate } — convert array to keyed object const arr = dungeonSystem.getEnemyTemplates(); const templates = {}; for (const e of arr) templates[e.id] = e; socket.emit('enemy_templates_data', templates); }); // Economy System Packet Handlers socket.on('get_economy_data', () => { console.log('[GAME SERVER] Sending economy data to:', socket.id); const clientData = connectedClients.get(socket.id); if (clientData && clientData.playerData) { const economyData = { credits: clientData.playerData.stats.credits || 0, gems: clientData.playerData.stats.gems || 0 }; socket.emit('economy_data', economyData); } }); // Function to broadcast economy updates to specific client function broadcastEconomyUpdate(socketId) { const clientData = connectedClients.get(socketId); if (clientData && clientData.playerData) { const economyData = { credits: clientData.playerData.stats.credits || 0, gems: clientData.playerData.stats.gems || 0 }; io.to(socketId).emit('economy_data', economyData); console.log('[GAME SERVER] Broadcasted economy update to:', socketId, economyData); } } // ════════════════════════════════════════════════════════════════════════ // BUILDINGS / BASE HANDLERS // ════════════════════════════════════════════════════════════════════════ // Building definitions (in-memory; extend to JSON data later) const BUILDING_DEFS = { // GDD §6.2 — buildings cost metal/gas/crystal in addition to credits command_center: { name:'Command Center', maxLevel:20, baseCost:{credits:0, metal:100 }, timeBase:60, effects:{buildSlots:1}, icon:'fa-satellite-dish', description:'Gates all other building levels.' }, mining_facility: { name:'Mining Facility', maxLevel:20, baseCost:{credits:0, metal:300 }, timeBase:90, effects:{metalPerHr:100}, icon:'fa-industry', description:'+15% metal/hr per level.' }, gas_extractor: { name:'Gas Extractor', maxLevel:20, baseCost:{credits:0, metal:200, gas:100 }, timeBase:90, effects:{gasPerHr:80}, icon:'fa-cloud', description:'+15% gas/hr per level.' }, power_reactor: { name:'Power Reactor', maxLevel:20, baseCost:{credits:0, metal:400, crystal:50 }, timeBase:120, effects:{energyPerHr:200}, icon:'fa-bolt', description:'+200 energy/hr per level.' }, shipyard: { name:'Shipyard', maxLevel:15, baseCost:{credits:500, metal:1000, gas:300 }, timeBase:180, effects:{buildSpeed:10}, icon:'fa-anchor', description:'Build ships; each level +10% speed.' }, research_lab: { name:'Research Lab', maxLevel:20, baseCost:{credits:300, metal:600, crystal:100 }, timeBase:150, effects:{researchSpeed:8}, icon:'fa-flask', description:'+8% research speed per level.' }, defense_platform: { name:'Defense Platform', maxLevel:10, baseCost:{credits:0, metal:500, crystal:150 }, timeBase:120, effects:{baseDPS:50}, icon:'fa-shield-alt', description:'+50 auto-defense DPS per level.' }, storage_depot: { name:'Storage Depot', maxLevel:15, baseCost:{credits:0, metal:200 }, timeBase:60, effects:{storageBonus:2000}, icon:'fa-warehouse', description:'+2000 storage cap per level.' }, sensor_array: { name:'Sensor Array', maxLevel:10, baseCost:{credits:200, metal:300, crystal:50 }, timeBase:90, effects:{sensorRange:1}, icon:'fa-broadcast-tower',description:'Reveals sectors; level 7+ enables interdiction.' }, hangar_bay: { name:'Hangar Bay', maxLevel:10, baseCost:{credits:500, metal:1500, gas:400 }, timeBase:200, effects:{fleetSlots:1}, icon:'fa-shuttle-space', description:'Level 2/4/7/10 unlock extra fleet slots.' }, trade_port: { name:'Trade Port', maxLevel:10, baseCost:{credits:2500}, timeBase:180, effects:{tradeIncome:5}, icon:'fa-ship', description:'+5% market income per level.' }, shield_generator: { name:'Shield Generator', maxLevel:10, baseCost:{credits:4000}, timeBase:240, effects:{shieldDuration:8}, icon:'fa-circle-notch', description:'Deployable base shield +8hrs per level.' }, crystal_refinery: { name:'Crystal Refinery', maxLevel:20, baseCost:{credits:900}, timeBase:100, effects:{crystalPerHr:60}, icon:'fa-gem', description:'+12% crystal/hr per level.' }, }; function getBuildingCost(defId, currentLevel) { const def = BUILDING_DEFS[defId]; if (!def) return null; const mult = Math.pow(1.6, currentLevel); // exponential cost scaling return { credits: Math.floor((def.baseCost.credits || 0) * mult) }; } function getBuildingBuildTime(defId, currentLevel) { const def = BUILDING_DEFS[defId]; if (!def) return 60; return Math.floor(def.timeBase * Math.pow(1.8, currentLevel)); } socket.on('get_base_data', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return socket.emit('base_data', { success: false, error: 'Not authenticated' }); const pd = client.playerData; // Init buildings if missing if (!pd.buildings) pd.buildings = { command_center: { level: 1, buildQueue: null } }; // Check for completed builds const now = Date.now(); let changed = false; for (const [id, bld] of Object.entries(pd.buildings)) { if (bld.buildQueue && now >= bld.buildQueue.completesAt) { bld.level = (bld.level || 1) + 1; bld.buildQueue = null; changed = true; socket.emit('building_upgraded', { success: true, buildingId: id, newLevel: bld.level }); } } if (changed) await savePlayerData(pd.userId, pd); const buildings = Object.entries(pd.buildings).map(([id, bld]) => { const def = BUILDING_DEFS[id] || {}; const nextCost = getBuildingCost(id, bld.level || 1); const nextTime = getBuildingBuildTime(id, bld.level || 1); return { id, name: def.name || id, level: bld.level || 1, maxLevel: def.maxLevel || 10, icon: def.icon || 'fa-building', description: def.description || '', buildQueue: bld.buildQueue || null, nextCost, nextTime, effects: def.effects || {} }; }); // Available to build (not yet built) const available = Object.entries(BUILDING_DEFS) .filter(([id]) => !pd.buildings[id]) .map(([id, def]) => ({ id, name: def.name, icon: def.icon, description: def.description, maxLevel: def.maxLevel, effects: def.effects, cost: getBuildingCost(id, 0), buildTime: getBuildingBuildTime(id, 0) })); socket.emit('base_data', { success: true, buildings, available }); } catch (err) { console.error('[BASE] get_base_data error:', err); socket.emit('base_data', { success: false, error: err.message }); } }); socket.on('construct_building', async ({ buildingId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; if (!pd.buildings) pd.buildings = {}; if (pd.buildings[buildingId]) return socket.emit('building_constructed', { success: false, error: 'Already built' }); const def = BUILDING_DEFS[buildingId]; if (!def) return socket.emit('building_constructed', { success: false, error: 'Unknown building' }); const cost = getBuildingCost(buildingId, 0); // Deduct credits if ((pd.stats.credits || 0) < (cost.credits||0)) return socket.emit('building_constructed', { success: false, error: `Need ${cost.credits} credits` }); pd.stats.credits -= (cost.credits||0); // Deduct resources (GDD §6.2) resourceSystem.initResources(pd); const resCost = {}; if (cost.metal) resCost.metal = cost.metal; if (cost.gas) resCost.gas = cost.gas; if (cost.crystal) resCost.crystal = cost.crystal; if (Object.keys(resCost).length > 0) { try { resourceSystem.spend(pd, resCost); } catch(e) { pd.stats.credits += (cost.credits||0); return socket.emit('building_constructed',{success:false,error:e.message}); } } const buildTime = getBuildingBuildTime(buildingId, 0) * 1000; pd.buildings[buildingId] = { level: 0, buildQueue: { startedAt: Date.now(), completesAt: Date.now() + buildTime } }; await savePlayerData(pd.userId, pd); socket.emit('building_constructed', { success: true, buildingId, completesAt: Date.now() + buildTime }); socket.emit('economy_data', { credits: pd.stats.credits, gems: pd.stats.gems }); socket.emit('resource_update',{resources:pd.resources,rates:resourceSystem.getProductionRates(pd),caps:resourceSystem.getStorageCaps(pd)}); } catch (err) { socket.emit('building_constructed', { success: false, error: err.message }); } }); socket.on('upgrade_building', async ({ buildingId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; const bld = pd.buildings?.[buildingId]; if (!bld) return socket.emit('building_upgraded', { success: false, error: 'Building not found' }); if (bld.buildQueue) return socket.emit('building_upgraded', { success: false, error: 'Already building' }); const def = BUILDING_DEFS[buildingId]; if (bld.level >= (def?.maxLevel || 10)) return socket.emit('building_upgraded', { success: false, error: 'Already max level' }); const cost = getBuildingCost(buildingId, bld.level); if ((pd.stats.credits || 0) < (cost.credits||0)) return socket.emit('building_upgraded', { success: false, error: `Need ${cost.credits} credits` }); pd.stats.credits -= (cost.credits||0); resourceSystem.initResources(pd); const resCost = {}; if (cost.metal) resCost.metal = cost.metal; if (cost.gas) resCost.gas = cost.gas; if (cost.crystal) resCost.crystal = cost.crystal; if (Object.keys(resCost).length > 0) { try { resourceSystem.spend(pd, resCost); } catch(e) { pd.stats.credits += (cost.credits||0); return socket.emit('building_upgraded',{success:false,error:e.message}); } } const buildTime = getBuildingBuildTime(buildingId, bld.level) * 1000; bld.buildQueue = { startedAt: Date.now(), completesAt: Date.now() + buildTime }; await savePlayerData(pd.userId, pd); socket.emit('building_upgraded', { success: true, buildingId, completesAt: Date.now() + buildTime }); socket.emit('economy_data', { credits: pd.stats.credits, gems: pd.stats.gems }); socket.emit('resource_update',{resources:pd.resources,rates:resourceSystem.getProductionRates(pd),caps:resourceSystem.getStorageCaps(pd)}); } catch (err) { socket.emit('building_upgraded', { success: false, error: err.message }); } }); socket.on('start_dungeon', (data) => { console.log('[GAME SERVER] Starting dungeon for:', socket.id, data); try { const { dungeonId, userId } = data; // Check if dungeon is one-time only and already completed const dungeon = dungeonSystem.getDungeon(dungeonId); if (dungeon && dungeon.oneTimeOnly) { const completedDungeons = dungeonSystem.getPlayerCompletedDungeons(userId); if (completedDungeons.includes(dungeonId)) { socket.emit('dungeon_started', { success: false, error: 'This dungeon can only be completed once per character.' }); return; } } const instance = dungeonSystem.createInstance(dungeonId, userId, []); socket.emit('dungeon_started', { instance }); } catch (error) { console.error('[GAME SERVER] Error starting dungeon:', error); socket.emit('dungeon_started', { success: false, error: error.message }); } }); socket.on('process_encounter', (data) => { console.log('[GAME SERVER] Processing encounter for:', socket.id, data); try { const { instanceId, userId } = data; const result = dungeonSystem.startEncounter(instanceId, userId); // Auto-complete combat for enemies with 0 attack if (result.encounter.enemies && result.encounter.enemies.length > 0) { const allZeroAttack = result.encounter.enemies.every(enemy => enemy.attack === 0); if (allZeroAttack) { console.log('[GAME SERVER] Auto-combat: All enemies have 0 attack, completing encounter'); const completionResult = dungeonSystem.completeEncounter(instanceId, userId, { victory: true }); socket.emit('encounter_completed', { success: true, rewards: completionResult.rewards, nextEncounter: completionResult.nextEncounter, encounterIndex: completionResult.encounterIndex }); return; } } socket.emit('encounter_data', { encounter: result.encounter, encounterIndex: result.encounterIndex, instance: result.instance }); } catch (error) { console.error('[GAME SERVER] Error processing encounter:', error); socket.emit('encounter_data', { success: false, error: error.message }); } }); socket.on('complete_dungeon', async (data) => { console.log('[GAME SERVER] Completing dungeon for:', socket.id, data); try { const { instanceId, userId } = data; const client = connectedClients.get(socket.id); const result = dungeonSystem.completeDungeon(instanceId); // Grant item rewards + gem bonuses to player inventory if (result.success && client?.userId) { try { const playerData = await loadPlayerData(client.userId, client.username || 'Player'); if (playerData) { // Grant item rewards if (result.rewards?.length) { for (const reward of result.rewards) { const item = itemSystem.getItem(reward.itemId); if (!item) continue; itemSystem.applyPurchase(item, playerData, reward.quantity || 1); } } // Gem rewards by dungeon difficulty (GDD §11 v3.2) // extreme=1, legendary=3, boss kills add 1 bonus gem const gemsByDifficulty = { tutorial:0, easy:0, medium:0, hard:0, extreme:1, legendary:3 }; const dungeon = dungeonSystem.getDungeon ? dungeonSystem.getDungeon(instanceId) : null; const difficulty = dungeon?.difficulty || result.difficulty || 'medium'; const gemReward = gemsByDifficulty[difficulty] || 0; if (gemReward > 0) { playerData.stats.gems = (playerData.stats.gems || 0) + gemReward; result.gemsAwarded = gemReward; } await savePlayerData(client.userId, playerData); client.playerData = playerData; if (gemReward > 0) { socket.emit('economy_data', { credits: playerData.stats.credits, gems: playerData.stats.gems }); } } } catch (grantErr) { console.error('[GAME SERVER] Failed to grant dungeon rewards:', grantErr.message); } } // Log to combat log (GDD §9.5) if (client?.playerData) { socialSystem.addCombatLogEntry(client.playerData.userId, { type: 'dungeon', outcome: 'win', enemy: instanceId, xpGained: result.xpReward || 0, timestamp: new Date() }).catch(()=>{}); } socket.emit('dungeon_completed', { rewards: result }); } catch (error) { console.error('[GAME SERVER] Error completing dungeon:', error); socket.emit('dungeon_completed', { success: false, error: error.message }); } }); socket.on('get_quests', (data) => { console.log('[GAME SERVER] Getting quests for:', socket.id); try { const userId = connectedClients.get(socket.id)?.userId || 'anonymous'; const playerQD = questSystem.getPlayerQuests(userId); // Build quests with per-player progress merged in const buildList = (category) => questSystem.getQuestsByCategory(category).map(q => { const activeState = playerQD.active[q.id]; return { ...q, status: playerQD.completed.includes(q.id) ? 'completed' : activeState ? 'active' : (q.prerequisites?.playerLevelMin || 1) <= 1 ? 'available' : 'locked', objectives: q.objectives.map(obj => ({ ...obj, progress: activeState?.objectives?.[obj.id]?.progress ?? 0, complete: activeState?.objectives?.[obj.id]?.complete ?? false, })), }; }); socket.emit('quests_data', { success: true, mainQuests: buildList('main_story'), dailyQuests: buildList('daily'), weeklyQuests: buildList('weekly'), monthlyQuests:buildList('monthly'), playerState: playerQD, }); } catch (error) { console.error('[GAME SERVER] Error getting quests:', error); socket.emit('quests_data', { success: false, error: error.message }); } }); // ── quest_completed: a system or action triggers a quest completion ──── // Note: io.on doesn't exist; use a proper socket-level handler instead. // Clients can emit 'complete_quest' to finish a quest; the server validates & pushes result. socket.on('complete_quest', async (data) => { const { questId } = data || {}; const client = connectedClients.get(socket.id); if (!client?.userId) { socket.emit('quest_completed', { success: false, error: 'Not authenticated' }); return; } try { const quest = questSystem.getQuest(questId); if (!quest) { socket.emit('quest_completed', { success: false, error: 'Quest not found' }); return; } const result = questSystem.completeQuest(client.userId, questId, quest.rewards); // Resolve reward amounts into flat numbers for the client const rewardCredits = quest.rewards ? quest.rewards.filter(r => r.type === 'coin').reduce((s, r) => s + (r.amount || 0), 0) : 0; const rewardMoney = quest.rewards ? quest.rewards.filter(r => r.type === 'money').reduce((s, r) => s + (r.amount || 0), 0) : 0; // Credit the player + grant item rewards if (client.playerData) { if (rewardCredits > 0) { client.playerData.stats.credits = (client.playerData.stats.credits || 0) + rewardCredits; } client.playerData.stats.questsCompleted = (client.playerData.stats.questsCompleted || 0) + 1; // Grant item rewards (decorations, consumables, etc.) const itemRewards = quest.rewards?.filter(r => r.type === 'item') || []; for (const reward of itemRewards) { const item = itemSystem.getItem(reward.itemId); if (item) itemSystem.applyPurchase(item, client.playerData, reward.quantity || 1); } await savePlayerData(client.playerData.userId, client.playerData); } socket.emit('quest_completed', { success: true, questId, questName: quest.name, rewards: { credits: rewardCredits, money: rewardMoney, items: quest.rewards?.filter(r => r.type === 'item') || [] }, }); // Push updated economy to client socket.emit('economy_data', { credits: client.playerData?.stats?.credits || 0, gems: client.playerData?.stats?.gems || 0, }); } catch (err) { console.error('[GAME SERVER] complete_quest error:', err); socket.emit('quest_completed', { success: false, error: err.message }); } }); socket.on('next_room', (data) => { console.log('[GAME SERVER] Moving to next room for:', socket.id, data); try { const { instanceId, userId } = data; const result = dungeonSystem.moveToNextRoom(instanceId, userId); socket.emit('next_room_data', result); } catch (error) { console.error('[GAME SERVER] Error moving to next room:', error); socket.emit('next_room_data', { success: false, error: error.message }); } }); socket.on('get_dungeon_status', (data) => { console.log('[GAME SERVER] Getting dungeon status for:', socket.id, data); try { const { userId } = data; const instance = dungeonSystem.getPlayerInstance(userId); if (instance) { socket.emit('dungeon_status', { hasActiveDungeon: true, currentInstance: instance }); } else { socket.emit('dungeon_status', { hasActiveDungeon: false, currentInstance: null }); } } catch (error) { console.error('[GAME SERVER] Error getting dungeon status:', error); socket.emit('dungeon_status', { success: false, error: error.message }); } }); // ── joinServer: client emits after connecting ────────────────────────── socket.on('joinServer', (data) => { const { serverId, userId, username } = data || {}; console.log(`[GAME SERVER] joinServer from ${username} (${userId}) for server ${serverId}`); // Join a socket.io room named after the serverId so we can broadcast to it if (serverId) socket.join(serverId); const client = connectedClients.get(socket.id); if (client) { client.serverId = serverId; } // Notify room that a player joined socket.to(serverId).emit('playerJoined', { userId, username }); }); // ── getPlayerList ─────────────────────────────────────────────────────── socket.on('getPlayerList', (data) => { const { serverId } = data || {}; const players = []; for (const [sid, cd] of connectedClients) { if (cd.serverId === serverId && cd.username) { players.push({ userId: cd.userId, username: cd.username }); } } socket.emit('playerList', { players }); }); // ── chatMessage: relay to everyone in the server room ────────────────── socket.on('chatMessage', (data) => { const client = connectedClients.get(socket.id); const serverId = client?.serverId; const payload = { userId: client?.userId || data.userId, username: client?.username || data.username || 'Unknown', message: data.message || '', timestamp: Date.now(), }; if (serverId) { io.to(serverId).emit('chatMessage', payload); } else { socket.emit('chatMessage', payload); // echo back to sender only } }); // ── updatePlayerStats: client-reported stat delta (server validates) ──── socket.on('updatePlayerStats', async (data) => { const client = connectedClients.get(socket.id); if (!client?.playerData) { socket.emit('player_stat_update', { success: false, error: 'Not authenticated' }); return; } try { const pd = client.playerData; // Only trust non-economy fields from client; economy is server-authoritative const allowed = ['playTime']; for (const key of allowed) { if (data.playerStats?.[key] !== undefined) { pd.stats[key] = data.playerStats[key]; } } await savePlayerData(pd.userId, pd); socket.emit('player_stat_update', { success: true, stats: pd.stats }); } catch (err) { console.error('[GAME SERVER] updatePlayerStats error:', err); socket.emit('player_stat_update', { success: false, error: err.message }); } }); // ── exit_dungeon: player voluntarily leaves a dungeon ────────────────── socket.on('exit_dungeon', (data) => { console.log('[GAME SERVER] exit_dungeon for:', socket.id); try { const { instanceId, userId } = data || {}; if (instanceId) dungeonSystem.abandonInstance(instanceId); socket.emit('dungeon_exited', { success: true }); } catch (err) { console.error('[GAME SERVER] exit_dungeon error:', err); socket.emit('dungeon_exited', { success: false, error: err.message }); } }); // ── get_room_types: dungeon room type definitions ─────────────────────── socket.on('get_room_types', () => { socket.emit('room_types_data', dungeonSystem.roomTypes || {}); }); // ════════════════════════════════════════════════════════════════════════ // SEASON SYSTEM (GDD §20.3) // ════════════════════════════════════════════════════════════════════════ socket.on('get_season', () => { const data = seasonSystem.getCurrentSeason(); if (data.active) { const client = connectedClients.get(socket.id); if (client?.playerData) data.myScore = seasonSystem.getSeasonScore(client.playerData); } socket.emit('season_data', data); }); // ════════════════════════════════════════════════════════════════════════ // GALAXY EVENTS (GDD §20.2) // ════════════════════════════════════════════════════════════════════════ socket.on('get_galaxy_event', () => { socket.emit('galaxy_event_data', galaxyEventSystem.getEventData()); }); socket.on('claim_event_reward', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = galaxyEventSystem.claimEventReward(client.playerData); if (result.success) await savePlayerData(client.playerData.userId, client.playerData); socket.emit('event_reward_result', result); } catch(err) { socket.emit('event_reward_result',{success:false,error:err.message}); } }); // ════════════════════════════════════════════════════════════════════════ // REPUTATION HANDLERS (GDD §15.3) // ════════════════════════════════════════════════════════════════════════ socket.on('get_reputation', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; reputationSystem.initReputation(client.playerData); socket.emit('reputation_data', { success:true, reputations: reputationSystem.getReputationData(client.playerData) }); } catch(err) { socket.emit('reputation_data',{success:false,error:err.message}); } }); // ════════════════════════════════════════════════════════════════════════ // SOCIAL: FRIENDS + COMBAT LOG (GDD §17.2, §9.5) // ════════════════════════════════════════════════════════════════════════ socket.on('get_friends', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const data = await socialSystem.getFriendsList(client.playerData.userId, client.playerData.username, connectedClients); socket.emit('friends_data', { success:true, ...data }); } catch(err) { socket.emit('friends_data',{success:false,error:err.message}); } }); socket.on('add_friend', async ({ username }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = await socialSystem.sendFriendRequest(client.playerData.userId, client.playerData.username, username, connectedClients); socket.emit('friend_request_sent', { success:true, targetName: result.targetName }); // Notify target if online for (const [sid, cd] of connectedClients.entries()) { if (cd.userId === result.targetId) io.to(sid).emit('friend_request', { fromId: client.playerData.userId, fromName: client.playerData.username }); } } catch(err) { socket.emit('friend_request_sent',{success:false,error:err.message}); } }); socket.on('accept_friend', async ({ fromId, fromName }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; await socialSystem.acceptFriendRequest(client.playerData.userId, client.playerData.username, fromId, fromName); socket.emit('friend_accepted', { success:true, friendId: fromId, friendName: fromName }); for (const [sid, cd] of connectedClients.entries()) { if (cd.userId === fromId) io.to(sid).emit('friend_accepted', { friendId: client.playerData.userId, friendName: client.playerData.username }); } } catch(err) { socket.emit('friend_accepted',{success:false,error:err.message}); } }); socket.on('remove_friend', async ({ friendId }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; await socialSystem.removeFriend(client.playerData.userId, friendId); socket.emit('friend_removed', { success:true }); } catch(err) { socket.emit('friend_removed',{success:false,error:err.message}); } }); socket.on('send_gift', async ({ targetId, amount }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = await socialSystem.sendGift(client.playerData, targetId, amount); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('gift_sent', { success:true, amount }); socket.emit('economy_data',{credits:client.playerData.stats.credits,gems:client.playerData.stats.gems}); // Give credits to recipient if online for (const [sid, cd] of connectedClients.entries()) { if (cd.userId === targetId) { cd.playerData.stats.credits = (cd.playerData.stats.credits||0) + amount; io.to(sid).emit('gift_received', { fromName: client.playerData.username, amount }); io.to(sid).emit('economy_data',{credits:cd.playerData.stats.credits,gems:cd.playerData.stats.gems}); await savePlayerData(cd.userId, cd.playerData); } } } catch(err) { socket.emit('gift_sent',{success:false,error:err.message}); } }); socket.on('get_combat_log', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const log = await socialSystem.getCombatLog(client.playerData.userId); socket.emit('combat_log_data', { success:true, log }); } catch(err) { socket.emit('combat_log_data',{success:false,error:err.message}); } }); // ════════════════════════════════════════════════════════════════════════ // PLAYER MARKET HANDLERS (GDD §14) // ════════════════════════════════════════════════════════════════════════ socket.on('get_market', async ({ itemId, category } = {}) => { try { const listings = await marketSystem.getListings({ itemId, category }); const tradeRes = marketSystem.getTradeableResources(); socket.emit('market_data', { success:true, listings, tradeableResources: tradeRes }); } catch(err) { socket.emit('market_data',{success:false,error:err.message}); } }); socket.on('get_my_listings', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const listings = await marketSystem.getMyListings(client.playerData.userId); socket.emit('my_listings', { success:true, listings }); } catch(err) { socket.emit('my_listings',{success:false,error:err.message}); } }); socket.on('list_resource', async (data={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; resourceSystem.initResources(client.playerData); const result = await marketSystem.listResource(client.playerData, data); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('list_result', { success:true, listing:result.listing, listingFee:result.listingFee }); socket.emit('resource_update',{resources:client.playerData.resources,rates:resourceSystem.getProductionRates(client.playerData),caps:resourceSystem.getStorageCaps(client.playerData)}); } catch(err) { socket.emit('list_result',{success:false,error:err.message}); } }); socket.on('list_item', async (data={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = await marketSystem.listItem(client.playerData, data); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('list_result', { success:true, listing:result.listing, listingFee:result.listingFee }); } catch(err) { socket.emit('list_result',{success:false,error:err.message}); } }); socket.on('buy_listing', async ({ listingId }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = await marketSystem.buyListing(client.playerData, listingId); // Credit seller — online: immediate push; offline: persist to DB so they receive on next login let sellerCredited = false; for (const [sid, cd] of connectedClients.entries()) { if (cd.userId === result.listing.sellerId) { cd.playerData.stats.credits = (cd.playerData.stats.credits||0) + result.proceeds; io.to(sid).emit('economy_data',{credits:cd.playerData.stats.credits,gems:cd.playerData.stats.gems}); io.to(sid).emit('market_sale', { listingId, itemName: result.listing.itemName, amount: result.proceeds }); await savePlayerData(cd.userId, cd.playerData); sellerCredited = true; } } // Offline seller: load their data from DB, credit, and resave if (!sellerCredited) { try { const PlayerData = require('./models/PlayerData'); const sellerDoc = await PlayerData.findOne({ userId: result.listing.sellerId }); if (sellerDoc) { sellerDoc.stats.credits = (sellerDoc.stats.credits || 0) + result.proceeds; await sellerDoc.save(); } } catch(e) { console.warn('[market] offline seller credit error:', e.message); } } await savePlayerData(client.playerData.userId, client.playerData); socket.emit('buy_result',{success:true,listingId,proceeds:result.proceeds,itemName:result.listing.itemName}); socket.emit('economy_data',{credits:client.playerData.stats.credits,gems:client.playerData.stats.gems}); } catch(err) { socket.emit('buy_result',{success:false,error:err.message}); } }); // Price history for market sparklines (GDD §14 v3.2) socket.on('get_price_history', async ({ itemId, days = 14 } = {}) => { try { if (!itemId) return socket.emit('price_history', { error: 'itemId required' }); const history = await marketSystem.getPriceHistory(itemId, Math.min(days, 30)); socket.emit('price_history', history); } catch(err) { socket.emit('price_history', { error: err.message }); } }); socket.on('cancel_listing', async ({ listingId }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; await marketSystem.cancelListing(client.playerData, listingId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('cancel_listing_result',{success:true}); } catch(err) { socket.emit('cancel_listing_result',{success:false,error:err.message}); } }); // ════════════════════════════════════════════════════════════════════════ // ALLIANCE HANDLERS (GDD §12) // ════════════════════════════════════════════════════════════════════════ socket.on('get_alliance', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; const allianceData = pd.allianceId ? await allianceSystem.getAllianceData(pd.allianceId) : null; socket.emit('alliance_data', { success:true, alliance:allianceData, playerRank: pd.allianceRank, playerAllianceId: pd.allianceId }); } catch(err) { socket.emit('alliance_data',{success:false,error:err.message}); } }); socket.on('search_alliances', async ({ query }={}) => { try { const results = await allianceSystem.searchAlliances(query); socket.emit('alliance_search_results', { success:true, results }); } catch(err) { socket.emit('alliance_search_results',{success:false,error:err.message}); } }); socket.on('create_alliance', async ({ name, tag, description }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const alliance = await allianceSystem.createAlliance(client.playerData, { name, tag, description }); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('alliance_created', { success:true, alliance }); socket.emit('economy_data', { credits: client.playerData.stats.credits, gems: client.playerData.stats.gems }); } catch(err) { socket.emit('alliance_created',{success:false,error:err.message}); } }); socket.on('join_alliance', async ({ allianceId }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const alliance = await allianceSystem.joinAlliance(client.playerData, allianceId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('alliance_joined', { success:true, alliance }); } catch(err) { socket.emit('alliance_joined',{success:false,error:err.message}); } }); socket.on('leave_alliance', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; await allianceSystem.leaveAlliance(client.playerData); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('alliance_left', { success:true }); } catch(err) { socket.emit('alliance_left',{success:false,error:err.message}); } }); socket.on('alliance_deposit', async (data={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const warehouse = await allianceSystem.depositWarehouse(client.playerData, data); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('alliance_warehouse_update', { success:true, warehouse }); socket.emit('resource_update',{resources:client.playerData.resources,rates:resourceSystem.getProductionRates(client.playerData),caps:resourceSystem.getStorageCaps(client.playerData)}); } catch(err) { socket.emit('alliance_warehouse_update',{success:false,error:err.message}); } }); socket.on('alliance_withdraw', async (data={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const warehouse = await allianceSystem.withdrawWarehouse(client.playerData, data); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('alliance_warehouse_update', { success:true, warehouse }); } catch(err) { socket.emit('alliance_warehouse_update',{success:false,error:err.message}); } }); // Alliance Research Tree (GDD §12.3 — v3.2) socket.on('get_alliance_research', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData?.allianceId) return socket.emit('alliance_research_data', { error: 'Not in an alliance' }); const Alliance = require('./systems/AllianceSystem').Alliance || mongoose.model('Alliance'); const alliance = await Alliance.findOne({ allianceId: client.playerData.allianceId }); if (!alliance) return socket.emit('alliance_research_data', { error: 'Alliance not found' }); const tree = require('./data/gso/alliance/research_tree.json'); socket.emit('alliance_research_data', { tree, completed: alliance.research?.completed || [], inProgress: alliance.research?.inProgress || null, }); } catch(err) { socket.emit('alliance_research_data', { error: err.message }); } }); socket.on('start_alliance_research', async ({ techId } = {}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; if (!client.playerData.allianceId) return socket.emit('alliance_research_result', { success:false, error:'Not in an alliance' }); if (!['founder','officer'].includes(client.playerData.allianceRank)) return socket.emit('alliance_research_result', { success:false, error:'Only founders and officers can start research' }); const Alliance = mongoose.model('Alliance'); const alliance = await Alliance.findOne({ allianceId: client.playerData.allianceId }); if (!alliance) return socket.emit('alliance_research_result', { success:false, error:'Alliance not found' }); const tree = require('./data/gso/alliance/research_tree.json'); const tech = tree.tiers.flatMap(t => t.techs).find(t => t.id === techId); if (!tech) return socket.emit('alliance_research_result', { success:false, error:'Tech not found' }); const completed = alliance.research?.completed || []; // Check prerequisites for (const prereq of (tech.prereq || [])) { if (!completed.includes(prereq)) return socket.emit('alliance_research_result', { success:false, error:`Requires ${prereq} first` }); } if (completed.includes(techId)) return socket.emit('alliance_research_result', { success:false, error:'Already researched' }); if (alliance.research?.inProgress) return socket.emit('alliance_research_result', { success:false, error:'Research already in progress' }); // Deduct cost from warehouse const cost = tech.cost || {}; const wh = alliance.warehouse; for (const [res, amt] of Object.entries(cost)) { if (res === 'credits') { if ((wh.credits||0) < amt) return socket.emit('alliance_research_result', {success:false, error:`Need ${amt} warehouse credits`}); wh.credits -= amt; } else { if ((wh[res]||0) < amt) return socket.emit('alliance_research_result', {success:false, error:`Need ${amt} warehouse ${res}`}); wh[res] -= amt; } } const researchTimeSec = 3600; // 1 hour per tech (Phase 3 may vary) alliance.research = { ...(alliance.research||{}), completed, inProgress: { techId, startedAt: Date.now(), completesAt: Date.now() + researchTimeSec * 1000 }, }; alliance.markModified('research'); alliance.markModified('warehouse'); await alliance.save(); socket.emit('alliance_research_result', { success: true, techId, completesAt: alliance.research.inProgress.completesAt, warehouse: alliance.warehouse, }); } catch(err) { socket.emit('alliance_research_result', {success:false, error:err.message}); } }); socket.on('collect_alliance_research', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData?.allianceId) return; const Alliance = mongoose.model('Alliance'); const alliance = await Alliance.findOne({ allianceId: client.playerData.allianceId }); if (!alliance?.research?.inProgress) return socket.emit('alliance_research_result', {success:false, error:'No research in progress'}); if (Date.now() < alliance.research.inProgress.completesAt) return socket.emit('alliance_research_result', {success:false, error:'Not complete yet', remainingMs: alliance.research.inProgress.completesAt - Date.now()}); const techId = alliance.research.inProgress.techId; alliance.research.completed = [...(alliance.research.completed||[]), techId]; alliance.research.inProgress = null; alliance.markModified('research'); await alliance.save(); socket.emit('alliance_research_result', { success:true, completed: techId, allCompleted: alliance.research.completed }); } catch(err) { socket.emit('alliance_research_result', {success:false, error:err.message}); } }); // ════════════════════════════════════════════════════════════════════════ // FLEET MISSION HANDLERS (GDD §8.3) // ════════════════════════════════════════════════════════════════════════ socket.on('get_missions', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; // Auto-complete any finished missions const results = missionSystem.collectMissions(pd, resourceSystem); if (results.length > 0) { await savePlayerData(pd.userId, pd); for (const r of results) socket.emit('mission_completed', r); } socket.emit('missions_data', missionSystem.getMissionsForPlayer(pd)); } catch(err) { console.error('[MISSIONS]', err); } }); socket.on('start_mission', async ({ missionType, fleetShipIds } = {}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const mission = missionSystem.startMission(client.playerData, { missionType, fleetShipIds }); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('mission_started', { success: true, mission }); socket.emit('missions_data', missionSystem.getMissionsForPlayer(client.playerData)); } catch(err) { socket.emit('mission_started', { success: false, error: err.message }); } }); socket.on('collect_missions', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; const results = missionSystem.collectMissions(pd, resourceSystem); await savePlayerData(pd.userId, pd); socket.emit('missions_collected', { results, missions: missionSystem.getMissionsForPlayer(pd) }); if (results.some(r => r.rewards?.metal || r.rewards?.gas)) { socket.emit('resource_update', { resources:pd.resources, rates:resourceSystem.getProductionRates(pd), caps:resourceSystem.getStorageCaps(pd) }); } } catch(err) { console.error('[COLLECT_MISSIONS]', err); } }); socket.on('get_faction_missions', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; // Auto-collect completed faction missions first const results = missionSystem.collectFactionMissions(pd, resourceSystem, reputationSystem); if (results.length > 0) { await savePlayerData(pd.userId, pd); results.forEach(r => socket.emit('mission_completed', r)); } socket.emit('faction_missions_data', { success: true, available: missionSystem.getAvailableFactionMissions(pd), active: (pd.fleetMissions||[]).filter(m => m.type==='faction'), }); } catch(err) { socket.emit('faction_missions_data',{success:false,error:err.message}); } }); socket.on('start_faction_mission', async ({ missionId }={}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const job = missionSystem.startFactionMission(client.playerData, missionId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('faction_mission_started', { success:true, job }); } catch(err) { socket.emit('faction_mission_started',{success:false,error:err.message}); } }); socket.on('recall_mission', async ({ missionId } = {}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; missionSystem.recallMission(client.playerData, missionId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('missions_data', missionSystem.getMissionsForPlayer(client.playerData)); } catch(err) { socket.emit('recall_mission_result', { success:false, error:err.message }); } }); // ════════════════════════════════════════════════════════════════════════ // RESOURCE HANDLERS (GDD §5) // ════════════════════════════════════════════════════════════════════════ socket.on('get_resources', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; resourceSystem.initResources(client.playerData); // Run a tick to credit any offline production resourceSystem.tick(client.playerData); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('resource_update', { resources: client.playerData.resources, rates: resourceSystem.getProductionRates(client.playerData), caps: resourceSystem.getStorageCaps(client.playerData), config: resourceSystem.getConfig(), }); } catch (err) { console.error('[RESOURCES] get_resources error:', err); } }); // ════════════════════════════════════════════════════════════════════════ // SHIP CONSTRUCTION QUEUE (GDD §7.4) // ════════════════════════════════════════════════════════════════════════ const SHIP_BLUEPRINTS = { starter_cruiser: { name:'Starter Cruiser', icon:'🚀', rarity:'common', hull:100, attack:10, defense:5, speed:15, buildTime:30, metalCost:200, gasCost:50, crystalCost:0, level:1 }, light_fighter: { name:'Light Fighter', icon:'✈', rarity:'uncommon', hull:80, attack:18, defense:3, speed:25, buildTime:60, metalCost:400, gasCost:150, crystalCost:50, level:3 }, heavy_bomber: { name:'Heavy Bomber', icon:'💣', rarity:'uncommon', hull:160, attack:25, defense:8, speed:10, buildTime:120, metalCost:800, gasCost:200, crystalCost:100, level:5 }, destroyer: { name:'Destroyer', icon:'⚔', rarity:'rare', hull:200, attack:30, defense:15, speed:18, buildTime:300, metalCost:1500, gasCost:500, crystalCost:300, level:8 }, heavy_cruiser: { name:'Heavy Cruiser', icon:'🛸', rarity:'rare', hull:350, attack:40, defense:25, speed:12, buildTime:600, metalCost:3000, gasCost:1000, crystalCost:800, level:12 }, battleship: { name:'Battleship', icon:'🌟', rarity:'epic', hull:600, attack:60, defense:45, speed:8, buildTime:1800, metalCost:8000, gasCost:3000, crystalCost:2000,level:20 }, }; function getShipyardLevel(playerData) { return playerData.buildings?.shipyard?.level || 0; } function getShipQueueMax(playerData) { const sy = getShipyardLevel(playerData); return sy >= 1 ? Math.min(3, 1 + Math.floor(sy / 3)) : 0; } function checkShipQueue(playerData) { const q = playerData.shipQueue || []; const now = Date.now(); let changed = false; const completed = []; const remaining = []; for (const job of q) { if (job.completesAt <= now) { // Add ship to inventory const bp = SHIP_BLUEPRINTS[job.shipId]; if (bp) { if (!playerData.inventory) playerData.inventory = []; playerData.inventory.push({ id: job.shipId + '_' + Date.now(), type: 'ship', shipId: job.shipId, name: bp.name, icon: bp.icon, rarity: bp.rarity, stats: { hull: bp.hull, maxHull: bp.hull, attack: bp.attack, defense: bp.defense, speed: bp.speed, currentHull: bp.hull } }); completed.push({ shipId: job.shipId, name: bp.name }); changed = true; } } else { remaining.push(job); } } playerData.shipQueue = remaining; return { changed, completed }; } socket.on('get_shipyard', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; checkShipQueue(pd); const ships = (pd.inventory || []).filter(i => i.type === 'ship'); socket.emit('shipyard_data', { success: true, blueprints: SHIP_BLUEPRINTS, queue: pd.shipQueue || [], ships, shipyardLevel: getShipyardLevel(pd), queueMax: getShipQueueMax(pd), activeShipId: pd.stats?.activeShipId, resources: pd.resources, }); } catch(err) { console.error('[SHIPYARD]', err); } }); socket.on('build_ship', async ({ shipId } = {}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; const bp = SHIP_BLUEPRINTS[shipId]; if (!bp) return socket.emit('build_ship_result', { success:false, error:'Unknown blueprint' }); const syLevel = getShipyardLevel(pd); if (syLevel < 1) return socket.emit('build_ship_result', { success:false, error:'Build a Shipyard first (Base → Buildings)' }); if ((pd.stats?.level || 1) < bp.level) return socket.emit('build_ship_result', { success:false, error:`Requires Commander Level ${bp.level}` }); const q = pd.shipQueue || []; const qMax = getShipQueueMax(pd); if (q.length >= qMax) return socket.emit('build_ship_result', { success:false, error:`Queue full (${qMax} slots). Upgrade Shipyard for more.` }); // Deduct resources resourceSystem.initResources(pd); try { resourceSystem.spend(pd, { metal: bp.metalCost, gas: bp.gasCost, crystal: bp.crystalCost }); } catch(e) { return socket.emit('build_ship_result', { success:false, error: e.message }); } const now = Date.now(); const job = { shipId, name:bp.name, icon:bp.icon, startedAt:now, completesAt: now + bp.buildTime*1000 }; pd.shipQueue = [...q, job]; await savePlayerData(pd.userId, pd); socket.emit('build_ship_result', { success:true, job, resources:pd.resources }); socket.emit('resource_update', { resources:pd.resources, rates:resourceSystem.getProductionRates(pd), caps:resourceSystem.getStorageCaps(pd) }); } catch(err) { console.error('[BUILD_SHIP]', err); socket.emit('build_ship_result',{success:false,error:'Server error'}); } }); socket.on('cancel_ship_build', async ({ shipId, startedAt } = {}) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const pd = client.playerData; const idx = (pd.shipQueue||[]).findIndex(j => j.shipId===shipId && j.startedAt===startedAt); if (idx < 0) return socket.emit('cancel_ship_result',{success:false,error:'Job not found'}); const job = pd.shipQueue[idx]; const bp = SHIP_BLUEPRINTS[job.shipId]; // Refund 75% if (bp) resourceSystem.add(pd, { metal:Math.floor(bp.metalCost*.75), gas:Math.floor(bp.gasCost*.75), crystal:Math.floor(bp.crystalCost*.75) }); pd.shipQueue.splice(idx,1); await savePlayerData(pd.userId, pd); socket.emit('cancel_ship_result',{success:true}); socket.emit('resource_update',{resources:pd.resources,rates:resourceSystem.getProductionRates(pd),caps:resourceSystem.getStorageCaps(pd)}); } catch(err) { console.error('[CANCEL_SHIP]',err); } }); // ════════════════════════════════════════════════════════════════════════ // LEADERBOARD HANDLER // ════════════════════════════════════════════════════════════════════════ socket.on('get_leaderboard', async ({ category = 'level' } = {}) => { try { const VALID_CATEGORIES = ['level', 'credits', 'questsCompleted', 'dungeonsCleared', 'totalKills']; if (!VALID_CATEGORIES.includes(category)) { return socket.emit('leaderboard_data', { success: false, error: 'Invalid category' }); } // Query top 20 players for the requested stat const sortField = `stats.${category}`; const players = await PlayerData.find( { [`stats.${category}`]: { $exists: true, $gt: 0 } }, { 'stats.username': 1, username: 1, [`stats.${category}`]: 1, 'stats.level': 1, updatedAt: 1 } ) .sort({ [sortField]: -1 }) .limit(20) .lean(); const myUserId = connectedClients.get(socket.id)?.userId; const entries = players.map((p, i) => ({ rank: i + 1, username: p.username || p.stats?.username || 'Unknown Commander', value: p.stats?.[category] || 0, level: p.stats?.level || 1, isMe: p.userId === myUserId || p._id?.toString() === myUserId, })); socket.emit('leaderboard_data', { success: true, category, entries }); } catch (err) { console.error('[LEADERBOARD] get_leaderboard error:', err); socket.emit('leaderboard_data', { success: false, error: err.message }); } }); // ════════════════════════════════════════════════════════════════════════ // FLEET HANDLERS // ════════════════════════════════════════════════════════════════════════ socket.on('get_fleet_data', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return socket.emit('fleet_data', { success: false, error: 'Not authenticated' }); // Auto-give starter ship if inventory has no ships const ships = (client.playerData.inventory || []).filter(i => i.type === 'ship'); if (ships.length === 0) { const starterShip = { id: `ship_starter_${Date.now()}`, name: 'Starter Cruiser', type: 'ship', rarity: 'common', texture: 'assets/gso/textures/ships/starter_cruiser_common.png', stats: { attack: 10, defense: 5, speed: 10, hull: 100, maxHull: 100, currentHull: 100 }, level: 1, equipped: false, }; client.playerData.inventory = client.playerData.inventory || []; client.playerData.inventory.push(starterShip); client.playerData.stats.activeShipId = starterShip.id; await savePlayerData(client.playerData.userId, client.playerData); } const fleetData = fleetSystem.getFleetData(client.playerData); const templates = fleetSystem.getAllShipTemplates(); socket.emit('fleet_data', { success: true, ...fleetData, templates }); } catch (err) { console.error('[FLEET] get_fleet_data error:', err); socket.emit('fleet_data', { success: false, error: err.message }); } }); socket.on('set_active_ship', async ({ shipId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const stats = fleetSystem.setActiveShip(client.playerData, shipId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('active_ship_set', { success: true, shipId, stats }); } catch (err) { socket.emit('active_ship_set', { success: false, error: err.message }); } }); socket.on('repair_ship', async ({ shipId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const credits = client.playerData.stats?.credits || 0; const result = fleetSystem.repairShip(client.playerData, shipId, credits); client.playerData.stats.credits -= result.repairCost; await savePlayerData(client.playerData.userId, client.playerData); socket.emit('ship_repaired', { success: true, shipId, repairCost: result.repairCost }); socket.emit('economy_data', { credits: client.playerData.stats.credits, gems: client.playerData.stats.gems }); } catch (err) { socket.emit('ship_repaired', { success: false, error: err.message }); } }); // ════════════════════════════════════════════════════════════════════════ // GALAXY MAP HANDLERS // ════════════════════════════════════════════════════════════════════════ socket.on('get_galaxy_map', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; // Seed home sector if first time if (!client.playerData.homeSector) { client.playerData.homeSector = '15_10'; client.playerData.exploredSectors = ['15_10']; await savePlayerData(client.playerData.userId, client.playerData); } const visibleSectors = galaxySystem.getVisibleSectors(client.playerData); socket.emit('galaxy_map_data', { success: true, sectors: visibleSectors, homeSector: client.playerData.homeSector, gridW: galaxySystem.GRID_W, gridH: galaxySystem.GRID_H, }); } catch (err) { console.error('[GALAXY] get_galaxy_map error:', err); socket.emit('galaxy_map_data', { success: false, error: err.message }); } }); socket.on('explore_sector', async ({ sectorId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const sector = galaxySystem.exploreSector(sectorId, client.playerData); await savePlayerData(client.playerData.userId, client.playerData); // XP reward for first explore const xpGain = 50 + sector.threat * 10; client.playerData.stats.experience = (client.playerData.stats.experience || 0) + xpGain; socket.emit('sector_explored', { success: true, sector, xpGain }); // Push updated visible sectors const visibleSectors = galaxySystem.getVisibleSectors(client.playerData); socket.emit('galaxy_map_data', { success: true, sectors: visibleSectors, homeSector: client.playerData.homeSector, gridW: galaxySystem.GRID_W, gridH: galaxySystem.GRID_H, }); } catch (err) { socket.emit('sector_explored', { success: false, error: err.message }); } }); // ════════════════════════════════════════════════════════════════════════ // RESEARCH HANDLERS // ════════════════════════════════════════════════════════════════════════ socket.on('get_research_data', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; // Check for completed research first const justCompleted = researchSystem.checkCompletion(client.playerData); if (justCompleted) { await savePlayerData(client.playerData.userId, client.playerData); socket.emit('research_completed', { tech: justCompleted }); } const available = researchSystem.getAvailableResearch(client.playerData); socket.emit('research_data', { success: true, research: available, inProgress: client.playerData.research?.inProgress || null, completed: client.playerData.research?.completed || [], effects: client.playerData.research?.effects || {}, }); } catch (err) { console.error('[RESEARCH] get_research_data error:', err); socket.emit('research_data', { success: false, error: err.message }); } }); socket.on('start_research', async ({ techId }) => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const result = researchSystem.startResearch(client.playerData, techId); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('research_started', { success: true, tech: result.tech, completesAt: result.completesAt }); socket.emit('economy_data', { credits: client.playerData.stats.credits, gems: client.playerData.stats.gems }); } catch (err) { socket.emit('research_started', { success: false, error: err.message }); } }); socket.on('cancel_research', async () => { try { const client = connectedClients.get(socket.id); if (!client?.playerData) return; const tech = researchSystem.cancelResearch(client.playerData); await savePlayerData(client.playerData.userId, client.playerData); socket.emit('research_cancelled', { success: true, tech }); socket.emit('economy_data', { credits: client.playerData.stats.credits, gems: client.playerData.stats.gems }); } catch (err) { socket.emit('research_cancelled', { success: false, error: err.message }); } }); // ════════════════════════════════════════════════════════════════════════ // SHIP MODULE SYSTEM (GDD §7.3) // ════════════════════════════════════════════════════════════════════════ // Get equipped modules for active ship socket.on('get_ship_modules', async () => { if (!socket.userId) return; try { const pd = await PlayerData.findOne({ userId: socket.userId }); if (!pd) return; const ship = pd.ship || {}; socket.emit('ship_modules_data', { shipId: ship.id || ship.shipId || null, modules: ship.modules || {}, availableSlots: _getShipSlots(ship), }); } catch (err) { console.error('[MODULES] get_ship_modules error:', err); } }); // Equip a module item into a slot socket.on('equip_module', async ({ itemId, slot } = {}) => { if (!socket.userId) return socket.emit('equip_result', { success: false, error: 'Not authenticated' }); if (!itemId || !slot) return socket.emit('equip_result', { success: false, error: 'Missing itemId or slot' }); try { const pd = await PlayerData.findOne({ userId: socket.userId }); if (!pd) return socket.emit('equip_result', { success: false, error: 'Player not found' }); // Find item in inventory const invItems = pd.inventory?.items || []; const itemIdx = invItems.findIndex(it => it && (it.id === itemId || it.itemId === itemId)); if (itemIdx === -1) return socket.emit('equip_result', { success: false, error: 'Item not in inventory' }); const item = invItems[itemIdx]; const ship = pd.ship || {}; if (!ship.modules) ship.modules = {}; // Unequip whatever is currently in that slot back to inventory const currentlyEquipped = ship.modules[slot]; if (currentlyEquipped) { invItems.push({ ...currentlyEquipped, source: 'unequipped', obtainedAt: Date.now() }); } // Remove item from inventory and equip invItems.splice(itemIdx, 1); ship.modules[slot] = { ...item, equippedAt: Date.now() }; pd.ship = ship; pd.markModified('ship'); pd.markModified('inventory'); await pd.save(); socket.emit('equip_result', { success: true, slot, equipped: ship.modules[slot], unequipped: currentlyEquipped || null, }); socket.emit('ship_modules_data', { shipId: ship.id || ship.shipId || null, modules: ship.modules, availableSlots: _getShipSlots(ship), }); socket.emit('inventory_update', { items: invItems, maxSize: pd.inventory?.maxSize || 50 }); console.log(`[MODULES] ${socket.username} equipped ${itemId} in slot ${slot}`); } catch (err) { console.error('[MODULES] equip_module error:', err); socket.emit('equip_result', { success: false, error: 'Server error' }); } }); // Unequip a module from a slot back to inventory socket.on('unequip_module', async ({ slot } = {}) => { if (!socket.userId || !slot) return; try { const pd = await PlayerData.findOne({ userId: socket.userId }); if (!pd) return; const ship = pd.ship || {}; if (!ship.modules?.[slot]) return socket.emit('equip_result', { success: false, error: 'Nothing equipped in that slot' }); const item = ship.modules[slot]; delete ship.modules[slot]; const invItems = pd.inventory?.items || []; invItems.push({ ...item, source: 'unequipped', obtainedAt: Date.now() }); pd.ship = ship; pd.markModified('ship'); pd.markModified('inventory'); await pd.save(); socket.emit('equip_result', { success: true, slot, unequipped: item }); socket.emit('ship_modules_data', { shipId: ship.id || null, modules: ship.modules, availableSlots: _getShipSlots(ship) }); socket.emit('inventory_update', { items: invItems, maxSize: pd.inventory?.maxSize || 50 }); } catch (err) { console.error('[MODULES] unequip_module error:', err); } }); // ════════════════════════════════════════════════════════════════════════ // PVP CHALLENGE SYSTEM (GDD §9.4) // ════════════════════════════════════════════════════════════════════════ // Challenge another online player socket.on('pvp_challenge', async ({ targetUsername } = {}) => { if (!socket.userId || !targetUsername) return; if (targetUsername === socket.username) return socket.emit('pvp_result', { success: false, error: 'Cannot challenge yourself' }); // Find target socket const targetEntry = [...connectedClients.entries()].find(([, c]) => c.username === targetUsername); if (!targetEntry) return socket.emit('pvp_result', { success: false, error: 'Player not online' }); const [targetSocketId, targetClient] = targetEntry; const challengeId = `pvp_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; // Store pending challenge if (!pvpChallenges) pvpChallenges = new Map(); pvpChallenges.set(challengeId, { challengerId: socket.userId, challengerName: socket.username, targetId: targetClient.userId, targetName: targetUsername, createdAt: Date.now(), status: 'pending', }); // Notify challenger socket.emit('pvp_challenge_sent', { challengeId, targetUsername, message: `Challenge sent to ${targetUsername}!` }); // Notify target io.to(targetSocketId).emit('pvp_challenge_received', { challengeId, challengerName: socket.username, message: `${socket.username} challenges you to PvP combat!`, }); // Auto-expire after 30 seconds setTimeout(() => { if (pvpChallenges && pvpChallenges.get(challengeId)?.status === 'pending') { pvpChallenges.delete(challengeId); socket.emit('pvp_challenge_expired', { challengeId }); io.to(targetSocketId).emit('pvp_challenge_expired', { challengeId }); } }, 30000); }); // Accept a PvP challenge socket.on('pvp_accept', async ({ challengeId } = {}) => { if (!socket.userId || !challengeId) return; if (!pvpChallenges?.has(challengeId)) return socket.emit('pvp_result', { success: false, error: 'Challenge not found or expired' }); const challenge = pvpChallenges.get(challengeId); if (challenge.targetId !== socket.userId) return socket.emit('pvp_result', { success: false, error: 'Not your challenge' }); if (challenge.status !== 'pending') return socket.emit('pvp_result', { success: false, error: 'Challenge already resolved' }); challenge.status = 'active'; try { // Load both players const [challenger, defender] = await Promise.all([ PlayerData.findOne({ userId: challenge.challengerId }), PlayerData.findOne({ userId: challenge.targetId }), ]); if (!challenger || !defender) return socket.emit('pvp_result', { success: false, error: 'Player data not found' }); // Simulate PvP battle const result = _simulatePvpBattle(challenger, defender); // Apply outcomes const winnerId = result.winner === 'challenger' ? challenge.challengerId : challenge.targetId; const loserId = result.winner === 'challenger' ? challenge.targetId : challenge.challengerId; const winnerPd = result.winner === 'challenger' ? challenger : defender; const loserPd = result.winner === 'challenger' ? defender : challenger; // Rewards const creditsWon = Math.floor(100 + (loserPd.level || 1) * 20); const xpWon = Math.floor(50 + (loserPd.level || 1) * 10); winnerPd.credits = (winnerPd.credits || 0) + creditsWon; winnerPd.experience = (winnerPd.experience || 0) + xpWon; // Record in combat log const battleLog = { type: 'pvp', opponent: result.winner === 'challenger' ? challenge.targetName : challenge.challengerName, result: 'victory', creditsWon, xpWon, rounds: result.rounds, timestamp: Date.now(), }; await Promise.all([winnerPd.save(), loserPd.save()]); pvpChallenges.delete(challengeId); // Find challenger socket const challengerEntry = [...connectedClients.entries()].find(([, c]) => c.userId === challenge.challengerId); const battleSummary = { success: true, challengeId, winner: result.winner === 'challenger' ? challenge.challengerName : challenge.targetName, loser: result.winner === 'challenger' ? challenge.targetName : challenge.challengerName, rounds: result.rounds, creditsWon, xpWon, }; socket.emit('pvp_result', { ...battleSummary, youWon: challenge.targetId === winnerId }); if (challengerEntry) { io.to(challengerEntry[0]).emit('pvp_result', { ...battleSummary, youWon: challenge.challengerId === winnerId }); } console.log(`[PVP] ${challenge.challengerName} vs ${challenge.targetName} — winner: ${battleSummary.winner}`); } catch (err) { console.error('[PVP] pvp_accept error:', err); pvpChallenges.delete(challengeId); socket.emit('pvp_result', { success: false, error: 'Battle error' }); } }); // Decline a PvP challenge socket.on('pvp_decline', ({ challengeId } = {}) => { if (!challengeId || !pvpChallenges?.has(challengeId)) return; const challenge = pvpChallenges.get(challengeId); pvpChallenges.delete(challengeId); const challengerEntry = [...connectedClients.entries()].find(([, c]) => c.userId === challenge.challengerId); if (challengerEntry) { io.to(challengerEntry[0]).emit('pvp_declined', { challengeId, targetName: challenge.targetName }); } socket.emit('pvp_result', { success: false, declined: true }); }); // ════════════════════════════════════════════════════════════════════════ // SEASON LEADERBOARD (GDD §20.3) // ════════════════════════════════════════════════════════════════════════ socket.on('get_season_leaderboard', async ({ limit = 50 } = {}) => { try { const season = seasonSystem.getCurrentSeason(); if (!season.active) return socket.emit('season_leaderboard_data', { entries: [], season: null }); // Get top players by credits+level score const players = await PlayerData.find({}) .select('username level credits experience reputation fleetMissions') .limit(200) .lean(); const scored = players .map(p => ({ username: p.username || 'Unknown', level: p.level || 1, score: seasonSystem.getSeasonScore(p), credits: p.credits || 0, })) .sort((a, b) => b.score - a.score) .slice(0, limit) .map((p, i) => ({ rank: i + 1, ...p })); socket.emit('season_leaderboard_data', { entries: scored, season: season.name, endsAt: season.endsAt, }); } catch (err) { console.error('[SEASON LEADERBOARD] error:', err); socket.emit('season_leaderboard_data', { entries: [], season: null }); } }); socket.on('disconnect', async () => { console.log('[GAME SERVER] Client disconnected:', socket.id); const clientData = connectedClients.get(socket.id); if (clientData) { // Save player data before disconnect if (clientData.playerData && clientData.userId) { try { // Final resource tick to capture any partial production resourceSystem.initResources(clientData.playerData); resourceSystem.tick(clientData.playerData); clientData.playerData.leaveServer(); await savePlayerData(clientData.userId, clientData.playerData); console.log(`[GAME SERVER] Saved data for ${clientData.username} on disconnect`); } catch (error) { console.error('[GAME SERVER] Error saving data on disconnect:', error); } } // Handle game instance cleanup if (clientData.instanceId) { const instance = gameInstances.get(clientData.instanceId); if (instance) { instance.players.delete(socket.id); // Clean up empty instances if (instance.players.size === 0) { gameInstances.delete(clientData.instanceId); } // Notify other players socket.to(clientData.instanceId).emit('playerLeftInstance', { playerId: socket.id, playerCount: instance.players.size }); } } } connectedClients.delete(socket.id); // Update player count on API server updatePlayerCountOnAPI(); }); }); // Load player data from database async function loadPlayerData(userId, username) { try { // Check if database is connected if (mongoose.connection.readyState !== 1) { console.error('[GAME SERVER] Database not connected, cannot load player data'); return null; } let playerData = await PlayerData.findOne({ userId }); if (!playerData) { // Create new player data with initialized systems console.log(`[GAME SERVER] Creating new player data for ${username} with system initialization`); // Initialize player data in all systems const questData = questSystem.initializePlayerData(userId); const skillData = skillSystem.initializePlayerData(userId); const craftingData = craftingSystem.initializePlayerData(userId); // For dungeons, initialize with available dungeons const availableDungeons = dungeonSystem.getAllDungeons(); const dungeonData = { availableDungeons: availableDungeons.map(d => ({ id: d.id, name: d.name, difficulty: d.difficulty, minLevel: d.minLevel || 1, maxPlayers: d.maxPlayers || 4, description: d.description })), completedDungeons: [], currentInstance: null, dungeonProgress: {} }; playerData = new PlayerData({ userId, username, resources: { metal: 500, gas: 200, crystal: 100, energyCells: 300, darkMatter: 0, lastTick: Date.now() }, stats: { level: 1, experience: 0, totalExperience: 0, credits: 1000, gems: 50, // Give some starting gems skillPoints: 0, totalKills: 0, dungeonsCleared: 0, questsCompleted: 0, playTime: 0, lastLogin: new Date() }, // Initialize with system data quests: { active: Array.from(questData.activeQuests.values()), completed: Array.from(questData.completedQuests.keys()) }, skills: skillData, dungeonSystem: dungeonData, crafting: craftingData }); await playerData.save(); console.log(`[GAME SERVER] Created new player data for ${username} with ${questData.activeQuests.size} active quests`); } else { // Auto-migration for existing players - check for missing data console.log(`[GAME SERVER] Loading existing player data for ${username} (Level ${playerData.stats.level})`); let migrated = false; let migrationLog = []; // Always categorize quests to ensure proper format console.log(`[GAME SERVER] Categorizing quests for ${username}`); console.log(`[GAME SERVER] Current playerData.quests before categorization:`, playerData.quests); const questData = questSystem.getPlayerData(userId); console.log(`[GAME SERVER] QuestSystem data:`, { activeQuestsCount: questData.activeQuests.size, completedQuestsCount: questData.completedQuests.size, activeQuests: Array.from(questData.activeQuests.values()) }); // Categorize quests by type for client const activeQuests = Array.from(questData.activeQuests.values()); const mainQuests = activeQuests.filter(quest => quest.type === 'main'); const dailyQuests = activeQuests.filter(quest => quest.type === 'daily'); const weeklyQuests = activeQuests.filter(quest => quest.type === 'weekly'); const tutorialQuests = activeQuests.filter(quest => quest.type === 'tutorial'); playerData.quests = { main: mainQuests, daily: dailyQuests, weekly: weeklyQuests, tutorial: tutorialQuests, active: activeQuests, completed: Array.from(questData.completedQuests.keys()) }; console.log(`[GAME SERVER] Quest categorization complete:`, { main: mainQuests.length, daily: dailyQuests.length, weekly: weeklyQuests.length, tutorial: tutorialQuests.length, total: activeQuests.length }); console.log(`[GAME SERVER] Final playerData.quests after categorization:`, playerData.quests); // IMPORTANT: Save the updated quest data to database try { await savePlayerData(userId, playerData); console.log(`[GAME SERVER] Saved categorized quest data for ${username}`); } catch (error) { console.error(`[GAME SERVER] Failed to save quest data for ${username}:`, error); } // Migrate skills if missing or empty if (!playerData.skills || Object.keys(playerData.skills).length === 0) { console.log(`[GAME SERVER] Migrating skills for ${username}`); const skillData = skillSystem.initializePlayerData(userId); playerData.skills = skillData; migrated = true; migrationLog.push('Added skill data'); } // Migrate dungeons if missing or empty if (!playerData.dungeonSystem || !playerData.dungeonSystem.availableDungeons || (playerData.dungeonSystem.availableDungeons && playerData.dungeonSystem.availableDungeons.length === 0)) { console.log(`[GAME SERVER] Migrating dungeons for ${username}`); const availableDungeons = dungeonSystem.getAllDungeons(); playerData.dungeonSystem = { availableDungeons: availableDungeons.map(d => ({ id: d.id, name: d.name, difficulty: d.difficulty, minLevel: d.minLevel || 1, maxPlayers: d.maxPlayers || 4, description: d.description })), completedDungeons: [], currentInstance: null, dungeonProgress: {} }; migrated = true; migrationLog.push(`Added ${availableDungeons.length} available dungeons`); } // Migrate crafting if missing or empty (this field might not exist at all) if (!playerData.crafting || Object.keys(playerData.crafting).length === 0) { console.log(`[GAME SERVER] Migrating crafting for ${username}`); const craftingData = craftingSystem.initializePlayerData(userId); playerData.crafting = craftingData; migrated = true; migrationLog.push('Added crafting data'); } // Initialize idle system data const idleData = idleSystem.initializePlayerData(userId); if (!playerData.idleSystem) { playerData.idleSystem = { lastActive: new Date().toISOString(), totalOfflineTime: 0, totalIdleCredits: 0 }; migrated = true; migrationLog.push('Added idle system data'); } // Save if migration occurred if (migrated) { await playerData.save(); console.log(`[GAME SERVER] Migration completed for ${username}: ${migrationLog.join(', ')}`); } // Update existing player login info playerData.username = username; playerData.lastLogin = new Date(); await playerData.save(); } return playerData; } catch (error) { console.error('[GAME SERVER] Error loading player data:', error); return null; } } // Save player data to database async function savePlayerData(userId, playerData) { try { // Check if database is connected if (mongoose.connection.readyState !== 1) { console.error('[GAME SERVER] Database not connected, cannot save player data'); return false; } await PlayerData.findOneAndUpdate( { userId }, playerData, { upsert: true, new: true } ); console.log(`[GAME SERVER] Saved player data for ${playerData.username}`); return true; } catch (error) { console.error('[GAME SERVER] Error saving player data:', error); return false; } } // Update player count on API server async function updatePlayerCountOnAPI() { try { const apiServerUrl = 'http://localhost:3001'; const currentPlayers = connectedClients.size; console.log(`[GAME SERVER] Updating player count: ${currentPlayers} players`); const response = await fetch(`${apiServerUrl}/api/servers/update-status/devgame-server-3002`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPlayers: currentPlayers, status: currentPlayers > 0 ? 'active' : 'waiting' }) }); if (response.ok) { const result = await response.json(); console.log(`[GAME SERVER] Player count updated:`, result); } else { console.error(`[GAME SERVER] Failed to update player count: ${response.status}`); } } catch (error) { console.error('[GAME SERVER] Error updating player count:', error); } } // Start server const PORT = process.env.PORT || 3002; async function startServer() { try { console.log('[GAME SERVER] Attempting to connect to database...'); // Connect to database await connectDB(); console.log('[GAME SERVER] Database connection established'); server.listen(PORT, async () => { console.log(`[GAME SERVER] Game server running on port ${PORT}`); console.log(`[GAME SERVER] Socket.IO ready for multiplayer connections`); // Register with API server await registerWithAPIServer(); // Start periodic player count updates setInterval(updatePlayerCountOnAPI, 30000); // Update every 30 seconds // Start online idle rewards generation (every 10 seconds) setInterval(async () => { console.log('[GAME SERVER] Idle reward timer triggered - checking', connectedClients.size, 'connected clients'); for (const [socketId, clientData] of connectedClients.entries()) { if (clientData.userId && clientData.playerData) { try { console.log('[GAME SERVER] Processing idle rewards for client:', socketId, 'user:', clientData.username); // Update playTime for active players const sessionTime = clientData.playerData.updatePlayTime(); console.log(`[GAME SERVER] Updated playTime for ${clientData.username}: +${sessionTime}ms, Total: ${clientData.playerData.stats.playTime}ms`); // Send playTime update to client io.to(socketId).emit('playTimeUpdated', { playTime: clientData.playerData.stats.playTime, sessionTime: sessionTime }); const onlineRewards = idleSystem.generateOnlineIdleRewards(clientData.userId, 10000); // 10 seconds console.log('[GAME SERVER] Generated online rewards for', clientData.username, ':', onlineRewards); if (onlineRewards.credits > 0) { // Update player data with online rewards clientData.playerData.stats.credits = (clientData.playerData.stats.credits || 0) + onlineRewards.credits; clientData.playerData.stats.experience = (clientData.playerData.stats.experience || 0) + onlineRewards.experience; console.log('[GAME SERVER] Applied idle rewards - Credits:', onlineRewards.credits, 'New balance:', clientData.playerData.stats.credits); // Send update to client io.to(socketId).emit('onlineIdleRewards', { credits: onlineRewards.credits, experience: onlineRewards.experience, newBalance: clientData.playerData.stats.credits, playTime: clientData.playerData.stats.playTime }); console.log('[GAME SERVER] Sent onlineIdleRewards to client:', socketId); } else { console.log('[GAME SERVER] No idle rewards generated for', clientData.username); } } catch (error) { console.error(`[GAME SERVER] Error generating online idle rewards for ${socketId}:`, error); } } } }, 10000); // Every 10 seconds // ── Market Tick: expire old listings every 15 min (GDD §18.2) ──────────── setInterval(async () => { try { const n = await marketSystem.expireListings(); if(n>0) console.log(`[MARKET TICK] Expired ${n} listings`); } catch(e) { console.error('[MARKET TICK]', e.message); } }, 15 * 60 * 1000); // ── Auto-collect missions on login (handled in get_missions) ───────────── // ── Economy Tick: resource production every 60s (GDD §18.2) ──────────── setInterval(async () => { for (const [socketId, clientData] of connectedClients.entries()) { if (!clientData.userId || !clientData.playerData) continue; try { resourceSystem.initResources(clientData.playerData); const { produced, capped } = resourceSystem.tick(clientData.playerData); if (Object.keys(produced).length > 0) { io.to(socketId).emit('resource_update', { resources: clientData.playerData.resources, produced, capped, rates: resourceSystem.getProductionRates(clientData.playerData), caps: resourceSystem.getStorageCaps(clientData.playerData), }); } } catch (err) { console.error('[ECONOMY TICK] Error:', err.message); } } }, 60000); // GDD §18.2: 60-second economy tick }); } catch (error) { console.error('[GAME SERVER] Failed to start server:', error); process.exit(1); } } // Register this GameServer with the API server async function registerWithAPIServer() { try { // Force use of local API server for development const apiServerUrl = 'http://localhost:3001'; const gameServerUrl = 'https://dev.gameserver.galaxystrike.online'; const serverData = { serverId: `devgame-server-${PORT}`, name: 'Dev Game Server', type: 'public', // Must be 'public' or 'private' according to model region: 'Europe', maxPlayers: 100, // Hardcoded to allow 100 players currentPlayers: 0, gameServerUrl: gameServerUrl, owner: { userId: 'developer', username: 'Developer' } }; console.log(`[GAME SERVER] Registering with API server at ${apiServerUrl}`); console.log(`[GAME SERVER] Server data:`, serverData); const response = await fetch(`${apiServerUrl}/api/servers/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(serverData) }); if (response.ok) { const result = await response.json(); console.log(`[GAME SERVER] Successfully registered with API server:`, result); } else { const errorText = await response.text(); console.error(`[GAME SERVER] Failed to register with API server: ${response.status}`); console.error(`[GAME SERVER] Error response:`, errorText); } } catch (error) { console.error('[GAME SERVER] Error registering with API server:', error); } } startServer(); module.exports = { app, server, io, gameInstances, connectedClients };