fix monster bug and let spectators join pending games

This commit is contained in:
jwinterm
2025-12-22 11:25:22 -05:00
parent 24f46fa35e
commit 7f91d83308
8 changed files with 207 additions and 46 deletions
+34 -5
View File
@@ -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') ?
+58 -1
View File
@@ -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 = {
<strong>👁️ Live Games</strong>
<button id="gamesListClose" style="background: none; border: 1px solid #0f0; color: #0f0; cursor: pointer; padding: 2px 6px;">×</button>
</div>
<div id="pendingGamesContent"></div>
<div id="gamesListContent"></div>
<div id="gamesListPagination" style="border-top: 1px solid #0f0; padding-top: 5px; margin-top: 10px; text-align: center;"></div>
</div>
@@ -803,14 +815,59 @@ const SocketHandlers = {
});
}
// Render pending games section
const $pendingContent = $('#pendingGamesContent');
$pendingContent.empty();
if (data.pendingGames && data.pendingGames.length > 0) {
$pendingContent.append(`
<div style="color: #fa0; font-size: 11px; margin-bottom: 5px; border-bottom: 1px solid #550;">
⏳ Pending (${data.pendingGames.length})
</div>
`);
data.pendingGames.forEach(function(pending) {
const waitTime = Math.floor((Date.now() - pending.queuedAt) / 1000);
const status = pending.waitingForConfirmation ? '⏳ Confirming' : '✅ Ready';
$pendingContent.append(`
<div style="
padding: 6px;
margin-bottom: 5px;
background: rgba(60, 40, 0, 0.5);
border: 1px solid #550;
border-radius: 3px;
font-size: 11px;
">
<div style="display: flex; justify-content: space-between;">
<span style="color: #fa0;">Player ${pending.playerId}</span>
<span style="color: #888;">${waitTime}s ago</span>
</div>
<div style="color: #888; margin-top: 2px;">
${status} - waiting for next block
</div>
</div>
`);
});
}
// Render active games section
const $content = $('#gamesListContent');
$content.empty();
if (!data.games || data.games.length === 0) {
$content.html('<div style="color: #888; text-align: center; padding: 20px;">No active games</div>');
if (!data.pendingGames || data.pendingGames.length === 0) {
$content.html('<div style="color: #888; text-align: center; padding: 20px;">No active games</div>');
}
return;
}
$content.append(`
<div style="color: #0f0; font-size: 11px; margin-bottom: 5px; margin-top: 10px; border-bottom: 1px solid #050;">
🎮 Live (${data.games.length})
</div>
`);
// Render each game
data.games.forEach(function(game) {
const duration = game.durationSeconds || 0;
+7 -2
View File
@@ -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
+18 -2
View File
@@ -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';
+8 -7
View File
@@ -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})`);
+14
View File
@@ -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;
+17 -9
View File
@@ -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);
+51 -20
View File
@@ -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);