/** * 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; // Preserve existing economy data if available (prevents wipe during re-initialization) const existingEconomy = window.game?.systems?.economy; const preservedCredits = existingEconomy?.credits || 0; const preservedGems = existingEconomy?.gems || 0; // Player resources this.credits = preservedCredits; this.gems = preservedGems; this.premiumCurrency = 0; // Transaction tracking this.transactionHistory = []; this.transactions = []; // Add missing transactions array // Owned cosmetics this.ownedCosmetics = []; // Add missing owned cosmetics array // Shop categories this.shopCategories = { ships: 'Ships', weapons: 'Weapons', modules: 'Modules', 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.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 = []; console.log('[ECONOMY] Economy system initialized with server-side ItemSystem'); console.log('[ECONOMY] Preserved values - Credits:', this.credits, 'Gems:', this.gems); // Set up socket listeners for economy sync this.setupSocketListeners(); // Request fresh economy data after a short delay to ensure sync if (window.smartSaveManager?.isMultiplayer) { setTimeout(() => { this.requestEconomyData(); }, 1000); } } /** * 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); this.credits = data.credits || 0; this.gems = data.gems || 0; // Update UI immediately if (this.game.ui) { this.game.ui.updatePlayerStats(); } console.log('[ECONOMY] Economy synced - Credits:', this.credits, 'Gems:', this.gems); }); } /** * 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); } 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); } } // UI updates updateUI() { // 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'); console.log('[ECONOMY] Multiplayer mode:', window.smartSaveManager?.isMultiplayer); console.log('[ECONOMY] ItemSystem available:', !!(this.game.systems.itemSystem)); console.log('[ECONOMY] ItemSystem catalog:', !!(this.game.systems.itemSystem?.itemCatalog)); 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, skipping shop update'); return; } // Safe to use ItemSystem now const items = Array.from(this.game.systems.itemSystem.shopItems || []); console.log('[ECONOMY] Rendering shop with', items.length, 'items from ItemSystem'); console.log('[ECONOMY] First few items:', items.slice(0, 3)); this.renderShopItems(items); } else { // Singleplayer mode - use local shop data console.log('[ECONOMY] Singleplayer mode - using local shop data'); const items = Object.values(this.randomShopItems).flat(); console.log('[ECONOMY] Rendering shop with', items.length, 'local items'); this.renderShopItems(items); } } 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); console.log('[ECONOMY] All items types:', items.map(item => ({id: item.id, type: item.type, name: item.name}))); console.log('[ECONOMY] Unique item types:', [...new Set(items.map(item => item.type))]); // Map category names to item types (handle plural/singular mismatches) const categoryTypeMap = { 'ships': 'ship', 'weapons': 'weapon', 'armors': 'armor', 'cosmetics': 'cosmetic', 'consumables': 'consumable', 'materials': 'material', 'keys': 'key' }; const targetItemType = categoryTypeMap[activeCategory] || activeCategory; console.log('[ECONOMY] Mapped category', activeCategory, 'to item type', targetItemType); const categoryItems = items.filter(item => item.type === targetItemType); console.log('[ECONOMY] Filtered items for category', activeCategory, '(type:', targetItemType, ') :', categoryItems.length, 'items'); 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); return `${item.description}