/** * Socket event handlers for the Wownerogue game */ const SocketHandlers = { _initialized: false, _didConnect: false, _setBannerStatus: function(text, color) { const el = $('#statusValue'); if (!el.length) return; el.text(text); if (color) el.css('color', color); }, _hasPayoutAddress: false, _creditsBalance: 0, _gameMode: 'free', _directPayoutsEnabled: false, _paymentsEnabled: false, payoutAddressRequired: function() { if (this._gameMode === 'PAID_SINGLE' && this._directPayoutsEnabled) return true; if (this._gameMode === 'PAID_CREDITS' && this._creditsPayoutsEnabled) return true; return false; }, canAffordGame: function() { if (this._gameMode === 'FREE') return true; if (this._gameMode === 'PAID_SINGLE') return true; // Payment will be requested if (this._gameMode === 'PAID_CREDITS') { return this._creditsBalance >= (this._creditsPerGame || 1); } return false; }, _updateAddressButtonStatus: function(hasAddress) { const $btn = $('#manageAddressButton'); this._hasPayoutAddress = !!hasAddress; 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) { return; } if (!window.socket) { console.error("Socket not available!"); return; } this.registerEventHandlers(); this._initialized = true; // Delegated handlers for proof copy buttons $(document).on('click', '.copy-hash-btn', function() { const hash = $(this).data('hash'); if (hash) { SocketHandlers._copyToClipboard(hash, $(this)); } }); $(document).on('click', '.copy-seed-btn', function() { const seed = $(this).data('seed'); if (seed) { SocketHandlers._copyToClipboard(seed, $(this)); } }); // If the low-level socket connected before handlers were registered, emulate onConnect. if (window.socket && window.socket.connected) { this.onConnect(); } }, _copyToClipboard: function(text, $btn) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function() { const origText = $btn.text(); $btn.text('Copied!'); setTimeout(function() { $btn.text(origText); }, 1500); }); } else { // Fallback const $temp = $(''); $('body').append($temp); $temp.val(text).select(); document.execCommand('copy'); $temp.remove(); const origText = $btn.text(); $btn.text('Copied!'); setTimeout(function() { $btn.text(origText); }, 1500); } }, registerEventHandlers: function() { // Connection handlers socket.on('connect', this.onConnect); socket.on('welcome', this.onWelcome); socket.on('session_token', this.onSessionToken); socket.on('session_resumed', this.onSessionResumed); socket.on('message', this.onMessage); socket.on('status_update', this.onStatusUpdate); socket.on('chat_broadcast', this.onChatBroadcast); socket.on('chat_history', this.onChatHistory); // Game state handlers socket.on('waiting_status', this.onWaitingStatus); socket.on('queue_joined', this.onQueueJoined); socket.on('game_start', this.onGameStart); socket.on('game_update', this.onGameUpdate); socket.on('game_over', this.onGameOver); socket.on('queue_cancelled', this.onQueueCancelled); // Payment/Address handlers socket.on('address_detected', this.onAddressDetected); socket.on('address_confirmed', this.onAddressConfirmed); socket.on('address_update_error', this.onAddressUpdateError); socket.on('address_prompt', this.onAddressPrompt); socket.on('game_mode_info', this.onGameModeInfo); 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); socket.on('user_count', this.onUserCount); // 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); socket.on('spectator_update', this.onSpectatorUpdate); socket.on('spectate_ended', this.onSpectateEnded); }, onConnect: function() { if (SocketHandlers._didConnect) return; // prevent duplicate registration emission SocketHandlers._didConnect = true; // Include stored session token in a lightweight resume emit (if server did not get it via handshake) try { const existing = localStorage.getItem('wownerogue_token'); if (existing) { // If we later decide to rely solely on query param at io() creation, this is harmless redundancy. socket.io.opts.query = socket.io.opts.query || {}; socket.io.opts.query.resumeToken = existing; } } catch (e) {} socket.emit('register_client', { clientId: socket.id, userAgent: navigator.userAgent }); // Update small status banner immediately SocketHandlers._setBannerStatus('Connected', '#0f0'); }, onSessionToken: function(data) { if (data && data.token) { try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {} $('#messages').append($('
  • ').text('New session established. Token stored.')); UI.scrollChat(); // New session = no address set yet SocketHandlers._updateAddressButtonStatus(false); } }, onSessionResumed: function(data) { if (data && data.token) { try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {} $('#messages').append($('
  • ').text('Session resumed.')); UI.scrollChat(); } if (data && typeof data.credits === 'number') { SocketHandlers._updateCreditsDisplay(data.credits); } if (data && data.payoutAddress) { $('#messages').append($('
  • ').text('Payout address restored.')); UI.scrollChat(); if (typeof AddressModal !== 'undefined') { AddressModal.setCurrentAddress(data.payoutAddress); } SocketHandlers._updateAddressButtonStatus(true); } else { SocketHandlers._updateAddressButtonStatus(false); } }, onCreditsUpdate: function(data) { if (!data) return; if (typeof data.balance === 'number') { SocketHandlers._updateCreditsDisplay(data.balance); } }, // Default credits per game (overridden by game_mode_info) _creditsPerGame: 1, _updateCreditsDisplay: function(balance, creditsPerGame) { // Store current balance this._creditsBalance = balance; // Update stored creditsPerGame if provided if (creditsPerGame && creditsPerGame > 0) { SocketHandlers._creditsPerGame = creditsPerGame; } let el = document.getElementById('creditsDisplay'); if (!el) { // Create a small unobtrusive badge in header if not present const header = document.getElementById('header') || document.body; el = document.createElement('div'); el.id = 'creditsDisplay'; el.style.cssText = 'position:absolute;top:4px;right:8px;font-size:12px;font-family:monospace;color:#0af;background:#111;padding:2px 6px;border:1px solid #044;border-radius:4px;'; header.appendChild(el); } // Calculate games remaining const perGame = SocketHandlers._creditsPerGame || 1; const gamesRemaining = Math.floor(balance / perGame); // Show credits and games remaining if (gamesRemaining > 0) { el.textContent = 'Credits: ' + balance + ' (' + gamesRemaining + ' game' + (gamesRemaining !== 1 ? 's' : '') + ')'; } else { el.textContent = 'Credits: ' + balance; } }, onWelcome: function(msg) { $('#messages').append($('
  • ').text("Connected to server!")); UI.scrollChat(); }, onMessage: function(msg) { $('#messages').append($('
  • ').text(msg)); UI.scrollChat(); }, onStatusUpdate: function(data) { const statusClass = data.type === 'error' ? 'error' : 'status'; const statusColor = data.type === 'error' ? '#f00' : data.type === 'warning' ? '#ff0' : data.type === 'success' ? '#0f0' : data.type === 'payment' ? '#0af' : '#fff'; $('#messages').append($('
  • ').text(data.message)); UI.scrollChat(); // Mirror concise status in header banner for key lifecycle types if (data.type === 'connection') { SocketHandlers._setBannerStatus('Ready', '#0f0'); } else if (data.type === 'queue') { SocketHandlers._setBannerStatus('Queued', '#0af'); } else if (data.type === 'payment') { // Generic payment message (more granular handlers override later) SocketHandlers._setBannerStatus('Payment', '#0af'); if (typeof AudioAlerts !== 'undefined') { AudioAlerts.playRequestCoin(); } } else if (data.type === 'error') { SocketHandlers._setBannerStatus('Error', '#f00'); } else if (data.type === 'success') { SocketHandlers._setBannerStatus('Success', '#0f0'); } }, onChatBroadcast: function(data) { const msgElement = $('
  • '); if (data.socketId) { msgElement.html('' + data.socketId.substring(0, 6) + ': ' + data.message); } else { msgElement.text(data.message); } $('#messages').append(msgElement); UI.scrollChat(); }, onChatHistory: function(data) { if (!data || !data.messages || !Array.isArray(data.messages)) return; // Add a separator for history messages if (data.messages.length > 0) { $('#messages').append($('
  • ').text('--- Recent Chat History ---')); } // Add each historical message data.messages.forEach(function(msg) { const msgElement = $('
  • '); const username = msg.username || (msg.socketId ? msg.socketId.substring(0, 6) : 'anon'); msgElement.html('' + username + ': ' + msg.message); $('#messages').append(msgElement); }); if (data.messages.length > 0) { $('#messages').append($('
  • ').text(data.message)); // Check if we should show waiting screen const addressRequired = SocketHandlers.payoutAddressRequired(); const hasAddress = SocketHandlers._hasPayoutAddress; const canAfford = SocketHandlers.canAffordGame(); const isFree = SocketHandlers._gameMode === 'FREE'; const shouldShowWaiting = isFree || (canAfford && (!addressRequired || hasAddress)); if (shouldShowWaiting && typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } } UI.scrollChat(); }, 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 ? UI._currentBlockHeight : '?'); const nextBlockFallback = (typeof data.nextBlock === 'number' && data.nextBlock > 0) ? data.nextBlock : (currentBlockFallback === '?' ? '?' : currentBlockFallback + 1); $('#messages').append($('
  • ').html( '⏳ Queue Joined! Position: ' + data.position + '
    ' + '📦 Current block: ' + currentBlockFallback + ', Next: ' + nextBlockFallback )); UI.scrollChat(); if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } SocketHandlers._setBannerStatus('Queued', '#0af'); }, onGameStart: function(data) { // Clear queued state SocketHandlers._isQueued = false; SocketHandlers._updateEarlyEntryButton(); // Hide any lingering QR code SocketHandlers.hidePaymentQR(); $('#messages').append($('
  • ').text("Starting game...")); // Display provably fair commitment if present if (data && data.proof && data.proof.commitment) { const shortHash = data.proof.commitment.substring(0, 16) + '...'; const $proofMsg = $('
  • ').html( '🔐 Provably Fair: Game hash commitment: ' + shortHash + ' ' + '' ); $('#messages').append($proofMsg); // Store for later verification display SocketHandlers._currentGameProof = data.proof; } if (typeof Game !== 'undefined' && Game.stopWaitingScreen) { Game.stopWaitingScreen(); } if (!data) { $('#messages').append($('
  • ').text("Error: No game data received from server.")); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); return; } try { var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles, data.lighting, data.torches); if (!success) { $('#messages').append($('
  • ').text("Game start failed. Check console for details.")); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); } else { setTimeout(function() { $('#game-display').focus(); }, 100); } } catch (error) { console.error("Error starting game:", error); $('#messages').append($('
  • ').text("Game start error: " + error.message)); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); } UI.scrollChat(); SocketHandlers._setBannerStatus('In Game', '#0f0'); // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate }, onGameUpdate: function(data) { // Server pushed a game state update. The Game object exposes updateGameState(). // Older code referenced Game.updateGame, which doesn't exist in the current refactor, // so updates were silently ignored (player appeared frozen). if (typeof Game !== 'undefined') { if (Game.updateGameState) { Game.updateGameState(data); } else if (Game.updateGame) { // Fallback if an alias gets added later Game.updateGame(data); } else { console.warn('Game update received but no updateGameState()/updateGame() method found on Game.'); } } UI.scrollChat(); }, onGameOver: function(data) { $('#messages').append($('
  • ').text("Game Over: " + data.message)); // Display provably fair verification info if present if (data && data.proof && data.proof.seed) { const shortSeed = data.proof.seed.substring(0, 16) + '...'; const shortCommitment = data.proof.commitment.substring(0, 16) + '...'; const verifyUrl = data.proof.verificationUrl || ('/verify/' + data.proof.gameId); const $proofReveal = $('
  • ').html( '🔓 Game Verified:
    ' + 'Seed: ' + shortSeed + ' ' + '
    ' + 'Hash: ' + shortCommitment + '
    ' + '🔗 Verify this game' ); $('#messages').append($proofReveal); // Clear stored proof SocketHandlers._currentGameProof = null; } if (typeof Game !== 'undefined') { if (Game.endGame) { Game.endGame(data); } else if (Game.drawLoseScreen && data && data.reason) { // Minimal fallback if endGame not present if (data.reason === 'monster') Game.drawLoseScreen('monster'); } } UI.scrollChat(); const won = data && (data.status === 'won' || data.reason === 'escaped'); if (won) { SocketHandlers._setBannerStatus('Won', '#0f0'); } else { SocketHandlers._setBannerStatus('Lost', '#f00'); } }, onQueueCancelled: function(data) { // Clear queued state SocketHandlers._isQueued = false; SocketHandlers._updateEarlyEntryButton(); $('#messages').append($('
  • ').text("Queue entry cancelled.")); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) { Game._drawWelcomeScreen(); } UI.scrollChat(); SocketHandlers._setBannerStatus('Ready', '#0f0'); }, onAddressDetected: function(data) { console.log('Address detected:', data); $('#messages').append($('
  • ').text(data.message)); UI.scrollChat(); }, onAddressConfirmed: function(data) { console.log('Address confirmed:', data); // Cleanup previous address-related messages to reduce clutter $('#messages li').each(function() { const text = $(this).text(); if (text.includes('Paste your payout address') || text.includes('Detected payout address') || text.includes('Type \'confirm\' to save') || text.includes('Address detected. Type confirm')) { $(this).remove(); } }); $('#messages').append($('
  • ').text(data.message)); UI.scrollChat(); 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) { if (typeof AddressModal !== 'undefined') { AddressModal.show({ existingAddress: data?.existingAddress || null, message: data?.message || null }); } }, onAddressUpdateError: function(data) { if (typeof AddressModal !== 'undefined') { AddressModal.handleError(data?.message || 'Failed to update address.'); } }, onGameModeInfo: function(data) { console.log('Game mode info received:', data); // Store creditsPerGame for calculating games remaining if (data.creditsPerGame) { SocketHandlers._creditsPerGame = data.creditsPerGame; } // Store game mode and early entry config SocketHandlers._gameMode = data.mode; SocketHandlers._earlyEntryConfig = data.earlyEntry || { enabled: false }; SocketHandlers._creditsPayoutsEnabled = !!data.creditsPayoutsEnabled; SocketHandlers._directPayoutsEnabled = !!data.directPayoutsEnabled; SocketHandlers._paymentsEnabled = !!data.paymentsEnabled; if (typeof UI !== 'undefined' && UI.updateGameTitle) { UI.updateGameTitle(data.cryptoType); } // Update ScreenManager with crypto type for welcome screen title if (typeof ScreenManager !== 'undefined' && ScreenManager.setCryptoType) { ScreenManager.setCryptoType(data.cryptoType); } // Display testnet/stagenet warning if applicable if (data.testnetWarning) { SocketHandlers._showNetworkWarning(data.testnetWarning, data.network); } else { SocketHandlers._hideNetworkWarning(); } if (typeof PaymentUI !== 'undefined') { PaymentUI.updateConfig(data); // Show shop button if payments enabled if (data.paymentsEnabled) { $('#shopButton').show(); } else { $('#shopButton').hide(); } } // Update HelpModal with config if (typeof HelpModal !== 'undefined') { HelpModal.updateConfig(data); } // Update early entry button visibility SocketHandlers._updateEarlyEntryButton(); }, _showNetworkWarning: function(message, network) { let $warning = $('#networkWarning'); if (!$warning.length) { $warning = $('
    '); // Insert at top of container $('.container').prepend($warning); } const networkUpper = (network || 'stagenet').toUpperCase(); $warning.html(`
    ⚠️ ${networkUpper} MODE ⚠️
    ${message}
    `).show(); }, _hideNetworkWarning: function() { $('#networkWarning').hide(); }, onPaymentCreated: function(data) { console.log('Payment created:', data); if (typeof AudioAlerts !== 'undefined') { AudioAlerts.playRequestCoin(); } if (typeof PaymentUI !== 'undefined') { PaymentUI.showPaymentRequest(data); } const parts = []; const reusedCopy = data.reused ? '🔁 Pending payment request still active. Use the same amount and address below.' : '💳 Payment Required'; parts.push(reusedCopy); const displayAmount = data.humanAmount || data.amountFormatted || data.amount; parts.push('Amount: ' + displayAmount + ' ' + (data.cryptoType || '')); const shortAddr = data.address.substring(0, 10) + '…' + data.address.slice(-6); parts.push('Address: ' + shortAddr + '' + ' '); // (Removed inline QR image to avoid duplication; dedicated pinned sidebar QR is used instead.) const $li = $('
  • ').html(parts.join('
    ')); $('#messages').append($li); // Attach copy handler (delegated inside this LI to avoid duplicates) const fullAddress = data.address; $li.on('click', '.copy-pay-address', function(e) { e.preventDefault(); const addr = $(this).data('address'); const doCopy = async () => { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(addr); } else { // Fallback const ta = document.createElement('textarea'); ta.value = addr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } $(this).text('Copied!'); setTimeout(() => { $(this).text('Copy'); }, 2000); } catch (err) { console.error('Copy failed', err); $(this).text('Error'); setTimeout(() => { $(this).text('Copy'); }, 2500); } }; doCopy(); }); // Toggle short/full address on span click let showingFull = false; $li.on('click', '.pay-address-full', function() { showingFull = !showingFull; $(this).text(showingFull ? fullAddress : shortAddr); }); UI.scrollChat(); setTimeout(function() { $('#chatInput').focus(); }, 100); SocketHandlers._setBannerStatus('Pay', '#0af'); if (typeof Game !== 'undefined' && Game._paymentRequested) Game._paymentRequested(); // Show waiting screen (which will now say "Awaiting payment") if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } // Sidebar QR (persistent) - create/update separate from chat scroll if (data.qr) { let qrHolder = document.getElementById('paymentQRContainer'); if (!qrHolder) { const statusDiv = document.querySelector('.status'); if (statusDiv) { qrHolder = document.createElement('div'); qrHolder.id = 'paymentQRContainer'; qrHolder.style.cssText = 'position:relative;margin-top:6px;padding:8px;border:1px solid #0f0;background:#000;display:block;width:calc(100% - 18px);text-align:center;'; statusDiv.appendChild(qrHolder); } } if (qrHolder) { // Create close button const closeBtn = '
    ✕
    '; qrHolder.innerHTML = closeBtn + 'Payment QR'; } } else { console.warn('Payment created but no QR data supplied by server.'); let qrHolder = document.getElementById('paymentQRContainer'); if (!qrHolder) { const statusDiv = document.querySelector('.status'); if (statusDiv) { qrHolder = document.createElement('div'); qrHolder.id = 'paymentQRContainer'; qrHolder.style.cssText = 'margin-top:6px;padding:4px;border:1px solid #f80;background:#000;display:inline-block;color:#f80;font-size:11px;max-width:150px;'; statusDiv.appendChild(qrHolder); } } if (qrHolder) { qrHolder.textContent = 'QR unavailable'; } } }, // Helper to hide/remove the payment QR code container hidePaymentQR: function() { const qrHolder = document.getElementById('paymentQRContainer'); if (qrHolder) { qrHolder.remove(); } }, 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) { console.log('Payment confirmed in block:', data); if (!data || !data.paymentId) return; // ignore malformed legacy event // Expire old confirmations (6h default) to prevent unbounded memory SocketHandlers._expireOldClientPaymentMarkers(); if (SocketHandlers._lastDisplayedConfirmation === data.paymentId) return; SocketHandlers._lastDisplayedConfirmation = data.paymentId; SocketHandlers._confirmationTimestamps[data.paymentId] = Date.now(); // Hide the QR code now that payment is confirmed SocketHandlers.hidePaymentQR(); if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } if (typeof Game !== 'undefined') { Game._pendingPaymentConfirmed(); } $('#messages').append($('
  • ').html( '✅ Payment confirmed in block. You are in the game queue.' )); UI.scrollChat(); SocketHandlers._setBannerStatus('Confirmed', '#0f0'); // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate }, _mempoolShownForPayment: new Set(), _mempoolTimestamps: {}, // paymentId -> ts onPaymentDetected: function(data) { console.log('Payment detected (mempool):', data); if (!data || !data.paymentId) return; // require paymentId for dedupe SocketHandlers._expireOldClientPaymentMarkers(); if (SocketHandlers._mempoolShownForPayment.has(data.paymentId)) return; SocketHandlers._mempoolShownForPayment.add(data.paymentId); SocketHandlers._mempoolTimestamps[data.paymentId] = Date.now(); // Hide the QR code now that payment was detected SocketHandlers.hidePaymentQR(); if (typeof Game !== 'undefined') { Game._pendingPaymentDetected(data); if (Game.drawWaitingScreen) Game.drawWaitingScreen(); } $('#messages').append($('
  • ').html( '🌀 Payment detected (mempool) – awaiting block confirmation...' )); UI.scrollChat(); SocketHandlers._setBannerStatus('Mempool', '#0af'); // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate }, // Internal: periodic cleanup of client-side payment marker sets (invoked opportunistically) _expireOldClientPaymentMarkers: function() { const TTL = 6 * 60 * 60 * 1000; // 6 hours const now = Date.now(); // Mempool markers for (const pid of Array.from(SocketHandlers._mempoolShownForPayment)) { const ts = SocketHandlers._mempoolTimestamps[pid]; if (!ts || now - ts > TTL) { SocketHandlers._mempoolShownForPayment.delete(pid); delete SocketHandlers._mempoolTimestamps[pid]; } } // Confirmation marker (single id tracking + map of timestamps) if (SocketHandlers._lastDisplayedConfirmation) { const lastId = SocketHandlers._lastDisplayedConfirmation; const ts = SocketHandlers._confirmationTimestamps[lastId]; if (ts && now - ts > TTL) { delete SocketHandlers._confirmationTimestamps[lastId]; // Allow new confirmation message for same id after TTL (rare, but cleans memory) SocketHandlers._lastDisplayedConfirmation = null; } } }, onBlockHeight: function(data) { if (typeof UI !== 'undefined' && UI.updateBlockHeight) { UI.updateBlockHeight(data.blockHeight); } // Sync to ScreenManager for welcome screen display if (typeof ScreenManager !== 'undefined' && data.blockHeight) { ScreenManager._currentBlockHeight = data.blockHeight; // Redraw welcome screen if not in a game, not spectating, and not showing waiting screen if (typeof GameState !== 'undefined' && !GameState.isGameActive() && typeof Game !== 'undefined' && !Game._isSpectating && !ScreenManager.isShowingWaitingScreen()) { ScreenManager.drawWelcomeScreen(); } } // Fallback: if status banner still says Connecting..., upgrade it const current = $('#statusValue').text(); if (current === 'Connecting...') { SocketHandlers._setBannerStatus('Ready', '#0f0'); } }, onUserCount: function(data) { console.log('👥 User count received:', data); if (!data || typeof data.count !== 'number') return; SocketHandlers._updateUserCountDisplay(data.count); }, _updateUserCountDisplay: function(count) { let el = document.getElementById('userCountDisplay'); if (!el) { // Create display element next to connection status const statusDiv = document.querySelector('.status') || document.getElementById('connectionStatus')?.parentElement; if (statusDiv) { el = document.createElement('div'); el.id = 'userCountDisplay'; el.style.cssText = 'font-size:12px;color:#0af;margin-top:2px;'; statusDiv.appendChild(el); } } if (el) { const plural = count === 1 ? '' : 's'; el.innerHTML = `👥 ${count} player${plural} online`; } }, // ====== SPECTATOR HANDLERS ====== _spectatorMode: false, _spectatingGameId: null, _activeGames: [], onActiveGames: function(data) { if (!data || !data.games) return; SocketHandlers._activeGames = data.games; SocketHandlers._updateGamesListPanel(data); }, onSpectateStart: function(data) { if (!data) return; SocketHandlers._spectatorMode = true; SocketHandlers._spectatingGameId = data.gameId; // Hide games panel SocketHandlers._hideGamesPanel(); // Show spectator controls SocketHandlers._showSpectatorControls(data.playerId); // Start the game display in spectator mode if (typeof Game !== 'undefined' && data.initialState) { const state = data.initialState; try { // Use a modified startGame that marks as spectator Game._isSpectating = true; Game.startGame( state.player, state.map, state.monster, state.items || {}, state.visibleTiles, state.lighting, state.torches ); $('#messages').append($('
  • ').text( '👁️ Now spectating player ' + data.playerId + '. Press ESC or click "Leave" to exit.' )); UI.scrollChat(); } catch (err) { console.error('Failed to start spectator view:', err); } } SocketHandlers._setBannerStatus('Spectating', '#0af'); }, onSpectatorUpdate: function(data) { if (!data || !SocketHandlers._spectatorMode) return; // Update the game display with new state if (typeof Game !== 'undefined' && typeof Game.updateGameState === 'function') { Game.updateGameState(data.gameState); } }, onSpectateEnded: function(data) { SocketHandlers._spectatorMode = false; SocketHandlers._spectatingGameId = null; if (typeof Game !== 'undefined') { Game._isSpectating = false; } // Hide spectator controls SocketHandlers._hideSpectatorControls(); // Show end message const reason = data?.reason || 'unknown'; const gameOverData = data?.gameOverData; let message = '👁️ Spectating ended'; if (reason === 'game_over' && gameOverData) { message = '👁️ Game ended: ' + (gameOverData.status === 'won' ? '🏆 Player escaped!' : '💀 Player caught!'); } else if (reason === 'user_left') { message = '👁️ Left spectator mode'; } $('#messages').append($('
  • ').text(message)); UI.scrollChat(); // Return to welcome screen if (typeof ScreenManager !== 'undefined' && ScreenManager.drawWelcomeScreen) { ScreenManager.drawWelcomeScreen(); } // Show games panel again SocketHandlers._showGamesPanel(); SocketHandlers._setBannerStatus('Ready', '#0f0'); }, _updateGamesListPanel: function(data) { let $panel = $('#gamesListPanel'); // Create panel if it doesn't exist if (!$panel.length) { $panel = $(`
    👁️ Live Games
    `); $('body').append($panel); // Close button handler $panel.on('click', '#gamesListClose', function() { $panel.hide(); }); // Spectate button handler (delegated) $panel.on('click', '.spectate-btn', function() { const gameId = $(this).data('gameid'); if (gameId && window.socket) { socket.emit('spectate_game', { gameId: gameId }); } }); } // Render pending games section const $pendingContent = $('#pendingGamesContent'); $pendingContent.empty(); if (data.pendingGames && data.pendingGames.length > 0) { $pendingContent.append(`
    ⏳ Pending (${data.pendingGames.length})
    `); data.pendingGames.forEach(function(pending) { const waitTime = Math.floor((Date.now() - pending.queuedAt) / 1000); const status = pending.waitingForConfirmation ? '⏳ Confirming' : '✅ Ready'; $pendingContent.append(`
    Player ${pending.playerId} ${waitTime}s ago
    ${status} - waiting for next block
    `); }); } // Render active games section const $content = $('#gamesListContent'); $content.empty(); if (!data.games || data.games.length === 0) { if (!data.pendingGames || data.pendingGames.length === 0) { $content.html('
    No active games
    '); } return; } $content.append(`
    🎮 Live (${data.games.length})
    `); // Render each game data.games.forEach(function(game) { const duration = game.durationSeconds || 0; const mins = Math.floor(duration / 60); const secs = duration % 60; const timeStr = mins > 0 ? mins + 'm ' + secs + 's' : secs + 's'; const $gameItem = $(`
    Player ${game.playerId} ${timeStr}
    ${game.moveCount} moves | ${game.hasTreasure ? '💎' : '⬜'} | 👁️ ${game.spectatorCount}
    `); $content.append($gameItem); }); // Pagination info const pag = data.pagination; if (pag && pag.totalGames > pag.pageSize) { $('#gamesListPagination').html( 'Page ' + pag.page + '/' + pag.totalPages + ' (' + pag.totalGames + ' games)' ); } else { $('#gamesListPagination').html(pag ? pag.totalGames + ' game(s)' : ''); } $panel.show(); }, _showGamesPanel: function() { $('#gamesListPanel').show(); }, _hideGamesPanel: function() { $('#gamesListPanel').hide(); }, _showSpectatorControls: function(playerId) { let $controls = $('#spectatorControls'); if (!$controls.length) { $controls = $(`
    👁️ Spectating: ---
    `); $('body').append($controls); $controls.on('click', '#leaveSpectate', function() { if (window.socket) { socket.emit('leave_spectate'); } }); } $('#spectatingPlayer').text(playerId); $controls.show(); }, _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 = $(` `); // Add CSS animation if (!$('#earlyEntryStyles').length) { $('head').append(` `); } // 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($('
  • ').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($('
  • ').text('Early entry failed: ' + message)); UI.scrollChat(); } }; if (typeof module !== 'undefined' && module.exports) { module.exports = SocketHandlers; }