`;
},
payoutSummaryHtml: function(mode, currency) {
const outcomes = this.config?.payoutOutcomes?.[mode];
if (!outcomes?.escape || !outcomes?.escapeWithTreasure) {
return 'Exact payout terms are unavailable. Reload before entering paid play.';
}
const describe = (label, outcome) => {
if (!outcome.payable) {
const why = outcome.suppressedReason === 'below_minimum' ? ' (below configured minimum)' : '';
return `${label}: no payout${why}`;
}
const amount = `${this.escapeHtml(outcome.amountFormatted)} ${this.escapeHtml(currency)}`;
if (outcome.capApplied) return `${label}: ${amount} (configured cap applied)`;
return `${label}: ${this.escapeHtml(outcome.multiplier)}× = ${amount}`;
};
return `💰 ${describe('Escape', outcomes.escape)} • ${describe('Treasure', outcomes.escapeWithTreasure)}`;
},
render: function() {
if (!this.config) return;
// Display label (sXMR on stagenet): distinct from cryptoType, which formatPrice
// still uses for decimals.
const currency = this.config.currencyLabel || this.config.cryptoType || 'WOW';
const hasCredits = this.userCredits >= (this.config.creditsPerGame || 1);
const creditPackages = this.config.creditPackages || [];
const cosmeticProducts = this.config.cosmeticProducts || [];
const $container = $('.game-modes');
$container.empty();
// Reset selected package
this.selectedPackageId = null;
// Explain the two leaderboards up front when both paid and free are available.
if (this.config.paymentsEnabled && this.config.freePlayEnabled) {
const paidEntryKinds = [];
if (this.config.creditsModeEnabled) paidEntryKinds.push('credits');
if (this.config.directModeEnabled) paidEntryKinds.push('single entry');
const paidEntryCopy = paidEntryKinds.join(' or ') || 'a paid entry';
$container.append(
'
' +
'🏅 Pay (' + this.escapeHtml(paidEntryCopy) + ') to put your high scores in the Hall of Champions. ' +
'🪙 Play free: high scores go to the Pleb leaderboard instead.' +
'
'
);
}
// === SECTION 1: Use existing credits (if available) ===
if (hasCredits && this.config.creditsModeEnabled) {
const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled;
let payoutNote = '';
if (creditsPayoutsEnabled) {
payoutNote = `${this.payoutSummaryHtml('credits', currency)} 🏅 High scores enter the Hall of Champions`;
} else {
payoutNote = '🏅 High scores enter the Hall of Champions';
}
$container.append(`
🎮 You have ${this.userCredits} credits
${payoutNote}
`);
}
// === SECTION 2: Direct payment (pay per game) ===
if (this.config.directModeEnabled) {
const price = this.config.singleGamePriceFormatted || this.formatPrice(this.config.singleGamePrice);
// Payout-honest: only show multipliers when payouts are actually on (default off, not on).
const directPayoutsEnabled = this.config.directPayoutsEnabled != null
? !!this.config.directPayoutsEnabled
: !!this.config.payoutsEnabled;
let payoutInfo = '';
if (directPayoutsEnabled) {
payoutInfo = `${this.payoutSummaryHtml('direct', currency)} 🏅 High scores enter the Hall of Champions`;
} else {
payoutInfo = '🏅 High scores enter the Hall of Champions';
}
$container.append(`
⚡ Single-game entry${price} ${currency}
${payoutInfo}
Adds one purchase-progress point for unlock thresholds; no spendable credit is added.
`);
}
// === SECTION 3: Buy credits (bulk discount) ===
if (this.config.creditsModeEnabled && creditPackages.length > 0) {
const singlePrice = this.config.singleGamePrice;
const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled;
// Build payout info for credits mode
let creditsPayoutInfo = '';
if (creditsPayoutsEnabled) {
creditsPayoutInfo = `
${this.payoutSummaryHtml('credits', currency)}
`;
} else {
creditsPayoutInfo = `
🏅 High scores enter the Hall of Champions • bulk discount
`);
}
// === 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 play === shown when the instance is free-only OR offers free as a choice
// alongside paid (freePlayEnabled). Free games go to the Pleb leaderboard.
if (this.config.freePlayEnabled) {
const note = this.config.paymentsEnabled
? '🪙 Free: high scores go to the Pleb leaderboard'
: 'No payments required';
$container.append(`
`);
}
},
handleModeSelection: function(mode, action, packageId, acknowledgementReady, legalAcknowledgement) {
console.log("Mode selected:", mode, "Action:", action, "Package:", packageId);
if (mode === 'FREE') {
$('#payment-ui').hide();
if (window.socket && typeof SocketHandlers !== 'undefined' && SocketHandlers.emitFairGameStart) {
// play_free records the game as FREE (Pleb leaderboard, no payout) even on
// an instance that also sells credits/entry.
SocketHandlers.emitFairGameStart('play_free', { free: true });
}
return;
}
// A checkbox in the browser is not a legal/identity verification, but requiring a fresh
// per-session statement prevents the UI from silently opening a value-bearing action.
if (!acknowledgementReady) {
if (typeof CommerceConsent === 'undefined' || !CommerceConsent.require) {
$('#payment-status').text('Paid-play disclosures are unavailable. No payment was requested.');
return;
}
CommerceConsent.require(function(acknowledgement) {
PaymentUI.handleModeSelection(mode, action, packageId, true, acknowledgement);
});
return;
}
// If using existing credits, just start the game
if (action === 'use_credit' && this.userCredits >= 1) {
$('#payment-ui').hide();
if (window.socket && typeof SocketHandlers !== 'undefined' && SocketHandlers.emitFairGameStart) {
SocketHandlers.emitFairGameStart('auto_start', {
legalAcknowledgement: legalAcknowledgement
}, true);
}
return;
}
// Request payment with type and optional packageId
let paymentType = 'single_game';
if (action === 'buy_credits') {
paymentType = 'credits_package';
} else if (action === 'buy_product') {
paymentType = 'cosmetic_pack';
}
const requestData = { type: paymentType };
if (legalAcknowledgement) requestData.legalAcknowledgement = legalAcknowledgement;
if (packageId) {
if (paymentType === 'cosmetic_pack') {
requestData.productId = packageId;
} else {
requestData.packageId = packageId;
}
}
if (window.socket) {
if (paymentType === 'single_game' && typeof SocketHandlers !== 'undefined' && SocketHandlers.fairnessAttempt) {
var fairRequest = SocketHandlers.fairnessAttempt(requestData);
if (!fairRequest) return;
window.socket.emit('request_payment', fairRequest);
} else {
window.socket.emit('request_payment', requestData);
}
// Show loading state
$('.game-modes').hide();
$('#payment-details').show();
$('#payment-status').text('Requesting payment address...');
$('#payment-amount').text('...');
$('#payment-address').val('Loading...');
}
},
showPaymentRequest: function(data) {
$('#payment-ui').show();
$('#payment-details').show();
$('.game-modes').hide();
const isCreditsPurchase = data.paymentType === 'credits_package';
const isProductPurchase = data.paymentType === 'cosmetic_pack';
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`;
} else if (isProductPurchase && data.package) {
headerText = `Buy ${data.package.label || data.package.id || 'Pack'}`;
}
$('.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.paymentId || data.id);
if (data.reused) {
$('#payment-status').html('♻️ Using existing pending payment');
} else {
const statusText = isCreditsPurchase
? 'Send payment to receive credits...'
: (isProductPurchase ? 'Send payment to unlock this pack...' : 'Waiting for payment...');
$('#payment-status').text(statusText);
}
},
show: function() {
this.render(); // Ensure options are up-to-date with current config
$('#payment-ui').show();
$('.game-modes').show();
$('#payment-details').hide();
$('.payment-header strong').text('🎮 Choose How to Play');
}
};
// Expose globally
window.PaymentUI = PaymentUI;
// Initialize when DOM is ready
$(document).ready(function() {
PaymentUI.init();
});