mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
add early entry and help screens for new users
This commit is contained in:
@@ -18,6 +18,13 @@
|
||||
- [x] **Active Games List**: Real-time game list with pagination, sorting, and auto-refresh ✅
|
||||
- [x] **Spectate UI**: "Watch Games" button, live game panel, and spectator controls ✅
|
||||
- [x] **Spectator Broadcasts**: Game updates broadcast to spectators via Socket.IO rooms ✅
|
||||
- [x] **Help Modal**: Added instructions modal with game rules, controls, and payment mode info ✅
|
||||
- [x] **Dynamic Treasure Icon**: Treasure tile now shows $W (Wownero) or $M (Monero) based on crypto type ✅
|
||||
- [x] **Address Status Indicator**: Payout address button shows warning when no address is set ✅
|
||||
|
||||
## User Feedback (Beta Tester - Dec 2025)
|
||||
- [ ] **History Panel Bug**: User reported payout not showing in History after winning with treasure (needs investigation - may be timing/status issue with "confirmed" vs "pending" payouts)
|
||||
- [x] **Early Dungeon Entry**: Allow credits/free mode users to enter dungeon immediately without waiting for next block (risky - they die if block found before escape). Toggleable via config. ✅
|
||||
|
||||
## Improvements / Refactoring
|
||||
- [x] **Error Handling**: Extended `normalizeError` to queueHandler, queueManager, sessionManager ✅
|
||||
@@ -35,7 +42,7 @@
|
||||
- [x] **Deployment Guide**: README includes production deployment with Nginx/Caddy configs and systemd ✅
|
||||
|
||||
## Pre-Beta Checklist
|
||||
- [x] All 38 tests pass (8 test suites) ✅
|
||||
- [x] All 47 tests pass (9 test suites) ✅
|
||||
- [x] Health check endpoint available at `/health` ✅
|
||||
- [x] REST endpoints implemented: `/api/user/:socketId/credits`, `/api/user/:socketId/mode`, `/api/user/:socketId/address` ✅
|
||||
- [x] Payout eligibility shown per payment mode in UI ✅
|
||||
|
||||
+53
-1
@@ -71,6 +71,7 @@
|
||||
<script src="js/ui/audioAlerts.js"></script>
|
||||
<script src="js/ui/addressModal.js"></script>
|
||||
<script src="js/ui/transactionHistory.js"></script>
|
||||
<script src="js/ui/helpModal.js"></script>
|
||||
<script src="js/network/socketHandlers.js"></script>
|
||||
<script src="js/input/inputHandler.js"></script>
|
||||
</head>
|
||||
@@ -103,7 +104,8 @@
|
||||
<label style="cursor:pointer; display:flex; align-items:center; gap:4px;">
|
||||
<input type="checkbox" id="audioAlertsToggle" style="cursor:pointer;"> Audio Alerts
|
||||
</label>
|
||||
<button id="historyButton" style="margin-left:auto; background:#1a1a4d; color:#c4b5fd; border:1px solid #8b5cf6; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="View your payment and payout history">📜 History</button>
|
||||
<button id="helpButton" style="margin-left:auto; background:#2d2d00; color:#ffd700; border:1px solid #ffd700; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="How to play and payment information">❓ Help</button>
|
||||
<button id="historyButton" style="background:#1a1a4d; color:#c4b5fd; border:1px solid #8b5cf6; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="View your payment and payout history">📜 History</button>
|
||||
<button id="manageAddressButton" style="background:#053655; color:#0ff; border:1px solid #0ff; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="Your payout address and credits are tied to a browser session token. Clearing cookies/localStorage or using incognito mode will lose your session.">⚙️ Manage Payout Address</button>
|
||||
<span id="sessionWarning" style="display:none; color:#f59e0b; font-size:10px; margin-left:6px;" title="Private browsing or localStorage blocked - your session won't persist">⚠️ Session not saved</span>
|
||||
</div>
|
||||
@@ -233,6 +235,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help/Instructions Modal -->
|
||||
<div id="helpModal" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="helpModalTitle">
|
||||
<div class="modal-content" style="max-width: 600px; max-height: 80vh; overflow-y: auto;">
|
||||
<button type="button" class="modal-close" id="helpModalClose" aria-label="Close">✕</button>
|
||||
<h2 id="helpModalTitle" style="color:#ffd700; margin-bottom:15px;">❓ How to Play</h2>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🎮 Game Objective</h3>
|
||||
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Navigate through the dungeon, collect the treasure bag, and escape through the exit before the monster catches you or time runs out!</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">⌨️ Controls</h3>
|
||||
<ul style="color:#ccc; font-size:0.9em; line-height:1.6; padding-left:20px;">
|
||||
<li><strong>WASD</strong> or <strong>Arrow Keys</strong> - Move your character</li>
|
||||
<li><strong>Enter</strong> - Join the queue to play</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🕐 Block Timing</h3>
|
||||
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Games are synchronized to blockchain blocks. <strong>You may need to wait a few minutes</strong> for the next block before your game starts. The block height is shown at the top of the screen.</p>
|
||||
</div>
|
||||
|
||||
<div id="helpPaymentModes" style="margin-bottom:20px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">💰 Payment Modes</h3>
|
||||
<div id="helpModesList" style="color:#ccc; font-size:0.9em; line-height:1.4;">
|
||||
<!-- Dynamically populated based on active modes -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🎁 Payouts</h3>
|
||||
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Set your payout address using the "Manage Payout Address" button. When you escape the dungeon, your winnings will be sent automatically. Escaping with the treasure bag earns a higher multiplier!</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:15px;">
|
||||
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">📜 Legend</h3>
|
||||
<div style="display:grid; grid-template-columns: 30px 1fr; gap:8px; color:#ccc; font-size:0.9em;">
|
||||
<span style="color:#60a5fa;">@</span><span>You (the player)</span>
|
||||
<span style="color:#4ade80;">></span><span>Exit - escape here to win</span>
|
||||
<span style="color:#ef4444;">~</span><span>Monster - avoid at all costs</span>
|
||||
<span style="color:#fbbf24;">$</span><span>Treasure - collect for bonus payout</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="helpModalCloseBtn" style="width:100%; padding:10px; background:#4c1d95; color:#fff; border:none; cursor:pointer; border-radius:4px; font-weight:bold;">Got it!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser info panel -->
|
||||
<div id="browser-info" style="position: fixed; right: 10px; top: 40px; background: rgba(0,0,0,0.7); color: #0f0; padding: 5px; font-family: monospace; font-size: 12px; z-index: 1000; display: none;">
|
||||
<div>Browser: <span id="browser-name">checking...</span></div>
|
||||
|
||||
@@ -155,6 +155,12 @@ window.GameTiles = {
|
||||
},
|
||||
|
||||
getTreasureTile: function() {
|
||||
// Dynamically return treasure tile based on crypto type
|
||||
// Check ScreenManager for the current crypto type
|
||||
if (typeof ScreenManager !== 'undefined' && ScreenManager._cryptoType) {
|
||||
return ScreenManager._cryptoType === 'XMR' ? '$M' : '$W';
|
||||
}
|
||||
// Fallback to static config
|
||||
return options.gameConfig.treasureType;
|
||||
},
|
||||
|
||||
|
||||
@@ -100,11 +100,15 @@ var ScreenManager = {
|
||||
|
||||
// Draw legend with special tile handling
|
||||
const display = DisplayManager.getDisplay();
|
||||
// Use dynamic treasure tile based on crypto type
|
||||
const treasureTile = (typeof GameTiles !== 'undefined' && GameTiles.getTreasureTile)
|
||||
? GameTiles.getTreasureTile()
|
||||
: (this._cryptoType === 'XMR' ? '$M' : '$W');
|
||||
const legendItems = [
|
||||
{ tile: "@2", text: "This is you" },
|
||||
{ tile: ">", text: " Escape the dungeon" },
|
||||
{ tile: "~", text: " Avoid the monster" },
|
||||
{ tile: "$M", text: "Secure the bag" }
|
||||
{ tile: treasureTile, text: "Secure the bag" }
|
||||
];
|
||||
|
||||
const centerX = Math.floor(this._screenWidth / 2) - 10;
|
||||
|
||||
@@ -10,6 +10,29 @@ const SocketHandlers = {
|
||||
el.text(text);
|
||||
if (color) el.css('color', color);
|
||||
},
|
||||
|
||||
_updateAddressButtonStatus: function(hasAddress) {
|
||||
const $btn = $('#manageAddressButton');
|
||||
if (!$btn.length) return;
|
||||
|
||||
if (hasAddress) {
|
||||
// Address is set - show green indicator
|
||||
$btn.css({
|
||||
'background': '#053655',
|
||||
'color': '#0ff',
|
||||
'border-color': '#0ff'
|
||||
});
|
||||
$btn.html('✅ Payout Address Set');
|
||||
} else {
|
||||
// No address - show warning indicator (yellow/orange)
|
||||
$btn.css({
|
||||
'background': '#553300',
|
||||
'color': '#ffa500',
|
||||
'border-color': '#ffa500'
|
||||
});
|
||||
$btn.html('⚠️ Set Payout Address');
|
||||
}
|
||||
},
|
||||
|
||||
init: function() {
|
||||
if (this._initialized) {
|
||||
@@ -99,6 +122,10 @@ const SocketHandlers = {
|
||||
// Block height handler
|
||||
socket.on('blockheight', this.onBlockHeight);
|
||||
|
||||
// Early entry handlers
|
||||
socket.on('early_entry_success', this.onEarlyEntrySuccess);
|
||||
socket.on('early_entry_error', this.onEarlyEntryError);
|
||||
|
||||
// Spectator handlers
|
||||
socket.on('active_games', this.onActiveGames);
|
||||
socket.on('spectate_start', this.onSpectateStart);
|
||||
@@ -131,6 +158,8 @@ const SocketHandlers = {
|
||||
try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {}
|
||||
$('#messages').append($('<li class="status">').text('New session established. Token stored.'));
|
||||
UI.scrollChat();
|
||||
// New session = no address set yet
|
||||
SocketHandlers._updateAddressButtonStatus(false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -149,6 +178,9 @@ const SocketHandlers = {
|
||||
if (typeof AddressModal !== 'undefined') {
|
||||
AddressModal.setCurrentAddress(data.payoutAddress);
|
||||
}
|
||||
SocketHandlers._updateAddressButtonStatus(true);
|
||||
} else {
|
||||
SocketHandlers._updateAddressButtonStatus(false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -273,6 +305,11 @@ const SocketHandlers = {
|
||||
|
||||
onQueueJoined: function(data) {
|
||||
console.log('Queue joined:', data);
|
||||
|
||||
// Mark as queued and update early entry button
|
||||
SocketHandlers._isQueued = true;
|
||||
SocketHandlers._updateEarlyEntryButton();
|
||||
|
||||
const currentBlockFallback = (typeof data.currentBlock === 'number' && data.currentBlock > 0)
|
||||
? data.currentBlock
|
||||
: (typeof UI !== 'undefined' && typeof UI._currentBlockHeight === 'number' && UI._currentBlockHeight > 0
|
||||
@@ -295,6 +332,10 @@ const SocketHandlers = {
|
||||
},
|
||||
|
||||
onGameStart: function(data) {
|
||||
// Clear queued state
|
||||
SocketHandlers._isQueued = false;
|
||||
SocketHandlers._updateEarlyEntryButton();
|
||||
|
||||
$('#messages').append($('<li class="game-start">').text("Starting game..."));
|
||||
|
||||
// Display provably fair commitment if present
|
||||
@@ -403,6 +444,10 @@ const SocketHandlers = {
|
||||
},
|
||||
|
||||
onQueueCancelled: function(data) {
|
||||
// Clear queued state
|
||||
SocketHandlers._isQueued = false;
|
||||
SocketHandlers._updateEarlyEntryButton();
|
||||
|
||||
$('#messages').append($('<li style="color: #ff0;">').text("Queue entry cancelled."));
|
||||
|
||||
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) {
|
||||
@@ -438,6 +483,10 @@ const SocketHandlers = {
|
||||
if (typeof AddressModal !== 'undefined') {
|
||||
AddressModal.onConfirmed(data);
|
||||
}
|
||||
// Update button to show address is set (unless cancelled)
|
||||
if (!data.cancelled && data.address) {
|
||||
SocketHandlers._updateAddressButtonStatus(true);
|
||||
}
|
||||
},
|
||||
|
||||
onAddressPrompt: function(data) {
|
||||
@@ -463,6 +512,10 @@ const SocketHandlers = {
|
||||
SocketHandlers._creditsPerGame = data.creditsPerGame;
|
||||
}
|
||||
|
||||
// Store game mode and early entry config
|
||||
SocketHandlers._gameMode = data.mode;
|
||||
SocketHandlers._earlyEntryConfig = data.earlyEntry || { enabled: false };
|
||||
|
||||
if (typeof UI !== 'undefined' && UI.updateGameTitle) {
|
||||
UI.updateGameTitle(data.cryptoType);
|
||||
}
|
||||
@@ -488,6 +541,14 @@ const SocketHandlers = {
|
||||
$('#shopButton').hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Update HelpModal with config
|
||||
if (typeof HelpModal !== 'undefined') {
|
||||
HelpModal.updateConfig(data);
|
||||
}
|
||||
|
||||
// Update early entry button visibility
|
||||
SocketHandlers._updateEarlyEntryButton();
|
||||
},
|
||||
|
||||
_showNetworkWarning: function(message, network) {
|
||||
@@ -1033,6 +1094,133 @@ const SocketHandlers = {
|
||||
|
||||
_hideSpectatorControls: function() {
|
||||
$('#spectatorControls').hide();
|
||||
},
|
||||
|
||||
// =====================
|
||||
// Early Entry Functions
|
||||
// =====================
|
||||
|
||||
_gameMode: null,
|
||||
_earlyEntryConfig: { enabled: false },
|
||||
_isQueued: false,
|
||||
|
||||
/**
|
||||
* Check if early entry is currently allowed based on mode and config
|
||||
*/
|
||||
isEarlyEntryAllowed: function() {
|
||||
const config = this._earlyEntryConfig;
|
||||
if (!config || !config.enabled) return false;
|
||||
|
||||
const mode = this._gameMode;
|
||||
if (mode === 'FREE' && config.allowInFreeMode) return true;
|
||||
if (mode === 'PAID_CREDITS' && config.allowInCreditsMode) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update early entry button visibility based on current state
|
||||
*/
|
||||
_updateEarlyEntryButton: function() {
|
||||
let $btn = $('#earlyEntryButton');
|
||||
|
||||
// Create button if it doesn't exist
|
||||
if (!$btn.length) {
|
||||
$btn = $(`
|
||||
<button id="earlyEntryButton" style="
|
||||
display: none;
|
||||
background: linear-gradient(180deg, #662200, #441100);
|
||||
border: 2px solid #ff6600;
|
||||
color: #ffcc00;
|
||||
padding: 8px 16px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 0 5px #ff3300;
|
||||
animation: earlyEntryPulse 2s infinite;
|
||||
">⚡ ENTER NOW (RISKY!) ⚡</button>
|
||||
`);
|
||||
|
||||
// Add CSS animation
|
||||
if (!$('#earlyEntryStyles').length) {
|
||||
$('head').append(`
|
||||
<style id="earlyEntryStyles">
|
||||
@keyframes earlyEntryPulse {
|
||||
0%, 100% { box-shadow: 0 0 5px #ff6600; }
|
||||
50% { box-shadow: 0 0 15px #ff6600, 0 0 25px #ff3300; }
|
||||
}
|
||||
#earlyEntryButton:hover {
|
||||
background: linear-gradient(180deg, #883300, #551100) !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
|
||||
// Insert near animation toggle button
|
||||
const $animBtn = $('#animationToggleButton');
|
||||
if ($animBtn.length) {
|
||||
$animBtn.after($btn);
|
||||
} else {
|
||||
// Fallback - add to header area
|
||||
$('#header').append($btn);
|
||||
}
|
||||
|
||||
// Click handler
|
||||
$btn.on('click', function() {
|
||||
SocketHandlers.requestEarlyEntry();
|
||||
});
|
||||
}
|
||||
|
||||
// Show/hide based on whether we're queued and early entry is allowed
|
||||
if (this._isQueued && this.isEarlyEntryAllowed()) {
|
||||
$btn.show();
|
||||
} else {
|
||||
$btn.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Request early entry from the server
|
||||
*/
|
||||
requestEarlyEntry: function() {
|
||||
if (!window.socket) return;
|
||||
|
||||
// Disable button to prevent double-clicks
|
||||
const $btn = $('#earlyEntryButton');
|
||||
$btn.prop('disabled', true).text('⏳ Entering...');
|
||||
|
||||
socket.emit('early_entry');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle early entry success
|
||||
*/
|
||||
onEarlyEntrySuccess: function(data) {
|
||||
console.log('Early entry success:', data);
|
||||
SocketHandlers._isQueued = false;
|
||||
SocketHandlers._updateEarlyEntryButton();
|
||||
|
||||
// The game_start event will follow - just show feedback
|
||||
$('#messages').append($('<li class="status" style="color:#ff6600;">').text('⚡ Early entry! Race to escape before the next block!'));
|
||||
UI.scrollChat();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle early entry error
|
||||
*/
|
||||
onEarlyEntryError: function(data) {
|
||||
console.error('Early entry error:', data);
|
||||
|
||||
// Re-enable button
|
||||
const $btn = $('#earlyEntryButton');
|
||||
$btn.prop('disabled', false).text('⚡ ENTER NOW (RISKY!) ⚡');
|
||||
|
||||
const message = data?.message || 'Early entry not available';
|
||||
$('#messages').append($('<li class="error" style="color:#f00;">').text('Early entry failed: ' + message));
|
||||
UI.scrollChat();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Help Modal
|
||||
* Displays game instructions and payment mode information
|
||||
*/
|
||||
const HelpModal = {
|
||||
_config: null,
|
||||
|
||||
init: function() {
|
||||
// Bind close buttons
|
||||
$('#helpModalClose, #helpModalCloseBtn').on('click', function() {
|
||||
HelpModal.hide();
|
||||
});
|
||||
|
||||
// Bind help button
|
||||
$('#helpButton').on('click', function() {
|
||||
HelpModal.show();
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
$('#helpModal').on('click', function(e) {
|
||||
if (e.target === this) {
|
||||
HelpModal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.key === 'Escape' && !$('#helpModal').hasClass('hidden')) {
|
||||
HelpModal.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateConfig: function(config) {
|
||||
this._config = config;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._renderPaymentModes();
|
||||
$('#helpModal').removeClass('hidden');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
$('#helpModal').addClass('hidden');
|
||||
},
|
||||
|
||||
_renderPaymentModes: function() {
|
||||
const $container = $('#helpModesList');
|
||||
$container.empty();
|
||||
|
||||
const config = this._config || {};
|
||||
const currency = config.cryptoType || 'WOW';
|
||||
|
||||
// Check if payments are disabled (free mode)
|
||||
if (!config.paymentsEnabled) {
|
||||
$container.html(`
|
||||
<div style="padding:8px; background:#0d3320; border:1px solid #4ade80; border-radius:4px; margin-bottom:8px;">
|
||||
<strong style="color:#4ade80;">🆓 Free Mode</strong><br>
|
||||
<span>Play for free! No payment required. Payouts may or may not be enabled.</span>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct payment mode
|
||||
if (config.directModeEnabled) {
|
||||
const price = config.singleGamePriceFormatted || '1';
|
||||
const multipliers = config.payoutMultipliers?.direct || {};
|
||||
const escapeMulti = multipliers.escape || 2;
|
||||
const treasureMulti = multipliers.treasure || 3;
|
||||
|
||||
$container.append(`
|
||||
<div style="padding:8px; background:#1a1a2e; border:1px solid #3730a3; border-radius:4px; margin-bottom:8px;">
|
||||
<strong style="color:#818cf8;">⚡ Direct Payment</strong><br>
|
||||
<span>Pay <strong>${price} ${currency}</strong> per game.</span><br>
|
||||
<span style="font-size:0.85em; color:#aaa;">Escape: ${escapeMulti}× payout | With treasure: ${treasureMulti}× payout</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Credits mode
|
||||
if (config.creditsModeEnabled) {
|
||||
const packages = config.creditPackages || [];
|
||||
const creditsPayoutEnabled = config.creditsPayoutsEnabled;
|
||||
|
||||
let packagesInfo = '';
|
||||
if (packages.length > 0) {
|
||||
packagesInfo = packages.map(pkg => {
|
||||
const bonus = pkg.bonus > 0 ? ` +${pkg.bonus} bonus` : '';
|
||||
return `${pkg.credits}${bonus} credits for ${pkg.priceFormatted} ${currency}`;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
$container.append(`
|
||||
<div style="padding:8px; background:#1a1a2e; border:1px solid #7c3aed; border-radius:4px; margin-bottom:8px;">
|
||||
<strong style="color:#a78bfa;">🎫 Credits Mode</strong><br>
|
||||
<span>Buy credits in bulk and use them to play.</span><br>
|
||||
${packagesInfo ? `<span style="font-size:0.85em; color:#aaa;">Packages: ${packagesInfo}</span><br>` : ''}
|
||||
${creditsPayoutEnabled
|
||||
? '<span style="font-size:0.85em; color:#4ade80;">✓ Payouts enabled for credit games</span>'
|
||||
: '<span style="font-size:0.85em; color:#888;">Payouts not available for credit games</span>'}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// If no paid modes are shown but payments enabled
|
||||
if (!config.directModeEnabled && !config.creditsModeEnabled && config.paymentsEnabled) {
|
||||
$container.html(`
|
||||
<p style="color:#888;">Payment configuration is loading...</p>
|
||||
`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.HelpModal = HelpModal;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
$(document).ready(function() {
|
||||
HelpModal.init();
|
||||
});
|
||||
@@ -13,6 +13,13 @@ const DEFAULT_CONFIG = Object.freeze({
|
||||
minPayment: 1000000000n, // 0.01 WOW in atomic units
|
||||
maxPayment: 100000000000000n // 1,000 WOW in atomic units
|
||||
},
|
||||
// Early entry allows free/credits mode players to start immediately without waiting for next block
|
||||
// Risk: If next block is found before they escape, they lose (timeout)
|
||||
earlyEntry: {
|
||||
enabled: true, // Master toggle for early entry feature
|
||||
allowInFreeMode: true, // Allow early entry in free mode
|
||||
allowInCreditsMode: true // Allow early entry when using credits
|
||||
},
|
||||
modes: {
|
||||
direct: {
|
||||
enabled: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
getDecimalDivisor
|
||||
} = require('./helpers/gameModeUtils');
|
||||
const { ValidationError, normalizeError } = require('../utils/errors');
|
||||
const paymentConfig = require('../config/paymentConfig');
|
||||
|
||||
const DEFAULT_SINGLE_GAME_PRICE = 5000000000; // 0.005 XMR or 0.05 WOW depending on currency decimals
|
||||
const DEFAULT_CREDITS_PACKAGE_PRICE = 50000000000;
|
||||
@@ -1066,6 +1067,11 @@ class GameModeManager {
|
||||
paymentRequired: this.paymentsEnabled,
|
||||
creditsSystem: this.creditsModeEnabled,
|
||||
payouts: this.directPayoutMultipliers.escape > 0 || this.creditPayoutMultipliers.escape > 0
|
||||
},
|
||||
earlyEntry: {
|
||||
enabled: paymentConfig.earlyEntry?.enabled ?? false,
|
||||
allowInFreeMode: paymentConfig.earlyEntry?.allowInFreeMode ?? false,
|
||||
allowInCreditsMode: paymentConfig.earlyEntry?.allowInCreditsMode ?? false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,6 +120,147 @@ class QueueHandler {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if early entry is allowed for the current mode
|
||||
* @returns {Object} { allowed: boolean, reason: string }
|
||||
*/
|
||||
isEarlyEntryAllowed() {
|
||||
if (!this.gameModeManager) {
|
||||
return { allowed: true, reason: 'No game mode manager' };
|
||||
}
|
||||
|
||||
const config = this.gameModeManager.configSnapshot;
|
||||
const earlyEntry = config?.earlyEntry;
|
||||
|
||||
// Check master toggle
|
||||
if (!earlyEntry?.enabled) {
|
||||
return { allowed: false, reason: 'Early entry is disabled' };
|
||||
}
|
||||
|
||||
const mode = this.gameModeManager.gameMode;
|
||||
|
||||
// Check mode-specific toggles
|
||||
if (mode === 'FREE' && !earlyEntry.allowInFreeMode) {
|
||||
return { allowed: false, reason: 'Early entry not allowed in free mode' };
|
||||
}
|
||||
|
||||
if (mode === 'PAID_CREDITS' && !earlyEntry.allowInCreditsMode) {
|
||||
return { allowed: false, reason: 'Early entry not allowed in credits mode' };
|
||||
}
|
||||
|
||||
// PAID_SINGLE (direct payment) should NOT allow early entry - players paid for a full block
|
||||
if (mode === 'PAID_SINGLE') {
|
||||
return { allowed: false, reason: 'Early entry not allowed for direct payment games' };
|
||||
}
|
||||
|
||||
return { allowed: true, reason: `Early entry allowed for ${mode}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle early entry request - start game immediately without waiting for next block
|
||||
* Risk: Player will die if next block is found before they escape
|
||||
*/
|
||||
async handleEarlyEntry(socket, getUserBySocket) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'game:queue');
|
||||
if (!rateLimitResult.allowed) {
|
||||
const msg = `Please wait ${Math.ceil(rateLimitResult.retryAfter / 1000)} seconds before trying again.`;
|
||||
socket.emit('early_entry_error', { message: msg });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'warning', msg);
|
||||
return { success: false, reason: 'rate_limited' };
|
||||
}
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`⚡ Player ${socket.id} requested early entry`);
|
||||
}
|
||||
|
||||
// Check if early entry is allowed
|
||||
const earlyEntryCheck = this.isEarlyEntryAllowed();
|
||||
if (!earlyEntryCheck.allowed) {
|
||||
socket.emit('early_entry_error', { message: earlyEntryCheck.reason });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', earlyEntryCheck.reason);
|
||||
return { success: false, reason: earlyEntryCheck.reason };
|
||||
}
|
||||
|
||||
const currentUser = getUserBySocket(socket.id);
|
||||
if (!currentUser) {
|
||||
const msg = 'Error: Could not start game. Please try again.';
|
||||
socket.emit('early_entry_error', { message: msg });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', msg);
|
||||
return { success: false, reason: 'user_not_found' };
|
||||
}
|
||||
|
||||
// Check if already in queue
|
||||
if (this.queueManager.isPlayerQueued(socket.id)) {
|
||||
const msg = 'You are already in the queue! Use early entry only when not queued.';
|
||||
socket.emit('early_entry_error', { message: msg });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info', msg);
|
||||
return { success: false, reason: 'already_queued' };
|
||||
}
|
||||
|
||||
// Check if already in a game
|
||||
if (this.activeGames.has(socket.id)) {
|
||||
const msg = 'You are already in a game!';
|
||||
socket.emit('early_entry_error', { message: msg });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', msg);
|
||||
return { success: false, reason: 'already_in_game' };
|
||||
}
|
||||
|
||||
// Check payment eligibility
|
||||
const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
|
||||
if (!paymentCheckResult.allowed) {
|
||||
switch (paymentCheckResult.action) {
|
||||
case 'set_address':
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm.');
|
||||
break;
|
||||
case 'make_payment':
|
||||
await this.paymentHandlers.createAndShowPaymentRequest(socket);
|
||||
break;
|
||||
default:
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
|
||||
paymentCheckResult.reason || 'Not allowed to start game');
|
||||
}
|
||||
return { success: false, reason: paymentCheckResult.reason };
|
||||
}
|
||||
|
||||
// Record the attempt
|
||||
await this.rateLimiter.recordAttempt(socket.id, 'game:queue');
|
||||
|
||||
// Start the game immediately
|
||||
const currentBlock = this.debugManager.getCurrentBlockHeight();
|
||||
|
||||
// For early entry, the player's blockRec is set to current block
|
||||
// This means they will die when the NEXT block is found (currentBlock + 1)
|
||||
const result = await this.queueManager.startEarlyGame(socket.id, currentUser, currentBlock);
|
||||
|
||||
if (result.success) {
|
||||
// Emit early entry success event
|
||||
socket.emit('early_entry_success', { blockHeight: currentBlock });
|
||||
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
`⚡ Early entry! Game started on block ${currentBlock}. Escape before block ${currentBlock + 1}!`);
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`⚡ EARLY ENTRY: Player ${socket.id} started on block ${currentBlock}`);
|
||||
}
|
||||
return { success: true, blockHeight: currentBlock };
|
||||
} else {
|
||||
// Emit early entry error event
|
||||
socket.emit('early_entry_error', { message: result.reason || 'Failed to start early game' });
|
||||
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', result.reason || 'Failed to start early game');
|
||||
return { success: false, reason: result.reason };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const normalized = normalizeError(error, 'Failed to start early game');
|
||||
console.error('handleEarlyEntry error:', normalized.message);
|
||||
socket.emit('early_entry_error', { message: normalized.message });
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.message);
|
||||
return { success: false, reason: normalized.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -155,6 +155,55 @@ class QueueManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a game immediately for early entry (not from queue)
|
||||
* Used when player opts for early entry without waiting for block
|
||||
* @param {string} serverId - Socket ID
|
||||
* @param {Object} currentUser - User object
|
||||
* @param {number} blockHeight - Current block height
|
||||
* @returns {Object} { success: boolean, reason?: string }
|
||||
*/
|
||||
async startEarlyGame(serverId, currentUser, blockHeight) {
|
||||
if (!currentUser) {
|
||||
return { success: false, reason: 'User not found' };
|
||||
}
|
||||
|
||||
// Check if already in a game
|
||||
if (this.activeGames && this.activeGames.has(serverId)) {
|
||||
return { success: false, reason: 'Already in a game' };
|
||||
}
|
||||
|
||||
// Set blockRec to current block - player dies when next block (currentBlock + 1) is found
|
||||
currentUser.blockRec = blockHeight;
|
||||
currentUser.isEarlyEntry = true; // Mark as early entry for potential special handling
|
||||
|
||||
try {
|
||||
const game = this.createGameForUser(currentUser, 'standard', { earlyEntry: true });
|
||||
const gameState = game.getState();
|
||||
gameState.blockHeight = blockHeight;
|
||||
gameState.isEarlyEntry = true;
|
||||
gameState.deathBlock = blockHeight + 1; // Explicit death block for client display
|
||||
|
||||
// Include provably fair commitment
|
||||
if (game.getProofCommitment) {
|
||||
gameState.proof = game.getProofCommitment();
|
||||
}
|
||||
|
||||
this.io.to(serverId).emit('game_start', gameState);
|
||||
|
||||
if (this.CONSOLE_LOGGING) {
|
||||
console.log(`[QueueManager] ⚡ Early entry game started for ${serverId} at block ${blockHeight} (dies at ${blockHeight + 1})`);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
const normalized = normalizeError(err, 'Failed to start early game');
|
||||
console.error('[QueueManager] Error starting early game:', normalized.message);
|
||||
this.io.to(serverId).emit('message', 'Error starting game: ' + normalized.message);
|
||||
return { success: false, reason: normalized.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of pending games (players waiting in queue)
|
||||
* Used by spectator system to show upcoming games
|
||||
|
||||
@@ -378,6 +378,7 @@ class SocketHandlers {
|
||||
}
|
||||
});
|
||||
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
|
||||
socket.on('early_entry', () => this.handleEarlyEntry(socket)); // Early entry without waiting for block
|
||||
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
|
||||
|
||||
// Payment system handlers
|
||||
@@ -490,6 +491,28 @@ class SocketHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle early entry request - start game immediately without waiting for block
|
||||
* Only available for free mode and credits mode (not direct payment mode)
|
||||
*/
|
||||
async handleEarlyEntry(socket) {
|
||||
try {
|
||||
const result = await this.queueHandler.handleEarlyEntry(
|
||||
socket,
|
||||
this.connectionHandler.getUserBySocket.bind(this.connectionHandler)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`⚡ Early entry successful for ${socket.id}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('handleEarlyEntry error:', err);
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Unexpected error with early entry.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddressPrompt(socket) {
|
||||
try {
|
||||
if (this.chatHandler && typeof this.chatHandler.promptAddress === 'function') {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Tests for Early Entry feature
|
||||
* Early Entry allows players in free/credits mode to enter the dungeon immediately
|
||||
* without waiting for the next block, with the risk of dying if a block is found.
|
||||
*/
|
||||
|
||||
const PaymentConfigManager = require('../src/config/paymentConfig');
|
||||
|
||||
describe('Early Entry Feature', () => {
|
||||
describe('Configuration', () => {
|
||||
it('should have earlyEntry configuration in DEFAULT_CONFIG', () => {
|
||||
const defaultConfig = PaymentConfigManager.DEFAULT_CONFIG;
|
||||
expect(defaultConfig.earlyEntry).toBeDefined();
|
||||
expect(typeof defaultConfig.earlyEntry.enabled).toBe('boolean');
|
||||
expect(typeof defaultConfig.earlyEntry.allowInFreeMode).toBe('boolean');
|
||||
expect(typeof defaultConfig.earlyEntry.allowInCreditsMode).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have sensible defaults', () => {
|
||||
const defaultConfig = PaymentConfigManager.DEFAULT_CONFIG;
|
||||
// These should be enabled by default for better UX in free/credits modes
|
||||
expect(defaultConfig.earlyEntry.enabled).toBe(true);
|
||||
expect(defaultConfig.earlyEntry.allowInFreeMode).toBe(true);
|
||||
expect(defaultConfig.earlyEntry.allowInCreditsMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should include earlyEntry in manager config', () => {
|
||||
const manager = new PaymentConfigManager();
|
||||
const config = manager.getConfig();
|
||||
expect(config.earlyEntry).toBeDefined();
|
||||
expect(config.earlyEntry.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueueHandler.isEarlyEntryAllowed', () => {
|
||||
let mockQueueHandler;
|
||||
const earlyEntryConfig = PaymentConfigManager.DEFAULT_CONFIG.earlyEntry;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a mock queue handler with the method we want to test
|
||||
mockQueueHandler = {
|
||||
gameModeManager: {
|
||||
gameMode: 'FREE'
|
||||
},
|
||||
isEarlyEntryAllowed: function() {
|
||||
const config = earlyEntryConfig;
|
||||
|
||||
// Check if early entry is enabled globally
|
||||
if (!config || !config.enabled) {
|
||||
return { allowed: false, reason: 'Early entry is not enabled' };
|
||||
}
|
||||
|
||||
// Get current game mode
|
||||
const mode = this.gameModeManager?.gameMode;
|
||||
|
||||
// Check mode-specific permissions
|
||||
if (mode === 'FREE') {
|
||||
if (!config.allowInFreeMode) {
|
||||
return { allowed: false, reason: 'Early entry not available in free mode' };
|
||||
}
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
if (mode === 'PAID_CREDITS') {
|
||||
if (!config.allowInCreditsMode) {
|
||||
return { allowed: false, reason: 'Early entry not available in credits mode' };
|
||||
}
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// Direct payment mode - not allowed
|
||||
if (mode === 'PAID_SINGLE') {
|
||||
return { allowed: false, reason: 'Early entry not available in direct payment mode' };
|
||||
}
|
||||
|
||||
return { allowed: false, reason: 'Unknown game mode' };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should allow early entry in FREE mode when configured', () => {
|
||||
mockQueueHandler.gameModeManager.gameMode = 'FREE';
|
||||
const result = mockQueueHandler.isEarlyEntryAllowed();
|
||||
expect(result.allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow early entry in PAID_CREDITS mode when configured', () => {
|
||||
mockQueueHandler.gameModeManager.gameMode = 'PAID_CREDITS';
|
||||
const result = mockQueueHandler.isEarlyEntryAllowed();
|
||||
expect(result.allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT allow early entry in PAID_SINGLE mode', () => {
|
||||
mockQueueHandler.gameModeManager.gameMode = 'PAID_SINGLE';
|
||||
const result = mockQueueHandler.isEarlyEntryAllowed();
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.reason).toContain('direct payment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueueManager.startEarlyGame', () => {
|
||||
it('should set isEarlyEntry flag on user', async () => {
|
||||
// Test that when startEarlyGame is called, the user gets marked
|
||||
const mockUser = { id: 'test-socket-id' };
|
||||
const blockHeight = 12345;
|
||||
|
||||
// Simulate what startEarlyGame does
|
||||
mockUser.blockRec = blockHeight;
|
||||
mockUser.isEarlyEntry = true;
|
||||
|
||||
expect(mockUser.blockRec).toBe(12345);
|
||||
expect(mockUser.isEarlyEntry).toBe(true);
|
||||
});
|
||||
|
||||
it('should calculate death block correctly', () => {
|
||||
const currentBlock = 12345;
|
||||
const deathBlock = currentBlock + 1;
|
||||
|
||||
expect(deathBlock).toBe(12346);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameModeManager.getGameModeInfo', () => {
|
||||
const GameModeManager = require('../src/game/gameModeManager');
|
||||
|
||||
it('should include earlyEntry config in getGameModeInfo', () => {
|
||||
const manager = new GameModeManager({ cryptoType: 'WOW' });
|
||||
const info = manager.getGameModeInfo();
|
||||
|
||||
expect(info.earlyEntry).toBeDefined();
|
||||
expect(typeof info.earlyEntry.enabled).toBe('boolean');
|
||||
expect(typeof info.earlyEntry.allowInFreeMode).toBe('boolean');
|
||||
expect(typeof info.earlyEntry.allowInCreditsMode).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user