+ `);
+
// 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);