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

No items available

'; return; } console.log('[ECONOMY] Filtered items for category', activeCategory, ':', categoryItems.length, 'items'); console.log('[ECONOMY] Item types in category:', categoryItems.map(item => item.type)); if (categoryItems.length === 0) { shopItemsElement.innerHTML = '

No items available in this category

'; return; } shopItemsElement.innerHTML = categoryItems.map(item => { const canAfford = this.canAfford(item.price, item.currency); const isOwned = item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id); // Generate image URL - server will serve images const imageUrl = this.getItemImageUrl(item); const placeholderUrl = 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png'; return `
${item.name}

${item.name}

${item.rarity}

${item.description}

${this.formatPrice(item)}
`; }).join(''); // Add event listeners to purchase buttons shopItemsElement.querySelectorAll('.shop-item-purchase-btn').forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const itemId = button.getAttribute('data-item-id'); if (itemId && !button.disabled) { console.log('[ECONOMY] Purchase button clicked for item:', itemId); this.purchaseItem(itemId, 1); } }); }); } formatPrice(item) { if (!item.price) return 'Free'; const currency = item.currency || 'credits'; const price = this.game.formatNumber(item.price); return `${price} ${currency}`; } /** * Get image URL for an item from server */ getItemImageUrl(item) { if (!item) return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png'; // For multiplayer, ALWAYS get from server if (window.smartSaveManager?.isMultiplayer && this.game.socket) { const serverUrl = this.getServerUrl(); // Map item types to proper server paths switch (item.type) { case 'ship': return `${serverUrl}/images/ships/${item.id}.png`; case 'weapon': return `${serverUrl}/images/weapons/${item.id}.png`; case 'armor': return `${serverUrl}/images/armors/${item.id}.png`; case 'material': return `${serverUrl}/images/items/materials/${item.id}.png`; case 'consumable': return `${serverUrl}/images/items/consumables/${item.id}.png`; case 'cosmetic': return `${serverUrl}/images/items/cosmetics/${item.id}.png`; default: return `${serverUrl}/images/ui/placeholder.png`; } } // For singleplayer, use local texture path (if available) if (item.texture) { return item.texture; } // Fallback to server placeholder return 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png'; } /** * Get server URL for image requests */ getServerUrl() { // Get server URL from socket connection if (this.game.socket && this.game.socket.io && this.game.socket.io.uri) { return this.game.socket.io.uri.replace('/socket.io', ''); } // Fallback to environment variable or production server return process.env.SERVER_URL || 'https://dev.gameserver.galaxystrike.online'; } // Save/Load functionality save() { return { credits: this.credits, gems: this.gems, premiumCurrency: this.premiumCurrency, transactions: this.transactions, ownedCosmetics: this.ownedCosmetics, shopData: { randomShopItems: this.randomShopItems, categoryPurchaseLimits: this.categoryPurchaseLimits, lastShopRefresh: this.lastShopRefresh } }; } load(data) { if (data.credits !== undefined) this.credits = data.credits; if (data.gems !== undefined) this.gems = data.gems; if (data.premiumCurrency !== undefined) this.premiumCurrency = data.premiumCurrency; if (data.transactions) this.transactions = data.transactions; if (data.ownedCosmetics) this.ownedCosmetics = data.ownedCosmetics; // Load shop data if (data.shopData) { this.randomShopItems = data.shopData.randomShopItems || {}; this.categoryPurchaseLimits = data.shopData.categoryPurchaseLimits || {}; this.lastShopRefresh = data.shopData.lastShopRefresh || null; } this.updateUI(); } // Reset functionality reset() { const oldState = { credits: this.credits, gems: this.gems, premiumCurrency: this.premiumCurrency, transactionCount: this.transactions.length, ownedCosmeticsCount: this.ownedCosmetics.length }; this.credits = 1000; this.gems = 10; this.premiumCurrency = 0; this.transactions = []; this.ownedCosmetics = []; // Reset daily rewards localStorage.removeItem('lastDailyReward'); this.updateUI(); return oldState; } clear() { const oldState = { credits: this.credits, gems: this.gems, premiumCurrency: this.premiumCurrency, transactionCount: this.transactions.length, ownedCosmeticsCount: this.ownedCosmetics.length }; this.credits = 0; this.gems = 0; this.premiumCurrency = 0; this.transactions = []; this.ownedCosmetics = []; this.updateUI(); return oldState; } // Initialize random shop for singleplayer (minimal implementation) initializeRandomShop() { console.log('[ECONOMY] Random shop not available in singleplayer mode'); this.randomShopItems = {}; } // Get system statistics getStats() { return { credits: this.credits, gems: this.gems, premiumCurrency: this.premiumCurrency, transactionCount: this.transactions.length, ownedCosmeticsCount: this.ownedCosmetics.length, shopItemsCount: this.game.systems.itemSystem && this.game.systems.itemSystem.itemCatalog ? this.game.systems.itemSystem.getStats().totalItems : 0 }; } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = Economy; } else { window.Economy = Economy; }