From 54e58ee6982503f655935db4d46d571881b89695 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 26 May 2025 19:23:07 -0400 Subject: [PATCH] torch density fixed and some refactoring --- index.js | 422 ---------------------------------- src/backend/dungeon.js | 90 ++++++-- src/backend/game.js | 253 +++++++++----------- src/backend/index.js | 47 +++- src/backend/lightingAndFov.js | 127 ++++++++++ src/backend/user.js | 169 ++++++++------ 6 files changed, 440 insertions(+), 668 deletions(-) delete mode 100644 index.js create mode 100644 src/backend/lightingAndFov.js diff --git a/index.js b/index.js deleted file mode 100644 index 7d823a4..0000000 --- a/index.js +++ /dev/null @@ -1,422 +0,0 @@ -const express = require('express'); -var app = require('express')(); -var http = require('http').Server(app); -var io = require('socket.io')(http); -var rpc = require('./src/backend/rpccalls.js'); -var db = require('./src/backend/dbcalls.js'); -var user = require('./src/backend/user'); - -// Function to get a user by socket ID -// Enhance the getUserBySocket function to try both IDs - -function getUserBySocket(socketId) { - console.log(`Looking up user with socketId: ${socketId}`); - - // Try direct lookup first - let foundUser = user.getUserBySocketId(socketId); - - // If not found but we have a socket map, try the mapped ID - if (!foundUser && clientSocketMap.has(socketId)) { - const mappedId = clientSocketMap.get(socketId); - console.log(`Socket ID ${socketId} not found directly, trying mapped ID: ${mappedId}`); - foundUser = user.getUserBySocketId(mappedId); - } - - console.log(`User lookup result for ${socketId}: ${foundUser ? "FOUND" : "NOT FOUND"}`); - return foundUser; -} - -const Game = require('./game'); -const activeGames = new Map(); // Maps socketId to Game objects -let lastBlockHeight = 0; // Track last block height -const MIN_FEE = 10; // Minimum entrance fee in WOW -const MAX_FEE = 100; // Maximum entrance fee in WOW -const WAITING_PLAYERS = []; // Players waiting for the next block -const DEBUG_MODE = true; // Set to false to disable debug mode -let debugBlockHeight = 1; -const clientSocketMap = new Map(); // To track client-to-server socket ID mappings - -app.use(express.static('html')); -app.get('/', function(req, res) { - res.sendfile('index.html'); -}); - -// Replace your existing block check interval with this conditional version -if (DEBUG_MODE) { - console.log("šŸ› DEBUG MODE ENABLED - Simulating blocks every 30 seconds"); - - // Initial debug block - io.emit('blockheight', debugBlockHeight); - - // Debug block height simulator - setInterval(function() { - debugBlockHeight++; - console.log(`šŸ› DEBUG: New simulated block: ${debugBlockHeight}`); - io.emit('blockheight', debugBlockHeight); - - // Start games for waiting players - startGamesForWaiting(debugBlockHeight); - - // Check active games for timeout - checkGamesTimeout(debugBlockHeight); - - }, 30000); // Every 30 seconds -} else { - // Your original block check code - setInterval(function() { - rpc.daemonCall("get_block_count", "", function(result) { - const currentHeight = result.result.count; - rpc.lastBlock.setHeight(currentHeight); - - // If new block found - if (currentHeight > lastBlockHeight) { - console.log(`New block found: ${currentHeight}`); - io.emit('blockheight', currentHeight); - - // Start games for waiting players - startGamesForWaiting(currentHeight); - - // Check active games for timeout - checkGamesTimeout(currentHeight); - - lastBlockHeight = currentHeight; - } else { - io.emit('blockheight', currentHeight); - } - }); - }, 5000); -} - -// Start games for waiting players -function startGamesForWaiting(blockHeight) { - console.log(`Starting games for ${WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`); - - while (WAITING_PLAYERS.length > 0) { - const playerEntry = WAITING_PLAYERS.shift(); - const serverId = playerEntry.serverId; - - console.log(`Processing player: server=${serverId}`); - - const currentUser = getUserBySocket(serverId); - - if (currentUser) { - currentUser.blockRec = blockHeight; - - try { - const game = currentUser.startGame(80, 40); - activeGames.set(serverId, game); - - const gameState = game.getState(); - gameState.blockHeight = blockHeight; - - console.log(`šŸŽ® SENDING GAME_START to ${serverId}`); - - // Send ONLY ONE message - the game state itself - io.to(serverId).emit('game_start', gameState); - - console.log(`Game started for player ${serverId}`); - } catch (error) { - console.error(`Error creating game:`, error); - io.to(serverId).emit('message', 'Error starting game: ' + error.message); - } - } else { - console.error(`User not found for socket ${serverId}`); - io.to(serverId).emit('message', 'Error: User not found'); - } - } -} - -// Check if any active games have timed out -function checkGamesTimeout(currentHeight) { - activeGames.forEach((game, socketId) => { - const user = getUserBySocket(socketId); - - // Player has 3 blocks to complete the dungeon - // If they started on block 2, they must escape before block 5 is found - if (user && user.blockRec && currentHeight > user.blockRec + 2) { - console.log(`Game timeout for player ${socketId}: started on block ${user.blockRec}, current block ${currentHeight} (allowed until block ${user.blockRec + 2})`); - - // Game has timed out - player didn't escape in time - game.gameState = 'lost'; - io.to(socketId).emit('game_over', { - status: 'lost', - reason: 'timeout', - message: 'You didn\'t escape before the block time limit!' - }); - - activeGames.delete(socketId); - } - }); -} - -// Socket.io handlers -io.on('connection', function(socket) { - console.log('A user connected'); - console.log(socket.client.id); - console.log(socket.handshake.address); - - // Create and register user (automatically stored in registry) - new user.User(socket.id, socket.handshake.address); - // Also store socket.client.id as an attribute - const newUser = getUserBySocket(socket.id); - if (newUser) { - newUser.clientId = socket.client.id; - console.log(`User created with both socket.id (${socket.id}) and socket.client.id (${socket.client.id})`); - } - - // Insert into visitors DB - db.insertVisitor(socket.client.id, socket.handshake.address, 0); - - // Send welcome message - io.to(socket.client.id).emit('welcome', socket.client.id); - - socket.on('chat message', function(msg) { - console.log('Message received:', msg); - - if (msg.toLowerCase() == 'hello') { - io.to(`${socket.client.id}`).emit('message', - 'Welcome, to enter the dungeon type Enter, and you will be given a Wownero address, ' + - 'which you must send between 10-100 WOW to enter. The more you send, the more you can win.'); - } - // Update the 'enter' command handler to send waiting_status - if (msg.toLowerCase() == 'enter') { - console.log(`Player ${socket.id} requested to enter the dungeon`); - - // Use the updated user lookup function - const currentUser = getUserBySocket(socket.id); - - if (currentUser) { - console.log(`Found user for socket ${socket.id}, adding to queue...`); - - // Add to waiting players queue - if (!WAITING_PLAYERS.some(p => p.serverId === socket.id)) { - WAITING_PLAYERS.push({ - serverId: socket.id, - clientId: socket.id, - user: currentUser, - joinedAt: Date.now() - }); - - // Send waiting status data for display - THIS IS THE KEY PART - io.to(socket.id).emit('waiting_status', { - status: 'waiting', - message: 'Waiting for the next block to be found...', - position: WAITING_PLAYERS.length, - currentBlock: debugBlockHeight || lastBlockHeight || 0, - joinTime: Date.now() - }); - - console.log(`Added player to queue. Current queue: ${WAITING_PLAYERS.length} players`); - } else { - io.to(socket.id).emit('message', 'You are already in the queue. Please wait.'); - } - } else { - io.to(socket.id).emit('message', 'Error: Could not add you to the game queue. Please try again.'); - } - } - // Add a handler for the 'cancel' command - else if (msg.toLowerCase() == 'cancel') { - // Remove player from waiting queue - const index = WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); - - if (index !== -1) { - WAITING_PLAYERS.splice(index, 1); - io.to(socket.id).emit('message', 'You have left the queue.'); - - // Reset to welcome screen - io.to(socket.id).emit('queue_cancelled'); - } else { - io.to(socket.id).emit('message', 'You were not in the queue.'); - } - } - else { - io.emit('message', msg); - } - }); - - // Handle player movement - socket.on('player_move', function(moveData) { // Changed from 'move' to 'player_move' and added moveData - console.log(`Player move event received from ${socket.id}:`, moveData); - const currentUser = getUserBySocket(socket.id); - - if (currentUser && currentUser.game && currentUser.game.gameState === 'active') { - if (typeof moveData.dx === 'number' && typeof moveData.dy === 'number') { - const game = currentUser.game; - const moveResult = game.movePlayer(moveData.dx, moveData.dy); // This should update player pos and FOV - - if (moveResult && moveResult.status === 'moved') { - // Move monster after player moves - game.moveMonster(); - - // Check if monster caught player - const GameModule = require('./game'); - const checkMonsterKill = GameModule.checkMonsterKill || - ((player, monster) => monster.x === player.x && monster.y === player.y); - - if (checkMonsterKill(game.player, game.monster)) { - game.gameState = 'lost'; - io.to(socket.id).emit('game_over', { - status: 'lost', - reason: 'monster', - message: 'The monster caught you!' - }); - activeGames.delete(socket.id); - return; - } - - // Check for treasure pickup - if (moveResult.event === 'treasure_found') { - io.to(socket.id).emit('message', 'You found the treasure!'); - } - - // Check for escape - if (moveResult.event === 'escaped') { - game.gameState = 'won'; - io.to(socket.id).emit('game_over', { - status: 'won', - reason: 'escaped', - message: 'Congratulations! You escaped the dungeon!' - }); - activeGames.delete(socket.id); - return; - } - - const updatedGameState = game.getState(); // Get the new state - - // Log before sending to client for debugging - console.log(`Sending game_update to ${socket.id} after player move. Player:`, updatedGameState.player, "Visible tiles keys:", Object.keys(updatedGameState.visibleTiles || {})); - - io.to(socket.id).emit('game_update', updatedGameState); - } else { - console.log(`Player move from ${socket.id} was invalid or resulted in no change.`); - // Optionally, send an update even for invalid moves if you want to provide feedback - // const currentGameState = game.getState(); - // io.to(socket.id).emit('game_update', currentGameState); - } - } else { - console.error(`Invalid moveData received from ${socket.id}:`, moveData); - } - } else { - console.log(`Player move event from ${socket.id} ignored: No active game found for user.`); - } - }); - - // Handle disconnection - socket.on('disconnect', function() { - console.log('User disconnected', socket.client.id); - user.removeUser(socket.client.id); - }); - - // Add this to your socket handlers in index.js - socket.on('debug_ping', function(data) { - console.log(`Debug ping received from ${socket.client.id}`); - socket.emit('debug_pong', { - message: "Hello from server!", - clientTime: data.time, - serverTime: Date.now(), - socketId: socket.client.id - }); - }); - - socket.on('register_client', function(data) { - console.log(`Client registered: ${socket.id} (server) <-> ${data.clientId} (client)`); - - // Store both mappings for lookup - clientSocketMap.set(data.clientId, socket.id); - clientSocketMap.set(socket.id, data.clientId); - - // Send confirmation back to client - socket.emit('socket_registered', { - clientId: data.clientId, - serverId: socket.id, - success: true - }); -}); - -}); // End of io.on('connection') handler - -// Add this debug function to check all registered users - -function debugRegisteredUsers() { - console.log("\nšŸ” CHECKING ALL REGISTERED USERS:"); - const allUsers = user.getAllUsers ? user.getAllUsers() : null; - - if (!allUsers) { - console.log("No getAllUsers function available or no users registered"); - return; - } - - console.log(`Found ${allUsers.length} registered users:`); - allUsers.forEach((u, i) => { - console.log(`User ${i+1}:`, { - id: u.id, - clientId: u.clientId || 'undefined', - address: u.address - }); - }); - console.log(); // Empty line for readability -} - -// Call this after a user registers and at intervals -setInterval(debugRegisteredUsers, 10000); - -http.listen(3000, function() { - console.log('listening on *:3000'); -}); - -// Make sure this function is defined in your file -function startGameForPlayer(socketId, blockHeight) { - console.log(`Starting game for player ${socketId} at block height ${blockHeight}`); - - const currentUser = getUserBySocket(socketId); - if (!currentUser) { - console.error(`User with socket ID ${socketId} not found`); - return; - } - - try { - // Create a new game - const game = currentUser.startGame(25, 19); - if (!game) { - console.error("Game creation failed"); - return; - } - - // Store the game in activeGames map - activeGames.set(socketId, game); - - // Get game state - ensure it has the required fields - const gameState = { - gameState: "active", // Add this so frontend knows it's active - player: { - x: game.player.x, - y: game.player.y, - hasKey: !!game.player.hasKey, - hasTreasure: !!game.player.hasTreasure - }, - monster: game.monster, - visibleTiles: game.calculateFOV ? game.calculateFOV() : {}, - entrance: [game.dungeon.entrance.x, game.dungeon.entrance.y], - exit: [game.dungeon.exit.x, game.dungeon.exit.y], - treasure: [game.dungeon.treasure.x, game.dungeon.treasure.y] - }; - - console.log("Sending game_start with state:", JSON.stringify(gameState, null, 2).substring(0, 200) + "..."); - - // Add a small delay before sending (sometimes helps with socket timing issues) - setTimeout(() => { - debugSocket(socketId, 'game_start', gameState); - // Send initial game state to client - io.to(socketId).emit('game_start', gameState); - console.log(`Game started for player ${socketId}`); - }, 100); - } catch (err) { - console.error("Error starting game:", err); - io.to(socketId).emit('message', 'Error starting game: ' + err.message); - } -} - -function debugSocket(socketId, eventName, data) { - console.log(`šŸ”Œ SOCKET DEBUG: Sending ${eventName} to ${socketId.substring(0, 8)}...`); - console.log(`šŸ“¦ PAYLOAD (first 300 chars): ${JSON.stringify(data).substring(0, 300)}...`); -} diff --git a/src/backend/dungeon.js b/src/backend/dungeon.js index 5e1ef24..b48a9e0 100644 --- a/src/backend/dungeon.js +++ b/src/backend/dungeon.js @@ -1,28 +1,64 @@ const ROT = require('./rot.js'); +// ======================================== +// DUNGEON CONFIGURATION SECTION +// ======================================== +// Edit these values to customize dungeon generation +const DUNGEON_CONFIGS = { + // Default dungeon dimensions + DEFAULT_WIDTH: 80, + DEFAULT_HEIGHT: 40, + + // Legacy smaller dungeon size (for compatibility) + LEGACY_WIDTH: 25, + LEGACY_HEIGHT: 19, + + // Dungeon generation settings + ROOM_WIDTH_RANGE: [3, 9], + ROOM_HEIGHT_RANGE: [3, 7], + CORRIDOR_LENGTH_RANGE: [2, 6], + DUG_PERCENTAGE: 0.2, + + // Lighting and appearance + TORCH_DENSITY: 0.2, // 20% chance for torches on wall tiles adjacent to floors + TORCH_ENABLED: true, // Set to false to disable torches completely + FLOOR_VARIATION: 0.01, // Chance for secondary floor tiles (visual variety) + + // Tile symbols + PRIMARY_FLOOR: "'1", + SECONDARY_FLOOR: "'2", + WALL_TILE: "#", + TORCH_TILE: "torch" +}; + /** * Dungeon generation utilities */ class DungeonGenerator { + // Get the configuration object (allows external access) + static getConfig() { + return { ...DUNGEON_CONFIGS }; + } + static generate(width, height, options = {}) { - // Default options for dungeon generation const defaultOptions = { - floorVariation: 0.01, // 1% secondary floor tiles - torchEnabled: true, // Enable torch placement - torchDensity: 0.05, // Default: 5% of wall tiles get torches (comment updated) - primaryFloor: "'1", // Primary floor tile - secondaryFloor: "'2", // Secondary floor tile - torchTile: "torch" // Torch tile type + floorVariation: DUNGEON_CONFIGS.FLOOR_VARIATION, + torchEnabled: DUNGEON_CONFIGS.TORCH_ENABLED, + torchDensity: DUNGEON_CONFIGS.TORCH_DENSITY, + primaryFloor: DUNGEON_CONFIGS.PRIMARY_FLOOR, + secondaryFloor: DUNGEON_CONFIGS.SECONDARY_FLOOR, + torchTile: DUNGEON_CONFIGS.TORCH_TILE }; const config = { ...defaultOptions, ...options }; - + console.log(`[DungeonGenerator] Generating dungeon with effective torchDensity: ${config.torchDensity}`); // Log effective torchDensity + // Create dungeon using ROT.js Map.Digger const digger = new ROT.Map.Digger(width, height, { - roomWidth: [3, 9], - roomHeight: [3, 7], - corridorLength: [2, 6], - dugPercentage: 0.2 + roomWidth: DUNGEON_CONFIGS.ROOM_WIDTH_RANGE, + roomHeight: DUNGEON_CONFIGS.ROOM_HEIGHT_RANGE, + corridorLength: DUNGEON_CONFIGS.CORRIDOR_LENGTH_RANGE, + dugPercentage: DUNGEON_CONFIGS.DUG_PERCENTAGE }); // Initialize the empty map with walls (1) @@ -84,11 +120,23 @@ class DungeonGenerator { // Enhanced map creation with floor variations and torch placement static enhanceMapWithVariations(basicMap, config) { - const height = basicMap.length; - const width = basicMap[0].length; - const enhancedMap = Array(height).fill().map(() => Array(width).fill(null)); + const enhancedMap = basicMap.map(row => row.slice()); // Create a deep copy + const { primaryFloor, secondaryFloor, floorVariation, torchEnabled, torchDensity, torchTile } = config; + + if (typeof primaryFloor === 'undefined' || typeof secondaryFloor === 'undefined' || typeof floorVariation === 'undefined' || typeof torchEnabled === 'undefined' || typeof torchDensity === 'undefined' || typeof torchTile === 'undefined') { + console.error("[DungeonGenerator.enhanceMapWithVariations] Critical config missing:", config); + // Fallback or throw error if essential configs are still missing despite defaults + // This indicates a problem upstream if defaults didn't propagate or were explicitly undefined. + } + let placedTorchesCount = 0; // Counter for torches placed in this function + // Log the torchDensity being used for this map generation pass + console.log(`[DungeonGenerator.enhanceMapWithVariations] Starting enhancement with torchDensity: ${config.torchDensity}`); + + const height = basicMap.length; // Use basicMap's dimensions + const width = basicMap[0].length; // Use basicMap's dimensions + for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const cell = basicMap[y][x]; @@ -130,12 +178,22 @@ class DungeonGenerator { return nx >= 0 && nx < width && ny >= 0 && ny < height && map[ny][nx] === 0; }); - return adjacentToFloor && Math.random() < torchDensity; + const randomValue = Math.random(); + const shouldPlace = adjacentToFloor && randomValue < torchDensity; + + return shouldPlace; } // Get positions of all torches for client-side rendering static getTorchPositions(enhancedMap, config) { const torches = []; + const { torchTile } = config; // Ensure config is used here + + if (typeof torchTile === 'undefined') { + console.error("[DungeonGenerator.getTorchPositions] torchTile config missing:", config); + return torches; // Or handle error appropriately + } + const height = enhancedMap.length; const width = enhancedMap[0].length; console.log(`[DungeonGenerator.getTorchPositions] Searching for torch tile '${config.torchTile}' in a ${width}x${height} map.`); diff --git a/src/backend/game.js b/src/backend/game.js index ad8f5e2..5d24cf0 100644 --- a/src/backend/game.js +++ b/src/backend/game.js @@ -1,29 +1,96 @@ +const { v4: uuidv4 } = require('uuid'); const ROT = require('./rot.js'); const Player = require('./player.js'); const Monster = require('./monster.js'); const DungeonGenerator = require('./dungeon.js'); +const LightingAndFov = require('./lightingAndFov.js'); class Game { - constructor(socketId, mapWidth, mapHeight, gameConfig = {}) { + /** + * Create a new game instance + * @param {string} socketId - Socket ID of the player + * @param {User} user - User object for statistics tracking + * @param {object} gameOptions - Optional game configuration overrides + */ + constructor(socketId, user, gameOptions = {}) { + this.user = user; + this.id = uuidv4(); this.socketId = socketId; - this.width = mapWidth || 25; // Reduced default size to match client display - this.height = mapHeight || 19; // Reduced default size to match client display - this.gameState = 'waiting'; // waiting, active, won, lost - this.startBlock = null; + this.players = {}; + this.monsters = {}; this.dungeon = null; + + // Get default configuration from DungeonGenerator + const dungeonConfig = DungeonGenerator.getConfig(); + + // Determine dimensions - use provided options or defaults + const width = gameOptions.width || dungeonConfig.DEFAULT_WIDTH; + const height = gameOptions.height || dungeonConfig.DEFAULT_HEIGHT; + + // Merge game configuration + this.gameConfig = { + width: width, + height: height, + // Dungeon generation options (can be overridden by gameOptions) + floorVariation: gameOptions.floorVariation !== undefined ? gameOptions.floorVariation : dungeonConfig.FLOOR_VARIATION, + torchEnabled: gameOptions.torchEnabled !== undefined ? gameOptions.torchEnabled : dungeonConfig.TORCH_ENABLED, + torchDensity: gameOptions.torchDensity !== undefined ? gameOptions.torchDensity : dungeonConfig.TORCH_DENSITY, + primaryFloor: gameOptions.primaryFloor || dungeonConfig.PRIMARY_FLOOR, + secondaryFloor: gameOptions.secondaryFloor || dungeonConfig.SECONDARY_FLOOR, + torchTile: gameOptions.torchTile || dungeonConfig.TORCH_TILE, + // Other game options + ...gameOptions + }; + + console.log(`[Game Constructor] Creating game for user ${user.id} (socket: ${socketId})`); + console.log(`[Game Constructor] Dimensions: ${width}x${height}`); + console.log(`[Game Constructor] Game config:`, JSON.stringify(this.gameConfig, null, 2)); + + this.initializeGame(); + } + + /** + * Static factory method to create a game with standard dimensions + */ + static createStandardGame(socketId, user, options = {}) { + return new Game(socketId, user, options); + } + + /** + * Static factory method to create a legacy-sized game + */ + static createLegacyGame(socketId, user, options = {}) { + const dungeonConfig = DungeonGenerator.getConfig(); + const legacyOptions = { + width: dungeonConfig.LEGACY_WIDTH, + height: dungeonConfig.LEGACY_HEIGHT, + ...options + }; + return new Game(socketId, user, legacyOptions); + } + + initializeGame() { + console.log(`[Game.initializeGame] Initializing game with config:`, JSON.stringify(this.gameConfig, null, 2)); + if (typeof this.gameConfig.width === 'undefined' || typeof this.gameConfig.height === 'undefined') { + console.error("[Game.initializeGame] CRITICAL: Width or Height is undefined in gameConfig!", this.gameConfig); + throw new Error(`Invalid game dimensions: width=${this.gameConfig.width}, height=${this.gameConfig.height}`); + } + + // Set game dimensions from config + this.width = this.gameConfig.width; + this.height = this.gameConfig.height; + + // Initialize game state + this.gameState = 'waiting'; + this.startBlock = null; this.player = new Player(); this.monster = new Monster(); - this.fee = 0; // Amount player paid - this.visibleTiles = {}; // Will store visible tiles - this.fov = null; // FOV calculator - this.gameConfig = gameConfig; // Store game configuration - this.generateDungeon(); - } - - generateDungeon() { - // Pass game configuration to dungeon generator - this.dungeon = DungeonGenerator.generate(this.width, this.height, this.gameConfig); + this.fee = 0; + + // Generate dungeon with the provided dimensions and config + this.dungeon = DungeonGenerator.generate(this.gameConfig.width, this.gameConfig.height, this.gameConfig); + // Place player at entrance if (this.dungeon.entrance) { this.player.moveTo(this.dungeon.entrance[0], this.dungeon.entrance[1]); } @@ -34,63 +101,28 @@ class Game { const center = monsterRoom.getCenter(); this.monster.moveTo(center[0], center[1]); } - - // Initialize FOV calculator - need to check for multiple floor types now - this.fov = new ROT.FOV.PreciseShadowcasting( - (x, y) => { - // Return true if the tile is transparent (can see through it) - if (!this.dungeon.map[y] || this.dungeon.map[y][x] === undefined) return false; - const tile = this.dungeon.map[y][x]; - // Check if it's any type of floor tile (primary or secondary) - const primaryFloor = this.gameConfig.primaryFloor || "'1"; - const secondaryFloor = this.gameConfig.secondaryFloor || "'2"; - return tile === primaryFloor || tile === secondaryFloor; - } - ); - - // Calculate initial FOV - this.updateFOV(); + + // Initialize lighting and FOV + this.lightingAndFov = new LightingAndFov(this.dungeon.map, this.dungeon.torches); + this._fovInstance = LightingAndFov.initializeFOV(this.dungeon.map, this.gameConfig); + const fovRadius = this.gameConfig.fovRadius || 10; + this.visibleTiles = LightingAndFov.updateFOV(this._fovInstance, this.player, this.dungeon.map, fovRadius); // Set game state to active this.gameState = 'active'; } updateFOV() { - // Reset visible tiles - this.visibleTiles = {}; - - // Calculate new visible tiles - this.fov.compute(this.player.x, this.player.y, 10, (x, y, r, visibility) => { // Added r and visibility params - if (visibility > 0) { // Only add if tile is actually visible - if (!this.visibleTiles[y]) { - this.visibleTiles[y] = {}; - } - // Store the actual tile type (0 for floor, 1 for wall) from the dungeon map - if (this.dungeon && this.dungeon.map && this.dungeon.map[y] && this.dungeon.map[y][x] !== undefined) { - this.visibleTiles[y][x] = this.dungeon.map[y][x]; - - // Debug log for specific problematic coordinates - if ((x === 36 && (y === 18 || y === 16)) || (x === 35 && (y === 18 || y === 16))) { - console.log(`šŸ” DEBUG FOV: (${x},${y}) - server map value: ${this.dungeon.map[y][x]}, sent to client: ${this.visibleTiles[y][x]}, visibility: ${visibility}`); - } - } else { - // Should not happen if FOV is within map bounds, but as a fallback: - // this.visibleTiles[y][x] = 1; // Default to wall if outside known map - } - } - }); - // console.log("FOV updated. Player:", this.player.x, this.player.y, "Visible tiles count:", Object.keys(this.visibleTiles).reduce((acc, k) => acc + Object.keys(this.visibleTiles[k]).length, 0)); + this.visibleTiles = LightingAndFov.updateFOV(this._fovInstance, this.player, this.dungeon.map, 10); } movePlayer(dx, dy) { const newX = this.player.x + dx; const newY = this.player.y + dy; - // Simple debug output console.log(`Move: (${this.player.x},${this.player.y}) -> (${newX},${newY})`); // Check if the move is valid (not into a wall and within map bounds) - // Updated to handle new floor tile types const primaryFloor = this.gameConfig.primaryFloor || "'1"; const secondaryFloor = this.gameConfig.secondaryFloor || "'2"; @@ -107,14 +139,12 @@ class Game { // Check for game events like finding treasure or exit if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], this.dungeon.exit[1])) { - // Handle win condition (e.g., escaped) this.gameState = 'won'; return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles }; } if (this.dungeon.treasure && this.player.isAt(this.dungeon.treasure[0], this.dungeon.treasure[1]) && !this.player.hasTreasure) { this.player.hasTreasure = true; console.log(`šŸ† TREASURE PICKUP: Player ${this.socketId} collected treasure at (${this.dungeon.treasure[0]}, ${this.dungeon.treasure[1]})`); - // Remove treasure from dungeon so it won't be sent in future updates this.dungeon.treasure = null; console.log(`šŸ—‘ļø TREASURE REMOVED: dungeon.treasure is now null for player ${this.socketId}`); console.log(`Player ${this.socketId} collected treasure! hasTreasure: ${this.player.hasTreasure}`); @@ -124,8 +154,6 @@ class Game { return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles }; } - - console.log(`Move BLOCKED: ${newX},${newY} - cell value: ${this.dungeon && this.dungeon.map[newY] ? this.dungeon.map[newY][newX] : 'undefined'}`); return { status: 'invalid' }; } @@ -134,7 +162,6 @@ class Game { this.monster.moveTowardPlayer(this.player, this.dungeon); } - // Update the getState() method to include all entities with relative positions getState() { const state = { gameState: this.gameState, @@ -145,21 +172,8 @@ class Game { entrance: this.dungeon ? this.dungeon.entrance : null, exit: this.dungeon ? this.dungeon.exit : null, treasure: this.dungeon ? this.dungeon.treasure : null, - // Torches array is not sent; client uses visibleTiles to identify torch locations }; - // Debug: Check if known torch locations are correctly identified in visibleTiles - if (this.dungeon && this.dungeon.torches && this.dungeon.torches.length > 0) { - const firstFewTorches = this.dungeon.torches.slice(0, Math.min(5, this.dungeon.torches.length)); - for (const torchPos of firstFewTorches) { - const tx = torchPos[0]; - const ty = torchPos[1]; - if (state.visibleTiles[ty] && state.visibleTiles[ty][tx] !== undefined) { - console.log(`[Game.getState DEBUG] Visible torch at (${tx},${ty}). TileType in visibleTiles: ${state.visibleTiles[ty][tx]}`); - } - } - } - console.log("šŸŽ® getState() sending entities - Monster:", state.monster, "Entrance:", state.entrance, "Exit:", state.exit, "Treasure:", state.treasure); if (state.treasure === null) { console.log("āœ… TREASURE NULL: Confirmed treasure is null in getState() - treasure was picked up!"); @@ -168,80 +182,27 @@ class Game { return state; } - // Calculate lighting levels for all visible tiles based on distance from torches calculateLighting() { - const lightingData = {}; - const playerX = this.player.x; - const playerY = this.player.y; - const maxTorchInfluenceRadius = 10 + 6; // Player FOV radius + max light distance - const allTorches = this.dungeon?.torches || []; - const nearbyTorches = allTorches.filter(torch => { - const distToPlayer = Math.max(Math.abs(torch[0] - playerX), Math.abs(torch[1] - playerY)); - return distToPlayer < maxTorchInfluenceRadius; - }); - - console.log(`[CalculateLighting] Player at (${playerX},${playerY}). Found ${allTorches.length} total torches. Nearby torches (radius ${maxTorchInfluenceRadius}): ${nearbyTorches.length}`); - - let sampleAlpha1 = -1, sampleAlpha2 = -1, sampleAlpha3 = -1; // For logging flicker - - for (const yKey in this.visibleTiles) { - const y = parseInt(yKey); - lightingData[y] = {}; - - for (const xKey in this.visibleTiles[y]) { - const x = parseInt(xKey); - - let minDistanceToTorch = Infinity; - if (nearbyTorches.length === 0) { - minDistanceToTorch = maxTorchInfluenceRadius; // Effectively max darkness if no torches nearby - } else { - for (const torch of nearbyTorches) { - const distance = Math.max(Math.abs(x - torch[0]), Math.abs(y - torch[1])); - minDistanceToTorch = Math.min(minDistanceToTorch, distance); - } - } - - let alpha = 0.0; - - if (minDistanceToTorch === 0) { - alpha = 0.0; - } else if (minDistanceToTorch === 1) { - // alpha = 0.15 + Math.random() * 0.15; // 0.15-0.3 - alpha = Math.random() < 0.5 ? 0.05 : 0.35; // TEST: More extreme flicker - if (sampleAlpha1 < 0) sampleAlpha1 = alpha; - } else if (minDistanceToTorch === 2) { - // alpha = 0.3 + Math.random() * 0.2; // 0.3-0.5 - alpha = Math.random() < 0.5 ? 0.2 : 0.6; // TEST: More extreme flicker - if (sampleAlpha2 < 0) sampleAlpha2 = alpha; - } else if (minDistanceToTorch === 3) { - // alpha = 0.5 + Math.random() * 0.2; // 0.5-0.7 - alpha = Math.random() < 0.5 ? 0.4 : 0.8; // TEST: More extreme flicker - if (sampleAlpha3 < 0) sampleAlpha3 = alpha; - } else { - const maxDistance = 6; - const scaledDistance = Math.min(minDistanceToTorch, maxDistance); - // Ensure non-flickering part is also somewhat dark if far - if (minDistanceToTorch >= maxDistance) { - alpha = 0.9; // Max darkness for distant tiles - } else { - // Gradual darkness for tiles between distance 3 and maxDistance (6) - // This part should not flicker as much, providing a stable falloff - alpha = 0.7 + ((scaledDistance - 3) / (maxDistance - 3)) * 0.2; // 0.7-0.9 - } - } - - alpha = Math.max(0.0, Math.min(0.9, alpha)); - lightingData[y][x] = alpha; - } - } - - // const lightingTileCount = Object.keys(lightingData).reduce((acc, yKey) => acc + Object.keys(lightingData[yKey]).length, 0); - // console.log(`šŸ’” calculateLighting() - generated lighting for ${lightingTileCount} tiles.`); - console.log(`šŸ’” Flicker samples (dist 1,2,3): ${sampleAlpha1.toFixed(3)}, ${sampleAlpha2.toFixed(3)}, ${sampleAlpha3.toFixed(3)}`); - - return lightingData; + return LightingAndFov.calculateLighting( + this.player, + this.dungeon?.torches || [], + this.visibleTiles, + 10 + ); } + /** + * End this game and update user statistics + * @param {string} result - 'won', 'lost', or 'abandoned' + * @param {object} gameStats - Additional statistics + */ + endGame(result, gameStats = {}) { + if (this.user) { + const score = gameStats.score || 0; + this.user.endGame(result, score, gameStats); + } + this.gameState = 'ended'; + } } module.exports = Game; \ No newline at end of file diff --git a/src/backend/index.js b/src/backend/index.js index 2b16e0f..58a22e8 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -31,6 +31,29 @@ const Game = require('./game'); const activeGames = new Map(); // Maps socketId to Game objects const playerMoveTimestamps = new Map(); // Track last move time per player const MOVE_COOLDOWN = 100; // Minimum 100ms between moves on server side + +/** + * Create a new game for a user and register it in activeGames + * @param {User} user - The user object + * @param {string} gameType - 'standard' or 'legacy' + * @param {object} options - Additional game options + */ +function createGameForUser(user, gameType = 'standard', options = {}) { + let game; + + if (gameType === 'legacy') { + game = Game.createLegacyGame(user.id, user, options); + } else { + game = Game.createStandardGame(user.id, user, options); + } + + // Register the game with the user and in activeGames + user.joinGame(game); + activeGames.set(user.id, game); + + console.log(`[createGameForUser] Created ${gameType} game ${game.id} for user ${user.id}`); + return game; +} let lastBlockHeight = 0; // Track last block height const MIN_FEE = 10; // Minimum entrance fee in WOW const MAX_FEE = 100; // Maximum entrance fee in WOW @@ -117,8 +140,7 @@ function startGamesForWaiting(blockHeight) { console.log(`šŸ•’ QUEUE ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); try { - const game = currentUser.startGame(80, 40); - activeGames.set(serverId, game); + const game = createGameForUser(currentUser, 'standard'); const gameState = game.getState(); gameState.blockHeight = blockHeight; @@ -274,8 +296,7 @@ io.on('connection', function(socket) { currentUser.blockRec = currentBlock; // Player enters on current block console.log(`šŸ•’ AUTO-ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); - const game = currentUser.startGame(80, 40); - activeGames.set(socket.id, game); + const game = createGameForUser(currentUser, 'standard'); const gameState = game.getState(); gameState.blockHeight = currentBlock; @@ -329,11 +350,12 @@ io.on('connection', function(socket) { }); // Handle player movement - socket.on('player_move', function(moveData) { // Changed from 'move' to 'player_move' and added moveData + socket.on('player_move', function(moveData) { console.log(`Player move event received from ${socket.id}:`, moveData); const currentUser = getUserBySocket(socket.id); + const game = activeGames.get(socket.id); - if (currentUser && currentUser.game && currentUser.game.gameState === 'active') { + if (currentUser && game && game.gameState === 'active') { // Server-side movement throttling const now = Date.now(); const lastMoveTime = playerMoveTimestamps.get(socket.id) || 0; @@ -346,7 +368,6 @@ io.on('connection', function(socket) { if (typeof moveData.dx === 'number' && typeof moveData.dy === 'number') { playerMoveTimestamps.set(socket.id, now); // Update timestamp before processing - const game = currentUser.game; const moveResult = game.movePlayer(moveData.dx, moveData.dy); // This should update player pos and FOV if (moveResult && moveResult.status === 'moved') { @@ -360,6 +381,7 @@ io.on('connection', function(socket) { if (checkMonsterKill(game.player, game.monster)) { game.gameState = 'lost'; + game.endGame('lost', { score: 0, reason: 'monster' }); io.to(socket.id).emit('game_over', { status: 'lost', reason: 'monster', @@ -377,10 +399,17 @@ io.on('connection', function(socket) { // Check for escape if (moveResult.event === 'escaped') { game.gameState = 'won'; + const score = game.player.hasTreasure ? 100 : 50; // Bonus for treasure + game.endGame('won', { + score: score, + reason: 'escaped', + treasuresFound: game.player.hasTreasure ? 1 : 0 + }); io.to(socket.id).emit('game_over', { status: 'won', reason: 'escaped', - message: 'Congratulations! You escaped the dungeon!' + message: 'Congratulations! You escaped the dungeon!', + score: score }); activeGames.delete(socket.id); return; @@ -511,7 +540,7 @@ function startGameForPlayer(socketId, blockHeight) { try { // Create a new game - const game = currentUser.startGame(25, 19); + const game = createGameForUser(currentUser, 'legacy'); if (!game) { console.error("Game creation failed"); return; diff --git a/src/backend/lightingAndFov.js b/src/backend/lightingAndFov.js new file mode 100644 index 0000000..70eef073 --- /dev/null +++ b/src/backend/lightingAndFov.js @@ -0,0 +1,127 @@ +const ROT = require('./rot.js'); + +class LightingAndFov { + /** + * Initializes the FOV calculator. + * @param {object} dungeonMap - The dungeon map. + * @param {object} gameConfig - Game configuration containing tile definitions. + * @returns {object} ROT.FOV.PreciseShadowcasting instance. + */ + static initializeFOV(dungeonMap, gameConfig) { + return new ROT.FOV.PreciseShadowcasting( + (x, y) => { + if (!dungeonMap[y] || dungeonMap[y][x] === undefined) return false; + const tile = dungeonMap[y][x]; + const primaryFloor = gameConfig.primaryFloor || "'1"; + const secondaryFloor = gameConfig.secondaryFloor || "'2"; + return tile === primaryFloor || tile === secondaryFloor; + } + ); + } + + /** + * Updates the visible tiles based on player position and FOV. + * @param {object} fov - The FOV calculator instance. + * @param {object} player - The player object with x, y coordinates. + * @param {object} dungeonMap - The dungeon map. + * @param {number} fovRadius - The radius of the FOV. + * @returns {object} The visible tiles object. + */ + static updateFOV(fov, player, dungeonMap, fovRadius = 10) { + const visibleTiles = {}; + fov.compute(player.x, player.y, fovRadius, (x, y, r, visibility) => { + if (visibility > 0) { + if (!visibleTiles[y]) { + visibleTiles[y] = {}; + } + if (dungeonMap && dungeonMap[y] && dungeonMap[y][x] !== undefined) { + visibleTiles[y][x] = dungeonMap[y][x]; + } + } + }); + return visibleTiles; + } + + /** + * Calculates lighting levels for all visible tiles based on distance from torches. + * @param {object} player - The player object with x, y coordinates. + * @param {Array} allTorches - An array of all torch positions [[x,y], ...]. + * @param {object} visibleTiles - The visible tiles object. + * @param {number} fovRadius - The player's FOV radius. + * @returns {object} The lighting data object. + */ + static calculateLighting(player, allTorches = [], visibleTiles, fovRadius = 10) { + const lightingData = {}; + const playerX = player.x; + const playerY = player.y; + // const maxLightDistance = 6; // Max distance light from a torch can reach // Old value + const maxDarknessDistance = 8; // Distance at which light fully drops to max darkness + const maxTorchInfluenceRadius = fovRadius + maxDarknessDistance; + + const nearbyTorches = allTorches.filter(torch => { + const distToPlayer = Math.max(Math.abs(torch[0] - playerX), Math.abs(torch[1] - playerY)); + return distToPlayer < maxTorchInfluenceRadius; + }); + + // console.log(`[CalculateLighting] Player at (${playerX},${playerY}). Found ${allTorches.length} total torches. Nearby torches (radius ${maxTorchInfluenceRadius}): ${nearbyTorches.length}`); + + // let sampleAlpha1 = -1, sampleAlpha2 = -1, sampleAlpha3 = -1; // For logging flicker + // let sampleAlpha4 = -1, sampleAlpha5 = -1; // Optional for extended logging + + for (const yKey in visibleTiles) { + const y = parseInt(yKey); + lightingData[y] = {}; + for (const xKey in visibleTiles[y]) { + const x = parseInt(xKey); + let minDistanceToTorch = Infinity; + + if (nearbyTorches.length === 0) { + minDistanceToTorch = maxTorchInfluenceRadius; // Effectively max darkness if no torches nearby + } else { + for (const torch of nearbyTorches) { + const distance = Math.max(Math.abs(x - torch[0]), Math.abs(y - torch[1])); + minDistanceToTorch = Math.min(minDistanceToTorch, distance); + } + } + + let alpha = 0.0; + if (minDistanceToTorch === 0) { // Tile is a torch + alpha = 0.0; + } else if (minDistanceToTorch === 1) { + alpha = 0.05 + Math.random() * 0.15; // Flicker: 0.05 - 0.20 + // if (sampleAlpha1 < 0) sampleAlpha1 = alpha; + } else if (minDistanceToTorch === 2) { + alpha = 0.15 + Math.random() * 0.15; // Flicker: 0.15 - 0.30 + // if (sampleAlpha2 < 0) sampleAlpha2 = alpha; + } else if (minDistanceToTorch === 3) { + alpha = 0.25 + Math.random() * 0.15; // Flicker: 0.25 - 0.40 + // if (sampleAlpha3 < 0) sampleAlpha3 = alpha; + } else if (minDistanceToTorch === 4) { + alpha = 0.35 + Math.random() * 0.15; // Flicker: 0.35 - 0.50 + // if (sampleAlpha4 < 0) sampleAlpha4 = alpha; + } else if (minDistanceToTorch === 5) { + alpha = 0.45 + Math.random() * 0.15; // Flicker: 0.45 - 0.60 + // if (sampleAlpha5 < 0) sampleAlpha5 = alpha; + } else { + // Stable falloff for distances > 5 up to maxDarknessDistance + const falloffStartDistance = 5; // End of flicker zones + const lightLevelAtFalloffStart = 0.6; // Max alpha from distance 5 flicker (0.45 + 0.15) + const maxDarknessAlpha = 0.9; + + if (minDistanceToTorch >= maxDarknessDistance) { + alpha = maxDarknessAlpha; + } else { // minDistanceToTorch is between (falloffStartDistance + 1) and (maxDarknessDistance - 1) + let progress = (minDistanceToTorch - falloffStartDistance) / (maxDarknessDistance - falloffStartDistance); + alpha = lightLevelAtFalloffStart + progress * (maxDarknessAlpha - lightLevelAtFalloffStart); + } + } + alpha = Math.max(0.0, Math.min(0.9, alpha)); // Clamp alpha between 0.0 and 0.9 + lightingData[y][x] = alpha; + } + } + // console.log(\`šŸ’” Flicker samples (dist 1,2,3): \${sampleAlpha1.toFixed(3)}, \${sampleAlpha2.toFixed(3)}, \${sampleAlpha3.toFixed(3)}\`); + return lightingData; + } +} + +module.exports = LightingAndFov; diff --git a/src/backend/user.js b/src/backend/user.js index 208bf78..7fa5a4a 100644 --- a/src/backend/user.js +++ b/src/backend/user.js @@ -1,80 +1,99 @@ -const Game = require('./game'); // Add this import at the top +const Game = require('./game'); // Ensure Game is required -// Global users registry +// User class definition +class User { + constructor(id, address) { + this.id = id; // This is the socket.id + this.address = address; + this.currentGame = null; // Reference to current game instance + this.blockRec = 0; // Block record for game entry timing + this.clientId = null; // To store socket.client.id if different + + // User statistics + this.stats = { + gamesPlayed: 0, + gamesWon: 0, + gamesLost: 0, + totalPlayTime: 0, + highestScore: 0, + treasuresFound: 0, + monstersDefeated: 0 + }; + + // Add user to the registry upon creation + userRegistry.set(id, this); + console.log(`User created and registered: ${id}`); + } + + /** + * Start tracking a new game for this user. + * @param {Game} gameInstance - The game instance this user is playing + */ + joinGame(gameInstance) { + this.currentGame = gameInstance; + this.stats.gamesPlayed++; + console.log(`[User.joinGame] User ${this.id} joined game ${gameInstance.id}. Total games played: ${this.stats.gamesPlayed}`); + } + + /** + * Game ended - update user statistics + * @param {string} result - 'won', 'lost', or 'abandoned' + * @param {number} score - Final score + * @param {object} gameStats - Additional game statistics + */ + endGame(result, score = 0, gameStats = {}) { + if (this.currentGame) { + this.currentGame = null; + + // Update statistics + if (result === 'won') { + this.stats.gamesWon++; + } else if (result === 'lost') { + this.stats.gamesLost++; + } + + if (score > this.stats.highestScore) { + this.stats.highestScore = score; + } + + if (gameStats.treasuresFound) { + this.stats.treasuresFound += gameStats.treasuresFound; + } + + if (gameStats.monstersDefeated) { + this.stats.monstersDefeated += gameStats.monstersDefeated; + } + + console.log(`[User.endGame] User ${this.id} game ended: ${result}. Score: ${score}. Stats: ${JSON.stringify(this.stats)}`); + } + } + + /** + * Get user statistics + */ + getStats() { + return { ...this.stats }; + } + + /** + * Get user's current game status + */ + isInGame() { + return this.currentGame !== null; + } + + // ... other User methods ... +} + +// User registry (maps socketId to User objects) const userRegistry = new Map(); -function User(socketid, ip) { - this.id = null; - this.socketid = socketid; - this.ip = ip; - this.nick = null; - this.receiveAddy = ""; - this.payoutAddy = ""; - this.blockRec = null; - this.map = null; - this.player = false; - this.won = false; - this.game = null; // Reference to current game - this.paymentAmount = 0; - this.paymentVerified = false; - - // Auto-register in the registry - userRegistry.set(socketid, this); - console.log(`User registered with socket ID: ${socketid}`); -} - -// Add lookup function -function getUserBySocketId(socketId) { - const user = userRegistry.get(socketId); - console.log(`Looking up user ${socketId}: ${user ? "FOUND" : "NOT FOUND"}`); - return user; -} - -// Add function to remove user -function removeUser(socketId) { - const removed = userRegistry.delete(socketId); - console.log(`User ${socketId} removed: ${removed ? "YES" : "NO"}`); - return removed; -} - -// Get all users as array -function getAllUsers() { - return Array.from(userRegistry.values()); -} - -// Add this method to your User function/class -User.prototype.startGame = function(width, height, gameConfig = {}) { - console.log(`Creating new game for user ${this.socketid} with dimensions ${width}x${height}`); - - // Default game configuration - can be customized per user/game - const defaultConfig = { - playerType: "@", // Options: "@", "@2" - treasureType: "$W", // Options: "$W" (Wownero), "$M" (Monero), "$B" (Bitcoin), "$" (Regular) - monsterType: "~", // Options: "~", "~2" - primaryFloor: "'1", // Main floor tile (99% of floors) - secondaryFloor: "'2", // Alt floor tile (1% of floors) - floorVariation: 0.01, // Percentage of secondary floor tiles (0.01 = 1%) - torchEnabled: true, // Enable/disable torch placement - torchDensity: 0.15, // Percentage of wall tiles that get torches (0.15 = 15%) - torchTile: "torch" // Torch tile identifier - }; - - const config = { ...defaultConfig, ...gameConfig }; - - try { - this.game = new Game(this.socketid, width, height, config); - return this.game; - } catch (err) { - console.error("Error creating game:", err); - return null; - } -}; - -// Rest of your existing code... - +// ... (rest of the file, e.g., getUserBySocketId, removeUser, module.exports) +// Ensure these functions and module.exports are correctly placed relative to the class definition. +// For example: module.exports = { - User: User, - getUserBySocketId: getUserBySocketId, - removeUser: removeUser, - getAllUsers: getAllUsers + User, + getUserBySocketId: (socketId) => userRegistry.get(socketId), + removeUser: (socketId) => userRegistry.delete(socketId), + getAllUsers: () => Array.from(userRegistry.values()) // If you need to iterate over users };