diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 55b3130..267270a 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -92,6 +92,7 @@ const SocketHandlers = { socket.on('payment_created', this.onPaymentCreated); socket.on('payment_confirmed', this.onPaymentConfirmed); socket.on('payment_detected', this.onPaymentDetected); + socket.on('show_payment_options', this.onShowPaymentOptions); socket.on('credits_update', this.onCreditsUpdate); // Block height handler @@ -603,6 +604,14 @@ const SocketHandlers = { } }, + onShowPaymentOptions: function(data) { + console.log('Show payment options:', data); + // Show the payment options modal so user can choose how to play + if (typeof PaymentUI !== 'undefined') { + PaymentUI.show(); + } + }, + _lastDisplayedConfirmation: null, _confirmationTimestamps: {}, // paymentId -> ts onPaymentConfirmed: function(data) { diff --git a/html/js/ui/paymentUI.js b/html/js/ui/paymentUI.js index 533a4cc..72016c1 100644 --- a/html/js/ui/paymentUI.js +++ b/html/js/ui/paymentUI.js @@ -6,6 +6,7 @@ const PaymentUI = { config: null, userCredits: 0, hasPayoutAddress: false, + selectedPackageId: null, init: function() { console.log("Initializing PaymentUI..."); @@ -19,7 +20,22 @@ const PaymentUI = { $('#payment-ui').on('click', '.mode-option', function() { const mode = $(this).data('mode'); const action = $(this).data('action'); - PaymentUI.handleModeSelection(mode, action); + const packageId = $(this).data('package-id'); + PaymentUI.handleModeSelection(mode, action, packageId); + }); + + // Bind package radio buttons + $('#payment-ui').on('change', 'input[name="credit-package"]', function() { + PaymentUI.selectedPackageId = $(this).val(); + // Enable buy button + $('#buy-credits-btn').prop('disabled', false); + }); + + // Bind buy credits button + $('#payment-ui').on('click', '#buy-credits-btn', function() { + if (PaymentUI.selectedPackageId) { + PaymentUI.handleModeSelection('PAID_CREDITS', 'buy_credits', PaymentUI.selectedPackageId); + } }); // Bind check payment button @@ -36,14 +52,12 @@ const PaymentUI = { $('#copy-address').on('click', function() { const addr = $('#payment-address').val(); if(addr) { - // Try modern clipboard API first if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(addr).then(() => { $('#copy-address').text('Copied!'); setTimeout(() => $('#copy-address').html('📋 Copy Address'), 2000); }); } else { - // Fallback const $temp = $(""); $("body").append($temp); $temp.val(addr).select(); @@ -55,7 +69,7 @@ const PaymentUI = { } }); - // Bind Shop Button (will be added to index.html) + // Bind Shop Button $('#shopButton').on('click', function() { PaymentUI.show(); }); @@ -77,79 +91,180 @@ const PaymentUI = { updateCredits: function(balance) { this.userCredits = balance || 0; this.render(); - // Update credit display if visible $('#user-credits-display').text(this.userCredits); }, + formatPrice: function(atomicAmount) { + const currency = this.config?.cryptoType || 'WOW'; + const decimals = currency === 'XMR' ? 12 : 11; + const divisor = Math.pow(10, decimals); + return (atomicAmount / divisor).toFixed(decimals === 12 ? 4 : 2); + }, + render: function() { if (!this.config) return; const currency = this.config.cryptoType || 'WOW'; - const decimals = currency === 'XMR' ? 12 : 11; - const divisor = Math.pow(10, decimals); - const bothEnabled = this.config.directModeEnabled && this.config.creditsModeEnabled; const hasCredits = this.userCredits >= (this.config.creditsPerGame || 1); + const creditPackages = this.config.creditPackages || []; - // Build dynamic options container const $container = $('.game-modes'); $container.empty(); - // Add credits balance display if credits mode is enabled - if (this.config.creditsModeEnabled) { + // Reset selected package + this.selectedPackageId = null; + + // === SECTION 1: Use existing credits (if available) === + if (hasCredits && this.config.creditsModeEnabled) { + const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled; + const creditsMultipliers = this.config.payoutMultipliers?.credits || { escape: 2, escapeWithTreasure: 3 }; + const creditsBaseValue = this.config.creditsPayoutBaseValue || this.config.singleGamePrice; + + let payoutNote = ''; + if (creditsPayoutsEnabled) { + const baseFormatted = this.formatPrice(creditsBaseValue); + const escapeWin = (parseFloat(baseFormatted) * creditsMultipliers.escape).toFixed(2); + const treasureWin = (parseFloat(baseFormatted) * creditsMultipliers.escapeWithTreasure).toFixed(2); + payoutNote = `💰 Win: ${creditsMultipliers.escape}x (${escapeWin} ${currency}) • Treasure: ${creditsMultipliers.escapeWithTreasure}x (${treasureWin} ${currency})`; + } else { + payoutNote = 'No crypto payouts • Play for fun'; + } + $container.append(` -
- Your Credits: - ${this.userCredits} +
+
+ 🎮 You have ${this.userCredits} credits +
+
+ ${payoutNote} +
+
`); } - // If user has credits and credits mode is enabled, show "Use Credit" option first - if (hasCredits && this.config.creditsModeEnabled) { - const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled; - const payoutNote = creditsPayoutsEnabled - ? '💰 Payouts enabled' - : '⚡ Fast play • No payouts'; - $container.append(` - - `); - } - - // Show direct payment option + // === SECTION 2: Direct payment (pay per game) === if (this.config.directModeEnabled) { - const price = (this.config.singleGamePrice / divisor).toFixed(decimals === 12 ? 4 : 2); - const recommended = !hasCredits ? 'recommended' : ''; - const directPayoutsEnabled = this.config.directPayoutsEnabled !== false; // Default true - const payoutInfo = directPayoutsEnabled - ? '💰 2x payout on escape, 3x with treasure' - : '⚡ Play mode • No payouts'; + const price = this.config.singleGamePriceFormatted || this.formatPrice(this.config.singleGamePrice); + const multipliers = this.config.payoutMultipliers?.direct || { escape: 2, escapeWithTreasure: 3 }; + const directPayoutsEnabled = this.config.directPayoutsEnabled !== false; + + let payoutInfo = ''; + if (directPayoutsEnabled) { + const escapeWin = (parseFloat(price) * multipliers.escape).toFixed(2); + const treasureWin = (parseFloat(price) * multipliers.escapeWithTreasure).toFixed(2); + payoutInfo = `Win: ${multipliers.escape}x (${escapeWin} ${currency}) • Treasure: ${multipliers.escapeWithTreasure}x (${treasureWin} ${currency})`; + } else { + payoutInfo = 'No crypto payouts'; + } + $container.append(` - +
+
+ ⚡ Single Game + ${price} ${currency} +
+
+ ${payoutInfo} +
+ +
`); } - // Show buy credits option if credits mode is enabled - if (this.config.creditsModeEnabled) { - const price = (this.config.creditsPackagePrice / divisor).toFixed(decimals === 12 ? 4 : 2); - const pkgCredits = this.config.creditsPackageCount || 10; + // === SECTION 3: Buy credits (bulk discount) === + if (this.config.creditsModeEnabled && creditPackages.length > 0) { + const singlePrice = this.config.singleGamePrice; + const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled; + const creditsMultipliers = this.config.payoutMultipliers?.credits || { escape: 2, escapeWithTreasure: 3 }; + const creditsBaseValue = this.config.creditsPayoutBaseValue || this.config.singleGamePrice; + + // Build payout info for credits mode + let creditsPayoutInfo = ''; + if (creditsPayoutsEnabled) { + const baseFormatted = this.formatPrice(creditsBaseValue); + const escapeWin = (parseFloat(baseFormatted) * creditsMultipliers.escape).toFixed(2); + const treasureWin = (parseFloat(baseFormatted) * creditsMultipliers.escapeWithTreasure).toFixed(2); + creditsPayoutInfo = `
💰 Win: ${creditsMultipliers.escape}x (${escapeWin} ${currency}) • Treasure: ${creditsMultipliers.escapeWithTreasure}x (${treasureWin} ${currency})
`; + } else { + creditsPayoutInfo = `
No crypto payouts • Play for fun with bulk discount
`; + } + + let packagesHtml = ''; + creditPackages.forEach((pkg, index) => { + const totalCredits = pkg.credits + (pkg.bonus || 0); + const pricePerCredit = pkg.price / totalCredits; + const priceFormatted = pkg.priceFormatted || this.formatPrice(pkg.price); + const perGamePrice = this.formatPrice(pricePerCredit); + + // Calculate discount vs direct play + let discountBadge = ''; + if (singlePrice > 0) { + const directCostForSameGames = singlePrice * totalCredits; + const savings = ((directCostForSameGames - pkg.price) / directCostForSameGames * 100).toFixed(0); + if (savings > 0) { + discountBadge = `Save ${savings}%`; + } + } + + const bonusText = pkg.bonus > 0 + ? ` +${pkg.bonus} bonus` + : ''; + + const checked = index === 0 ? 'checked' : ''; + if (index === 0) this.selectedPackageId = pkg.id; + + packagesHtml += ` + + `; + }); + $container.append(` - +
+
+ 🎫 Buy Credits + Bulk discount • Never expire +
+ ${creditsPayoutInfo} +
+ ${packagesHtml} +
+ +
`); } - // Show FREE option only if payments are disabled + // === Credits balance display (if has some but not enough) === + if (this.config.creditsModeEnabled && this.userCredits > 0 && !hasCredits) { + $container.prepend(` +
+ ⚠️ You have ${this.userCredits} credits (need ${this.config.creditsPerGame || 1} to play) +
+ `); + } + + // === FREE mode (only if payments disabled) === if (!this.config.paymentsEnabled) { $container.append(` - @@ -157,12 +272,11 @@ const PaymentUI = { } }, - handleModeSelection: function(mode, action) { - console.log("Mode selected:", mode, "Action:", action); + handleModeSelection: function(mode, action, packageId) { + console.log("Mode selected:", mode, "Action:", action, "Package:", packageId); if (mode === 'FREE') { $('#payment-ui').hide(); - // Emit to start game directly if (window.socket) { window.socket.emit('enter_game'); } @@ -173,19 +287,24 @@ const PaymentUI = { if (action === 'use_credit' && this.userCredits >= 1) { $('#payment-ui').hide(); if (window.socket) { - window.socket.emit('enter_game'); // Will use credits automatically + window.socket.emit('enter_game'); } return; } - // Otherwise, request payment + // Request payment with type and optional packageId let paymentType = 'single_game'; if (action === 'buy_credits') { paymentType = 'credits_package'; } + const requestData = { type: paymentType }; + if (packageId) { + requestData.packageId = packageId; + } + if (window.socket) { - window.socket.emit('request_payment', { type: paymentType }); + window.socket.emit('request_payment', requestData); // Show loading state $('.game-modes').hide(); @@ -199,16 +318,20 @@ const PaymentUI = { showPaymentRequest: function(data) { $('#payment-ui').show(); $('#payment-details').show(); - $('.game-modes').hide(); // Hide selection, show payment + $('.game-modes').hide(); const isCreditsPurchase = data.paymentType === 'credits_package'; - const headerText = isCreditsPurchase ? '🎫 Buy Credits' : '💳 Payment Required'; + let headerText = '💳 Payment Required'; + if (isCreditsPurchase && data.package) { + const bonus = data.package.bonus > 0 ? ` +${data.package.bonus} bonus` : ''; + headerText = `🎫 Buy ${data.package.credits}${bonus} Credits`; + } $('.payment-header strong').text(headerText); const displayAmount = data.humanAmount || data.amountFormatted || data.amount; $('#payment-amount').text(displayAmount + ' ' + (data.currency || '')); $('#payment-address').val(data.address); - $('#check-payment').data('payment-id', data.id); + $('#check-payment').data('payment-id', data.paymentId || data.id); if (data.reused) { $('#payment-status').html('♻️ Using existing pending payment'); @@ -224,7 +347,7 @@ const PaymentUI = { $('#payment-ui').show(); $('.game-modes').show(); $('#payment-details').hide(); - $('.payment-header strong').text('🎮 Game Mode Selection'); + $('.payment-header strong').text('🎮 Choose How to Play'); } }; diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js index 7a9575f..002ce87 100644 --- a/src/game/gameModeManager.js +++ b/src/game/gameModeManager.js @@ -824,6 +824,7 @@ class GameModeManager { try { const user = await this.getOrCreateUser(socketId); const reuseExisting = options.reuseExisting !== false; + const requestedPackageId = options.packageId; let amount; let description; @@ -836,12 +837,26 @@ class GameModeManager { break; } case 'credits_package': { - const primaryPackage = this.getPrimaryCreditPackage(); - const packagePrice = primaryPackage?.price ?? this.creditsPackagePrice; - amount = Number(packagePrice); - packageInfo = primaryPackage; - const creditCount = primaryPackage?.credits ?? 10; - description = `Wowngeon ${creditCount} credits package (${this.cryptoType})`; + // Find the requested package, or fall back to primary + const packages = this.configSnapshot?.modes?.credits?.packages || []; + let selectedPackage = null; + + if (requestedPackageId && packages.length > 0) { + selectedPackage = packages.find(p => p.id === requestedPackageId); + } + + // Fall back to first package if not found + if (!selectedPackage) { + selectedPackage = this.getPrimaryCreditPackage(); + } + + const packagePrice = selectedPackage?.price ?? this.creditsPackagePrice; + amount = typeof packagePrice === 'bigint' ? Number(packagePrice) : Number(packagePrice); + packageInfo = selectedPackage; + const creditCount = selectedPackage?.credits ?? 10; + const bonusCredits = selectedPackage?.bonus ?? 0; + const bonusText = bonusCredits > 0 ? ` (+${bonusCredits} bonus)` : ''; + description = `Wowngeon ${creditCount}${bonusText} credits package (${this.cryptoType})`; break; } default: @@ -1003,6 +1018,19 @@ class GameModeManager { ? `⚠️ STAGENET MODE: This server is using ${this.network} XMR. Do NOT send real mainnet XMR! Only ${this.network} XMR will be accepted.` : null; + // Serialize all credit packages (convert BigInt prices to Number) + const packages = this.configSnapshot?.modes?.credits?.packages || []; + const creditPackages = packages.map(pkg => ({ + id: pkg.id, + credits: pkg.credits, + price: typeof pkg.price === 'bigint' ? Number(pkg.price) : pkg.price, + bonus: pkg.bonus || 0, + priceFormatted: this.formatAtomicHuman( + typeof pkg.price === 'bigint' ? Number(pkg.price) : pkg.price, + 2 + ) + })); + return { mode: this.gameMode, cryptoType: this.cryptoType, @@ -1010,13 +1038,20 @@ class GameModeManager { isTestNetwork: this.isTestNetwork, testnetWarning: testnetWarning, singleGamePrice: this.singleGamePrice, + singleGamePriceFormatted: this.formatAtomicHuman(this.singleGamePrice, 2), creditsPackagePrice: this.creditsPackagePrice, creditsPerGame: this.creditsPerGameCost, + creditPackages: creditPackages, + creditsPayoutBaseValue: this.creditsPayoutBaseValue, paymentsEnabled: this.paymentsEnabled, directModeEnabled: this.directModeEnabled, creditsModeEnabled: this.creditsModeEnabled, directPayoutsEnabled: this.directPayoutMultipliers.escape > 0, creditsPayoutsEnabled: this.creditsPayoutEnabled, + payoutMultipliers: { + direct: this.directPayoutMultipliers, + credits: this.creditPayoutMultipliers + }, features: { paymentRequired: this.paymentsEnabled, creditsSystem: this.creditsModeEnabled, diff --git a/src/network/paymentHandlers.js b/src/network/paymentHandlers.js index 994ba8c..1916f19 100644 --- a/src/network/paymentHandlers.js +++ b/src/network/paymentHandlers.js @@ -47,8 +47,10 @@ class PaymentHandlers { return; } try { - const { gameMode } = data; - const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, gameMode); + const { type, packageId } = data; + const paymentType = type || data.gameMode || 'single_game'; + const options = { packageId }; + const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType, options); this.io.to(socket.id).emit('payment_created', { paymentId: paymentRequest.id, address: paymentRequest.address, @@ -56,7 +58,7 @@ class PaymentHandlers { amountFormatted: paymentRequest.amountFormatted, currency: this.gameModeManager.cryptoType, package: paymentRequest.package, - gameMode: gameMode + paymentType: paymentType }); if (this.debugManager.CONSOLE_LOGGING) console.log(`💳 Payment request created for ${socket.id}: ${paymentRequest.amount}`); } catch (e) { @@ -66,8 +68,22 @@ class PaymentHandlers { } } - async createAndShowPaymentRequest(socket) { + async createAndShowPaymentRequest(socket, options = {}) { if (!this.gameModeManager) return; + + // If both direct and credits modes are enabled, let the user choose + const bothModesEnabled = this.gameModeManager.directModeEnabled && this.gameModeManager.creditsModeEnabled; + const forceShowOptions = options.showOptions === true; + + if (bothModesEnabled && !options.paymentType) { + // Send event to show payment options modal on client + this.io.to(socket.id).emit('show_payment_options', { + reason: 'choose_payment_method', + message: 'Choose how you want to play' + }); + return; + } + try { // Ensure payout address exists for modes that may payout (PAID_SINGLE or PAID_CREDITS with payouts) const mode = this.gameModeManager.gameMode; @@ -89,16 +105,23 @@ class PaymentHandlers { // caller (SocketHandlers) will handle user existence; keep method generic const gameMode = this.gameModeManager.gameMode; const cryptoType = this.gameModeManager.cryptoType; - let paymentType; + let paymentType = options.paymentType; let amount; let description; - if (gameMode === 'PAID_SINGLE') { - paymentType = 'single_game'; + // Determine payment type if not specified + if (!paymentType) { + if (gameMode === 'PAID_SINGLE' || this.gameModeManager.directModeEnabled) { + paymentType = 'single_game'; + } else if (gameMode === 'PAID_CREDITS') { + paymentType = 'credits_package'; + } + } + + if (paymentType === 'single_game') { amount = this.gameModeManager.singleGamePrice; description = 'Single game entry'; - } else if (gameMode === 'PAID_CREDITS') { - paymentType = 'credits_package'; + } else if (paymentType === 'credits_package') { const primaryPackage = this.gameModeManager.getPrimaryCreditPackage(); amount = Number(primaryPackage?.price ?? this.gameModeManager.creditsPackagePrice); const credits = primaryPackage?.credits ?? this.gameModeManager.creditsPerGameCost * 10; @@ -108,7 +131,10 @@ class PaymentHandlers { return; } - const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType, { reuseExisting: true }); + const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType, { + reuseExisting: true, + packageId: options.packageId + }); const reused = !!paymentRequest.reused; const formattedAmount = paymentRequest.amountFormatted ?? this.gameModeManager.formatAtomicHuman(amount, 3); let qrDataUrl = null;