API/Client/js/core/Economy.js
2026-01-26 18:46:53 -04:00

749 lines
28 KiB
JavaScript

/**
* 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 = '<p>No items available in this category</p>';
return;
}
shopItemsElement.innerHTML = categoryItems.map(item => {
const canAfford = this.canAfford(item.price, item.currency);
const isOwned = item.type === 'cosmetic' && this.ownedCosmetics.includes(item.id);
// Generate image URL - server will serve images
const imageUrl = this.getItemImageUrl(item);
const placeholderUrl = 'https://dev.gameserver.galaxystrike.online/images/ui/placeholder.png';
return `
<div class="shop-item ${canAfford ? '' : 'cant-afford'} ${isOwned ? 'owned' : ''}" data-item-id="${item.id}">
<div class="shop-item-content">
<div class="shop-item-image">
<img src="${imageUrl}" alt="${item.name}"
onerror="this.src='${placeholderUrl}'"
loading="lazy">
</div>
<div class="shop-item-info">
<div class="shop-item-header">
<h3 class="shop-item-name">${item.name}</h3>
<span class="shop-item-rarity ${item.rarity}">${item.rarity}</span>
</div>
<div class="shop-item-body">
<p class="shop-item-description">${item.description}</p>
<div class="shop-item-price">
${this.formatPrice(item)}
</div>
</div>
<div class="shop-item-footer">
<button class="shop-item-purchase-btn"
data-item-id="${item.id}"
${!canAfford || isOwned ? 'disabled' : ''}>
${isOwned ? 'Owned' : 'Purchase'}
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
}
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;
}