diff --git a/html/js/display/screenManager.js b/html/js/display/screenManager.js index d3c4e3a..b5dea79 100644 --- a/html/js/display/screenManager.js +++ b/html/js/display/screenManager.js @@ -5,13 +5,32 @@ var ScreenManager = { _blockTimer: null, _updateInterval: null, // Track the update interval separately _waitingForBlock: false, + _isShowingWaitingScreen: false, // Track if waiting screen is active + _cryptoType: 'WOW', // Default to WOW, updated on game mode info + + // Get the game title based on crypto type + getGameTitle: function() { + return this._cryptoType === 'XMR' ? 'MONEROGUE' : 'WOWNEROGUE'; + }, + + // Set crypto type (called from socket handlers) + setCryptoType: function(cryptoType) { + this._cryptoType = cryptoType || 'WOW'; + }, + + // Check if waiting screen is currently being shown + isShowingWaitingScreen: function() { + return this._isShowingWaitingScreen; + }, init: function(screenWidth, screenHeight) { this._screenWidth = screenWidth; this._screenHeight = screenHeight; this._currentBlockHeight = 0; this._waitingForBlock = false; + this._isShowingWaitingScreen = false; this._updateInterval = null; // Initialize update interval tracker + this._cryptoType = 'WOW'; // Initialize the waiting screen animator if (typeof WaitingScreenAnimator !== 'undefined') { @@ -60,15 +79,19 @@ var ScreenManager = { drawWelcomeScreen: function() { if (!DisplayManager.ensureDisplay()) return; + + // Mark that we're NOT showing the waiting screen + this._isShowingWaitingScreen = false; + DisplayManager.clearDisplay(); // Draw the border this.drawBorder(); - // Draw content + // Draw content - use dynamic title based on crypto type let y = 3; - this.drawCenteredText(y, "WOWGUE"); - y += 4; + this.drawCenteredText(y, this.getGameTitle()); + y += 3; // Draw instructions this.drawCenteredText(y, "Type enter to queue!"); @@ -102,9 +125,11 @@ var ScreenManager = { y++; } - // Draw current block info + // Draw current block info on two lines y += 2; - this.drawCenteredText(y, `Current Block: ${this._currentBlockHeight}`); + this.drawCenteredText(y, "Current Block:"); + y += 1; + this.drawCenteredText(y, `${this._currentBlockHeight}`); // Control HTML button visibility const startButton = document.getElementById('startButton'); @@ -228,6 +253,10 @@ var ScreenManager = { drawWaitingScreen: function(_internalLoopCall = false) { if (!DisplayManager.ensureDisplay()) return; + + // Mark that we're showing the waiting screen + this._isShowingWaitingScreen = true; + DisplayManager.clearDisplay(); const animationEnabled = (typeof WaitingScreenAnimator !== 'undefined') ? diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 20a24b9..fdf82d3 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -442,6 +442,11 @@ const SocketHandlers = { 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); @@ -653,6 +658,12 @@ const SocketHandlers = { // 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(); @@ -783,6 +794,7 @@ const SocketHandlers = { 👁️ Live Games +
@@ -803,14 +815,59 @@ const SocketHandlers = { }); } + // 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) { - $content.html('
No active games
'); + 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; diff --git a/html/js/ui/ui.js b/html/js/ui/ui.js index 90250c6..8309e39 100644 --- a/html/js/ui/ui.js +++ b/html/js/ui/ui.js @@ -52,8 +52,8 @@ const UI = { updateGameTitle: function(cryptoType) { if (cryptoType === 'XMR') { - document.title = "XMROGUE - A Monero Roguelike"; - this._gameTitleFull = "XMROGUE - A Monero Roguelike"; + document.title = "Monerogue - A Monero Roguelike"; + this._gameTitleFull = "MONEROGUE - A Monero Roguelike"; } else { document.title = "Wownerogue - A Wownero Roguelike"; this._gameTitleFull = "WOWNEROGUE - A Wownero Roguelike"; @@ -63,6 +63,11 @@ const UI = { if ($('#staticbanner').is(':visible') && !$('#banner').is(':visible')) { $('#staticbanner').text(this._gameTitleFull); } + + // Also update ScreenManager for welcome screen display + if (typeof ScreenManager !== 'undefined' && ScreenManager.setCryptoType) { + ScreenManager.setCryptoType(cryptoType); + } }, // Initialize browser detection diff --git a/src/game/game.js b/src/game/game.js index e41cca9..5c51b48 100644 --- a/src/game/game.js +++ b/src/game/game.js @@ -168,8 +168,8 @@ class Game { this.dungeon.map[newY][newX] !== undefined && (this.dungeon.map[newY][newX] === primaryFloor || this.dungeon.map[newY][newX] === secondaryFloor)) { - this.player.moveTo(newX, newY); - this.moveCount += 1; + this.player.moveTo(newX, newY); + this.moveCount += 1; if (CONSOLE_LOGGING) { console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`); } @@ -177,6 +177,22 @@ class Game { // Update FOV after moving this.updateFOV(); + // Check if player walked into monster (death by collision) + if (this.monster && this.monster.x === newX && this.monster.y === newY) { + this.gameState = 'lost'; + if (CONSOLE_LOGGING) { + console.log(`💀 Player walked into monster at (${newX},${newY}) - GAME OVER`); + } + return { + status: 'moved', + event: 'monster_caught', + player: this.player.getState(), + monster: this.monster.getState(), + visibleTiles: this.visibleTiles, + moves: this.moveCount + }; + } + // Check for game events like finding treasure or exit if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], this.dungeon.exit[1])) { this.gameState = 'won'; diff --git a/src/game/gameManager.js b/src/game/gameManager.js index 84b0bd8..000486d 100644 --- a/src/game/gameManager.js +++ b/src/game/gameManager.js @@ -112,7 +112,14 @@ class GameManager { proof: proofReveal }); - // Notify spectators that the game has ended + // Persist completion details if DB available + await this._updateGameRecord(game, socketId, status, reason, moves, durationSeconds); + + // Clean up game from active games BEFORE notifying spectators + // This ensures the game list broadcast won't include this game + this.activeGames.delete(socketId); + + // Notify spectators that the game has ended and broadcast updated list if (this.spectatorManager) { this.spectatorManager.notifyGameEnded(game.id, { status, @@ -124,12 +131,6 @@ class GameManager { treasure: game.player.hasTreasure || false }); } - - // Persist completion details if DB available - await this._updateGameRecord(game, socketId, status, reason, moves, durationSeconds); - - // Clean up game from active games - this.activeGames.delete(socketId); if (this.debugManager.CONSOLE_LOGGING) { console.log(`🎮 Game ${game.id} ended for ${socketId}: ${status} (${reason})`); diff --git a/src/network/queueManager.js b/src/network/queueManager.js index cff3562..e2c2bd9 100644 --- a/src/network/queueManager.js +++ b/src/network/queueManager.js @@ -154,6 +154,20 @@ class QueueManager { return false; } } + + /** + * Get list of pending games (players waiting in queue) + * Used by spectator system to show upcoming games + * @returns {Array} List of pending game entries + */ + getPendingGamesList() { + return this._waitingPlayers.map(entry => ({ + playerId: entry.serverId.substring(0, 6), // Anonymized + queuedAt: entry.entryTime, + isConfirmed: entry.confirmed, + waitingForConfirmation: entry.requiresConfirmation && !entry.confirmed + })); + } } module.exports = QueueManager; diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index 0919e48..c7343d7 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -155,7 +155,8 @@ class SocketHandlers { io: this.io, activeGames: this.activeGames, broadcastManager: this.broadcastManager, - debugManager: this.debugManager + debugManager: this.debugManager, + queueManager: this.queueManager // For pending games list }); this.spectatorManager.initialize(); @@ -295,6 +296,20 @@ class SocketHandlers { * Handles monster chasing logic and immediate game over if monster catches player. */ afterPlayerMove(socketId, game, moveResult) { + // Check if player walked into monster (death by walking into monster) + if (moveResult && moveResult.event === 'monster_caught') { + const fakeSocket = { id: socketId }; + this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You walked into the monster!', 0); + return; + } + + // If player escaped, handle via game manager + if (moveResult && moveResult.event === 'escaped') { + const fakeSocket = { id: socketId }; + this.handleGameOver(fakeSocket, game, 'won', 'escaped', 'You escaped the dungeon!', 0); + return; + } + let monsterResult = null; // Move monster one step toward player each player action @@ -306,20 +321,13 @@ class SocketHandlers { } } - // If the monster caught the player, end the game immediately + // If the monster caught the player after moving, end the game immediately if (monsterResult && monsterResult.event === 'monster_caught') { const fakeSocket = { id: socketId }; this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You were caught by the monster!', 0); return; } - // If player escaped or triggered another event, handle via game manager - if (moveResult && moveResult.event === 'escaped') { - const fakeSocket = { id: socketId }; - this.handleGameOver(fakeSocket, game, 'won', 'escaped', 'You escaped the dungeon!', 0); - return; - } - // Re-emit state if treasure collected without auto game_update (fallback) if (moveResult && moveResult.event === 'treasure_found') { this.movementManager.emitGameUpdate(socketId); diff --git a/src/network/spectatorManager.js b/src/network/spectatorManager.js index c05ce17..32f2d73 100644 --- a/src/network/spectatorManager.js +++ b/src/network/spectatorManager.js @@ -10,11 +10,12 @@ */ class SpectatorManager { - constructor({ io, activeGames, broadcastManager, debugManager }) { + constructor({ io, activeGames, broadcastManager, debugManager, queueManager = null }) { this.io = io; this.activeGames = activeGames; this.broadcastManager = broadcastManager; this.debugManager = debugManager; + this.queueManager = queueManager; // For pending games list // Map of gameId -> Set of spectator socket IDs this._spectatorsByGame = new Map(); @@ -22,6 +23,10 @@ class SpectatorManager { // Map of socketId -> gameId (for quick lookup of what a spectator is watching) this._spectatorWatching = new Map(); + // Pending game subscriptions: Map of playerId -> Set of spectator socket IDs + // Spectators waiting for a pending game to start + this._pendingSubscriptions = new Map(); + // Cached game list for efficient broadcasting this._cachedGameList = []; this._gameListLastUpdate = 0; @@ -32,6 +37,13 @@ class SpectatorManager { this._broadcastIntervalMs = 3000; // Broadcast every 3 seconds } + /** + * Set queue manager (for late binding) + */ + setQueueManager(queueManager) { + this.queueManager = queueManager; + } + /** * Initialize the manager - start periodic broadcasts */ @@ -49,7 +61,7 @@ class SpectatorManager { /** * Get list of active games with spectator-safe info * @param {Object} options - Filtering/pagination options - * @returns {Array} List of active games with metadata + * @returns {Object} Object containing active games and pending games */ getActiveGamesList(options = {}) { const { page = 1, pageSize = 20, sortBy = 'newest' } = options; @@ -60,7 +72,7 @@ class SpectatorManager { return this._applyPagination(this._cachedGameList, page, pageSize); } - // Build fresh list + // Build fresh list of active games const games = []; for (const [socketId, game] of this.activeGames.entries()) { if (game && game.gameState === 'active') { @@ -78,6 +90,17 @@ class SpectatorManager { return this._applyPagination(games, page, pageSize); } + /** + * Get list of pending games (players waiting in queue) + * @returns {Array} List of pending games + */ + getPendingGamesList() { + if (!this.queueManager || typeof this.queueManager.getPendingGamesList !== 'function') { + return []; + } + return this.queueManager.getPendingGamesList(); + } + /** * Build a spectator-safe game summary * @private @@ -302,25 +325,27 @@ class SpectatorManager { */ notifyGameEnded(gameId, gameOverData) { const spectators = this._spectatorsByGame.get(gameId); - if (!spectators || spectators.size === 0) return; - // Emit game over to spectators - this.io.to(`spectate:${gameId}`).emit('spectate_ended', { - gameId: gameId, - reason: 'game_over', - gameOverData: gameOverData - }); - - // Remove all spectators from this game's tracking - for (const spectatorId of spectators) { - this._spectatorWatching.delete(spectatorId); - const sock = this._getSocket(spectatorId); - if (sock) sock.leave(`spectate:${gameId}`); + // Emit game over to spectators if any + if (spectators && spectators.size > 0) { + this.io.to(`spectate:${gameId}`).emit('spectate_ended', { + gameId: gameId, + reason: 'game_over', + gameOverData: gameOverData + }); + + // Remove all spectators from this game's tracking + for (const spectatorId of spectators) { + this._spectatorWatching.delete(spectatorId); + const sock = this._getSocket(spectatorId); + if (sock) sock.leave(`spectate:${gameId}`); + } + this._spectatorsByGame.delete(gameId); } - this._spectatorsByGame.delete(gameId); - // Invalidate cache + // Invalidate cache and immediately broadcast updated game list this._gameListLastUpdate = 0; + this._broadcastGameListToLobby(); } /** @@ -365,8 +390,14 @@ class SpectatorManager { // Get fresh game list const gameListData = this.getActiveGamesList({ page: 1, pageSize: 50 }); - // Only broadcast if there are active games - if (gameListData.pagination.totalGames === 0) return; + // Get pending games + const pendingGames = this.getPendingGamesList(); + + // Add pending games to the response + gameListData.pendingGames = pendingGames; + + // Only broadcast if there are active or pending games + if (gameListData.pagination.totalGames === 0 && pendingGames.length === 0) return; // Broadcast to a "lobby" room - users join this room when not in game this.io.to('lobby').emit('active_games', gameListData);