/** * Socket event handlers for the Wowngeon 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); }, init: function() { if (this._initialized) { return; } if (!window.socket) { console.error("Socket not available!"); return; } this.registerEventHandlers(); this._initialized = true; // If the low-level socket connected before handlers were registered, emulate onConnect. if (window.socket && window.socket.connected) { this.onConnect(); } }, 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); // 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('payment_created', this.onPaymentCreated); socket.on('payment_confirmed', this.onPaymentConfirmed); socket.on('payment_detected', this.onPaymentDetected); socket.on('credits_update', this.onCreditsUpdate); // Block height handler socket.on('blockheight', this.onBlockHeight); }, 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('wowngeon_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('wowngeon_token', data.token); } catch(e) {} $('#messages').append($('
  • ').text('New session established. Token stored.')); UI.scrollChat(); } }, onSessionResumed: function(data) { if (data && data.token) { try { localStorage.setItem('wowngeon_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(); } }, onCreditsUpdate: function(data) { if (!data) return; if (typeof data.balance === 'number') { SocketHandlers._updateCreditsDisplay(data.balance); } }, _updateCreditsDisplay: function(balance) { 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); } 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(); }, onWaitingStatus: function(data) { if (data.status === 'waiting') { $('#messages').append($('
  • ').text(data.message)); if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } } UI.scrollChat(); }, onQueueJoined: function(data) { console.log('Queue joined:', data); 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) { $('#messages').append($('
  • ').text("Starting game...")); 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'); if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) { try { AudioAlerts.playFile('game_start'); } catch(_) {} } }, 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)); 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) { $('#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); $('#messages').append($('
  • ').text(data.message)); UI.scrollChat(); }, onPaymentCreated: function(data) { console.log('Payment created:', data); if (typeof AudioAlerts !== 'undefined') { AudioAlerts.playRequestCoin(); } const parts = []; parts.push('💳 Payment Required'); parts.push('Amount: ' + data.humanAmount + ' ' + 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(); // 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 = '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) { qrHolder.innerHTML = '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'; } } }, _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(); 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'); if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) { try { AudioAlerts.playFile('payment_confirmed'); } catch(_) {} } }, _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(); 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'); // Fallback audio trigger if AudioAlerts patched earlier failed to wrap or user enabled after patch if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) { try { AudioAlerts.playFile('payment_detected'); } catch(_) {} } }, // 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); } // Fallback: if status banner still says Connecting..., upgrade it const current = $('#statusValue').text(); if (current === 'Connecting...') { SocketHandlers._setBannerStatus('Ready', '#0f0'); } } }; if (typeof module !== 'undefined' && module.exports) { module.exports = SocketHandlers; }