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}
+
+
+ ▶ PLAY NOW (use 1 credit)
+
`);
}
- // 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(`
-
- 🎮 Use 1 Credit (${this.userCredits} available)
- Start game immediately • ${payoutNote}
-
- `);
- }
-
- // 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(`
-
- 💰 Pay Per Game - ${price} ${currency}
- ${payoutInfo}
-
+
+
+ ⚡ Single Game
+ ${price} ${currency}
+
+
+ ${payoutInfo}
+
+
+ 💰 Pay ${price} ${currency} to Play
+
+
`);
}
- // 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 += `
+
+
+ ${pkg.credits} credits${bonusText} ${discountBadge}
+ ${priceFormatted} ${currency}
+
+ ${perGamePrice} ${currency}/game for ${totalCredits} games
+
+
+ `;
+ });
+
$container.append(`
-
- 🎫 Buy ${pkgCredits} Credits - ${price} ${currency}
- Bulk discount • Credits never expire
-
+
+
+ 🎫 Buy Credits
+ Bulk discount • Never expire
+
+ ${creditsPayoutInfo}
+
+ ${packagesHtml}
+
+
+ 🛒 Buy Selected Package
+
+
`);
}
- // 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(`
-
+
🆓 Free Play
No payments required
@@ -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;