diff --git a/README.md b/README.md index 18d7b5a..40d7317 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,12 @@ The game is built using Node.js for the backend, with Express and Socket.io for - **Random dungeons**: Each game features procedurally generated maps using ROT.js - **Monster AI**: Evade monster as you navigate the dungeon - **Treasure hunting**: Find treasure for bonus rewards -- **Real-time multiplayer**: Multiple players can play simultaneously -- **Chat system**: Players can communicate in real-time during gameplay +- **Real-time multiplayer**: Multiple players can play simultaneously with efficient socket broadcasting +- **Structured chat system**: Public chat broadcasts and private status updates with command separation - **Field of view**: Limited visibility adds challenge and atmosphere - **HTML START button**: Visible button in status area for game entry +- **Auto-return system**: 30-second timeout returns players to title screen after game over +- **Spectator-ready architecture**: Socket system designed for future spectator mode implementation ## Technology Stack @@ -26,7 +28,27 @@ The game is built using Node.js for the backend, with Express and Socket.io for - **Frontend**: HTML5, CSS, JavaScript with ROT.js for tile-based rendering - **Blockchain**: Wownero RPC integration for block monitoring - **Database**: SQLite for user data and game history -- **Real-time Communication**: WebSocket connections via Socket.io +- **Real-time Communication**: WebSocket connections via Socket.io with structured event broadcasting + +## Socket.io Real-Time Communication + +The game implements a sophisticated real-time communication system using Socket.io: + +### Broadcasting Strategy +- **Block Height**: Broadcast to all clients every 5 seconds (not just on changes) +- **Game Updates**: Player-specific with spectator support planned +- **Chat Messages**: Public broadcasts vs private status updates +- **Connection Status**: Immediate updates on connect/disconnect + +### Event Categories +1. **Global Broadcasts** (`io.emit()`) - Block height, public chat +2. **Player-Specific** (`io.to(socketId).emit()`) - Game updates, status messages +3. **Future Spectator Support** - Multi-cast game updates to viewers + +### Production Scalability +- Efficient event routing prevents unnecessary network traffic +- Structured for adding spectator mode without performance impact +- Clean separation between commands and actual chat messages ## Architecture @@ -34,7 +56,7 @@ The project has been refactored into a clean, modular architecture with clear se ### Backend Structure (`src/backend/`) -- **index.js** - Main server file handling HTTP requests, WebSocket connections, game lifecycle, and blockchain monitoring +- **index.js** - Main server file handling HTTP requests, WebSocket connections, game lifecycle, blockchain monitoring, and structured socket event broadcasting - **game.js** - Game class with dungeon generation, player movement, monster AI, and field-of-view calculations - **user.js** - User registration, authentication, game association, and payment tracking - **player.js** - Player state management and movement logic @@ -43,6 +65,12 @@ The project has been refactored into a clean, modular architecture with clear se - **dbcalls.js** - Database operations for user data and game history - **rpccalls.js** - Wownero blockchain RPC integration +#### Socket Event Broadcasting (index.js) +- **Helper Functions**: `sendGameUpdate()`, `broadcastBlockHeight()`, `sendStatusUpdate()` +- **Event Types**: Global broadcasts, player-specific updates, status messages +- **Spectator Ready**: Architecture supports future multi-cast to spectators +- **Regular Broadcasting**: Block height sent every 5 seconds to all clients + ### Frontend Structure (`html/`) #### Modular JavaScript Architecture: @@ -79,8 +107,88 @@ The project has been refactored into a clean, modular architecture with clear se - **socketHandlers.js** - WebSocket communication with initialization protection - **ui.js** - User interface management and chat functionality +## Socket Event Architecture + +The game uses a structured socket.io event system designed for real-time multiplayer gameplay with future spectator support: + +### Event Broadcasting Strategy + +#### **Block Height Broadcasting** (All Clients) +- `blockheight` - Broadcast to ALL connected clients every 5 seconds +- Ensures all players stay synchronized with blockchain state +- Immediate emission on client connection +- Regular updates prevent clients from missing block changes + +#### **Game State Updates** (Player + Future Spectators) +- `game_update` - Currently sent to active player only +- **Future Goal**: Broadcast to spectators watching specific games +- Structured for easy expansion to multi-viewer support +- Contains player position, FOV, monster state, items + +#### **Chat System** (Broadcast vs Player-Specific) +- `chat_broadcast` - PUBLIC chat messages sent to ALL clients +- `status_update` - PRIVATE status messages sent to individual players +- Commands (like "enter", "hello") trigger status updates, not chat broadcasts +- Real chat messages include username, timestamp, and message content + +#### **Connection & Status Management** +- `status_update` - Player-specific notifications (errors, confirmations, help) +- `welcome` - Initial connection acknowledgment +- `game_start` / `game_over` - Game lifecycle events +- Connection status immediately broadcast on client connect + +### Socket Event Types + +```javascript +// Broadcast Events (All Clients) +io.emit('blockheight', blockHeight); +io.emit('chat_broadcast', { username, message, timestamp }); + +// Player-Specific Events +io.to(socketId).emit('status_update', { type, message, timestamp }); +io.to(socketId).emit('game_update', gameState); +io.to(socketId).emit('game_start', gameState); +io.to(socketId).emit('game_over', { status, reason, message }); + +// Future Spectator Events (TODO) +io.to(spectatorId).emit('spectator_update', { playerSocketId, gameState }); +``` + +### Future Spectator System (Roadmap) + +The socket architecture is designed to support spectating with minimal changes: + +1. **Spectator Registration**: Players can request to watch active games +2. **Multi-Cast Updates**: Game updates broadcast to player + all their spectators +3. **Spectator-Specific Events**: Special events for spectator UI (player info, game stats) +4. **Spectator Chat**: Separate chat channels for spectators vs players +5. **Spectator Limits**: Configurable limits on spectators per game + +### Real-Time Communication Goals + +- **Blockchain Sync**: All clients receive block updates every few seconds (not just on block changes) +- **Scalable Game Updates**: Player actions broadcast efficiently to relevant viewers only +- **Rich Chat System**: Distinction between commands/status and actual chat communication +- **Spectator Ready**: Architecture supports adding spectator features without major refactoring + ## Recent Major Improvements +### ๐Ÿ”Œ Socket Architecture Overhaul +- **Implemented**: Structured event system for broadcast vs player-specific messages +- **Features**: Block height broadcasting, chat system separation, spectator-ready design +- **Helper Functions**: `sendGameUpdate()`, `broadcastBlockHeight()`, `sendStatusUpdate()` +- **Result**: Clean separation of concerns and foundation for spectator mode + +### ๐Ÿ’€ Death Logic Fix +- **Fixed**: Players not dying at correct block timing in autostart/debug mode +- **Solution**: Updated death logic to kill players the block after they enter +- **Result**: Players die exactly when block advances after entry (block N+1 after entering on block N) + +### โฐ Auto-Return to Title Screen +- **Added**: 30-second timeout after game over (win/lose/timeout) +- **Features**: Automatic return to welcome screen, proper game state reset +- **Result**: Seamless game flow without manual intervention + ### ๐ŸŽฏ Movement System Overhaul - **Fixed**: Players skipping over tiles in hallways - **Solution**: Added movement throttling (100ms) and prevented duplicate event handlers @@ -91,11 +199,6 @@ The project has been refactored into a clean, modular architecture with clear se - **Solution**: Moved to HTML status area with proper styling and visibility control - **Result**: Clearly visible green "๐ŸŽฎ START GAME" button -### โฑ๏ธ Auto-Entry Timing Fix -- **Fixed**: Players dying after 2+ blocks instead of 1 block in debug mode -- **Solution**: Fixed backend timing logic to set `currentUser.blockRec = currentBlock + 1` -- **Result**: Consistent 3-block timing for all entry methods - ### ๐Ÿš€ Auto-Start System - **Added**: Immediate game start for development and testing - **Features**: Debug mode detection, 'D' key shortcut, proper SocketHandlers initialization diff --git a/html/js/game.js b/html/js/game.js index 1b8c572..e4f6ee3 100644 --- a/html/js/game.js +++ b/html/js/game.js @@ -47,12 +47,14 @@ var Game = { return navigator.userAgent.toLowerCase().indexOf('firefox') > -1; }, - startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles) { + startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles, lightingData, torchData) { if (!this._ensureDisplay()) { console.error("Game: Cannot start game, display not ready."); return false; } console.log("Game: startGame called. PlayerData:", playerData, "MapData:", mapData, "InitialVisibleTiles:", initialVisibleTiles); + console.log("๐Ÿ”ฅ LIGHTING DATA received in startGame:", lightingData); + console.log("๐Ÿ”ฆ TORCH DATA received in startGame:", torchData); DisplayManager.clearDisplay(); @@ -92,6 +94,16 @@ var Game = { GameState._exit = null; GameState._treasure = null; + // Initialize lighting and torch data + if (lightingData) { + GameState._lighting = lightingData; + console.log("๐Ÿ”ฅ GameState: Lighting data stored with", Object.keys(lightingData).length, "rows"); + } + if (torchData) { + GameState._torches = torchData; + console.log("๐Ÿ”ฆ GameState: Torch data stored with", torchData.length, "torches"); + } + // Initialize map and validate player position GameState.initializeMap(mapData, initialVisibleTiles, this._screenWidth, this._screenHeight); @@ -155,6 +167,15 @@ var Game = { _drawGameScreen: function() { if (!this._ensureDisplay()) return; const gameState = GameState.getGameStateForRender(); + + // Debug: Verify lighting data is included in render state + console.log("๐ŸŽจ _drawGameScreen() passing lighting data:", { + hasLighting: !!gameState.lighting, + lightingKeys: gameState.lighting ? Object.keys(gameState.lighting).length : 0, + hasTorches: !!gameState.torches, + torchCount: gameState.torches ? gameState.torches.length : 0 + }); + RenderEngine.drawGameScreen(gameState); }, diff --git a/html/js/gameState.js b/html/js/gameState.js index fd13451..a1988f0 100644 --- a/html/js/gameState.js +++ b/html/js/gameState.js @@ -13,6 +13,8 @@ var GameState = { _message: null, _scheduler: null, _engine: null, + _lighting: {}, + _torches: [], init: function() { this.reset(); @@ -30,6 +32,8 @@ var GameState = { this._exit = null; this._treasure = null; this._message = null; + this._lighting = {}; + this._torches = []; if (this._scheduler) this._scheduler.clear(); if (!this._scheduler) this._scheduler = new ROT.Scheduler.Simple(); @@ -249,6 +253,21 @@ var GameState = { needsRedraw = true; } + // Update lighting and torch data + if (data.lighting !== undefined) { + console.log("๐Ÿ”ฅ BEFORE UPDATE: this._lighting has", this._lighting ? Object.keys(this._lighting).length : 0, "rows"); + console.log("๐Ÿ”ฅ NEW DATA: data.lighting has", Object.keys(data.lighting).length, "rows"); + console.log("๐Ÿ”ฅ LIGHTING DATA SAMPLE:", JSON.stringify(data.lighting).substring(0, 200)); + this._lighting = data.lighting; + console.log("๐Ÿ”ฅ AFTER UPDATE: this._lighting has", this._lighting ? Object.keys(this._lighting).length : 0, "rows"); + needsRedraw = true; + } + if (data.torches !== undefined) { + this._torches = data.torches; + needsRedraw = true; + console.log("๐Ÿ”ฆ GameState: Updated torch data with", data.torches.length, "torches"); + } + // Update visible tiles (most critical for movement feedback) if (data.visibleTiles && typeof data.visibleTiles === 'object' && Object.keys(data.visibleTiles).length > 0) { console.log("Visible tiles updated. Number of rows:", Object.keys(data.visibleTiles).length); @@ -305,20 +324,6 @@ var GameState = { } }, - getGameStateForRender: function() { - return { - player: this._player, - monster: this._monster, - items: this._items, - entrance: this._entrance, - exit: this._exit, - treasure: this._treasure, - visibleTiles: this._visibleTiles, - exploredTiles: this._exploredTiles, - message: this._message - }; - }, - // Debug functions debugPrintMap: function(screenWidth, screenHeight) { console.log("Current game map:"); @@ -338,7 +343,7 @@ var GameState = { debugTileMapping: function() { const testValues = [0, 1, undefined, null]; for (const val of testValues) { - const char = (val === 1) ? '#' : "'"; + const char = (val === 1) ? '#' : "\'"; const debugMsg = `Test mapping: value=${val} -> char='${char}'`; console.log(debugMsg); if (window.GameDebug) window.GameDebug.updateDebugDisplay(debugMsg); @@ -376,75 +381,18 @@ var GameState = { console.log("Visible tiles:", Object.keys(this._visibleTiles).length); }, - // Update game state with new data from server - updateGameState: function(data) { - let needsRedraw = false; - - if (data.player && (data.player.x !== this._player.x || data.player.y !== this._player.y)) { - this._player = data.player; - needsRedraw = true; - } - - if (data.monster) { - this._monster = data.monster; - needsRedraw = true; - } - - if (data.items) { - this._items = data.items; - needsRedraw = true; - } - - if (data.map) { - this._map = data.map; - needsRedraw = true; - } - - // Handle visibleTiles from server (this is the FOV data) - if (data.visibleTiles) { - this._visibleTiles = data.visibleTiles; - - // Update explored tiles with newly visible areas - for (const yKey in this._visibleTiles) { - const y = parseInt(yKey); - if (!this._exploredTiles[y]) this._exploredTiles[y] = {}; - for (const xKey in this._visibleTiles[y]) { - const x = parseInt(xKey); - this._exploredTiles[y][x] = this._visibleTiles[y][x]; - } - } - - // console.log("Updated visibleTiles from server, visible tile count:", - // Object.keys(this._visibleTiles).reduce((count, y) => count + Object.keys(this._visibleTiles[y]).length, 0)); - needsRedraw = true; - } - - if (data.entrance) { - this._entrance = data.entrance; - needsRedraw = true; - } - - if (data.exit) { - this._exit = data.exit; - needsRedraw = true; - } - - if (data.treasure !== undefined) { - this._treasure = data.treasure; - needsRedraw = true; - } - - if (data.message) { - this._message = data.message; - needsRedraw = true; - } - - return needsRedraw; - }, - // Get game state data for rendering getGameStateForRender: function() { - return { + // Debug: Log the actual lighting data being returned + console.log("๐Ÿ” getGameStateForRender() lighting check:", { + lightingExists: !!this._lighting, + lightingKeys: this._lighting ? Object.keys(this._lighting).length : 0, + lightingRef: this._lighting, + torchExists: !!this._torches, + torchCount: this._torches ? this._torches.length : 0 + }); + + const renderState = { map: this._map, player: this._player, monster: this._monster, @@ -455,8 +403,13 @@ var GameState = { visibleTiles: this._visibleTiles, exploredTiles: this._exploredTiles, message: this._message, - gameActive: this._gameActive + gameActive: this._gameActive, + lighting: this._lighting, + torches: this._torches }; + + console.log("๐Ÿ” getGameStateForRender() RETURNING lighting with", renderState.lighting ? Object.keys(renderState.lighting).length : 0, "keys"); + return renderState; }, // Move player and update game state @@ -485,9 +438,6 @@ var GameState = { this._player.x = newX; this._player.y = newY; - // Recompute field of view - this.computeFieldOfView(); - // console.log("Player moved to:", this._player.x, this._player.y); return true; } diff --git a/html/js/options.js b/html/js/options.js index ddfeb80..d51b640 100644 --- a/html/js/options.js +++ b/html/js/options.js @@ -19,6 +19,7 @@ var options = { height: 19, // 19 tiles * 32px/tile = 608px tileWidth: 32, // Width of a single tile in pixels tileHeight: 32, // Height of a single tile in pixels + tileColorize: true, // Enable colorization for tile stacks tileSet: tileSet, // GAME CONFIGURATION - Change these to select different assets diff --git a/html/js/renderEngine.js b/html/js/renderEngine.js index 00e8014..b428581 100644 --- a/html/js/renderEngine.js +++ b/html/js/renderEngine.js @@ -27,6 +27,7 @@ var RenderEngine = { const torches = gameState.torches || []; // New torch positions const visibleTiles = gameState.visibleTiles || {}; const exploredTiles = gameState.exploredTiles || {}; + const lighting = gameState.lighting || {}; // Lighting data from server const message = gameState.message; // Get configured tile types from options @@ -56,6 +57,12 @@ var RenderEngine = { let baseFg = this.defaultFg; let tileType; + // Debug log for a specific tile if needed - replace with actual test coordinates + // const wx_test = 10, wy_test = 10; // Example coordinates + // if (wx === wx_test && wy === wy_test) { + // console.log(`RenderEngine DEBUG: Tile (${wx},${wy}) raw lighting data: ${lighting[wy] ? lighting[wy][wx] : 'N/A'}`); + // } + // Render base terrain if (this.isVisible(wx, wy, visibleTiles)) { tileType = visibleTiles[wy][wx]; @@ -106,11 +113,28 @@ var RenderEngine = { if (baseChar && baseChar !== ' ') { charStack.push(baseChar); - // Use black transparent overlay for explored tiles, normal colors for currently visible tiles - if (this.isVisible(wx, wy, visibleTiles)) { - fgStack.push("transparent"); // Currently visible - use normal tile colors + // Get lighting information for this tile + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + + // Apply lighting shadows for terrain + const isCurrentlyVisible = this.isVisible(wx, wy, visibleTiles); + + // Added log to inspect lightingAlpha for visible, lit tiles + if (isCurrentlyVisible && lightingAlpha > 0.01) { // Using 0.01 to catch small, non-zero alphas + console.log(`RenderEngine TRACE: Tile (${wx},${wy}), Visible: ${isCurrentlyVisible}, lightingAlpha: ${lightingAlpha.toFixed(3)}, from: ${lighting[wy] ? lighting[wy][wx].toFixed(3) : 'N/A'}`); + } + + if (isCurrentlyVisible) { + if (lightingAlpha > 0) { + // Apply torch shadow using foreground overlay + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + // Normal tile with no shadow + fgStack.push("transparent"); + } } else { - fgStack.push("rgba(0, 0, 0, 0.6)"); // Explored but not visible - use dark transparent overlay + // Explored but not currently visible - apply darker shadow + fgStack.push(`rgba(0, 0, 0, 0.7)`); } bgStack.push("transparent"); // Always transparent background for terrain @@ -123,7 +147,13 @@ var RenderEngine = { const currentItem = items[itemKey]; if (currentItem && currentItem.x === wx && currentItem.y === wy && this.isVisible(wx, wy, visibleTiles)) { charStack.push('$'); - fgStack.push("transparent"); // Use transparent for now + // Apply torch lighting to items + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + if (lightingAlpha > 0) { + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + fgStack.push("transparent"); + } bgStack.push("transparent"); } } @@ -133,42 +163,68 @@ var RenderEngine = { // Render entrance if (entrance && entrance[0] === wx && entrance[1] === wy && this.isVisible(wx, wy, visibleTiles)) { charStack.push('<'); - fgStack.push("transparent"); // Use transparent for now + // Apply torch lighting to entrance + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + if (lightingAlpha > 0) { + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + fgStack.push("transparent"); + } bgStack.push("transparent"); } // Render exit if (exit && exit[0] === wx && exit[1] === wy && this.isVisible(wx, wy, visibleTiles)) { charStack.push('>'); - fgStack.push("transparent"); // Use transparent for now + // Apply torch lighting to exit + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + if (lightingAlpha > 0) { + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + fgStack.push("transparent"); + } bgStack.push("transparent"); } // Render treasure if (treasure && treasure[0] === wx && treasure[1] === wy && this.isVisible(wx, wy, visibleTiles)) { charStack.push(treasureTile); - fgStack.push("transparent"); // Use transparent for now + // Apply torch lighting to treasure + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + if (lightingAlpha > 0) { + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + fgStack.push("transparent"); + } bgStack.push("transparent"); } // Render torches (check if this tile has a torch) if (this.isVisible(wx, wy, visibleTiles) && visibleTiles[wy][wx] === 'torch') { charStack.push(torchTile); - fgStack.push("transparent"); // Use transparent for now + // Don't apply shadows to torches since they're light sources + fgStack.push("transparent"); bgStack.push("transparent"); } // Render monster if (monster && monster.x === wx && monster.y === wy && this.isVisible(wx, wy, visibleTiles)) { charStack.push(monsterTile); - fgStack.push("transparent"); // Use transparent for now + // Apply torch lighting to monster + const lightingAlpha = (lighting[wy] && lighting[wy][wx]) ? lighting[wy][wx] : 0.0; + if (lightingAlpha > 0) { + fgStack.push(`rgba(0, 0, 0, ${Math.min(lightingAlpha, 0.8)})`); + } else { + fgStack.push("transparent"); + } bgStack.push("transparent"); } // Render player (always on top) if (wx === playerWX && wy === playerWY) { charStack.push(playerTile); - fgStack.push("transparent"); // Use transparent for now + // Don't apply shadows to player - keep them clearly visible + fgStack.push("transparent"); bgStack.push("transparent"); } diff --git a/html/js/socketHandlers.js b/html/js/socketHandlers.js index 650a2ec..e7dc1eb 100644 --- a/html/js/socketHandlers.js +++ b/html/js/socketHandlers.js @@ -104,6 +104,8 @@ const SocketHandlers = { console.log(" - Monster:", data?.monster); console.log(" - Items:", data?.items); console.log(" - VisibleTiles keys:", data?.visibleTiles ? Object.keys(data.visibleTiles).length : "NO VISIBLE TILES"); + console.log(" - Lighting keys:", data?.lighting ? Object.keys(data.lighting).length : "NO LIGHTING"); + console.log(" - Torches:", data?.torches ? data.torches.length : "NO TORCHES (expected - lighting calculated server-side)"); $('#messages').append($('
  • ').text("Starting game...")); @@ -118,8 +120,8 @@ const SocketHandlers = { } try { - // Use the original game.js structure - var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles); + // Pass all game data including lighting and torches + var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles, data.lighting, data.torches); console.log("Game start result:", success ? "SUCCESS" : "FAILED"); if (!success) { @@ -143,6 +145,22 @@ const SocketHandlers = { onGameUpdate: function(data) { // Debug log to see what data we're receiving + console.log("๐ŸŽฎ GAME UPDATE received with keys:", Object.keys(data)); + + // Debug lighting data specifically + if (data.lighting && Object.keys(data.lighting).length > 0) { + console.log("๐Ÿ”ฅ GAME UPDATE: lighting data received with", Object.keys(data.lighting).length, "rows"); + } else { + console.log("โš ๏ธ GAME UPDATE: NO lighting data in update"); + } + + // Debug torch data specifically + if (data.torches && data.torches.length > 0) { + console.log("๐Ÿ”ฆ GAME UPDATE: torch data received with", data.torches.length, "torches"); + } else { + console.log("โ„น๏ธ GAME UPDATE: No torch data (expected - lighting calculated server-side)"); + } + if (data.visibleTiles) { const debugMsg = "๐Ÿ” SOCKET: Received game update with visibleTiles"; console.log(debugMsg); diff --git a/index.js b/index.js new file mode 100644 index 0000000..7d823a4 --- /dev/null +++ b/index.js @@ -0,0 +1,422 @@ +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/game.js b/src/backend/game.js index 6de2d80..1c5df7f 100644 --- a/src/backend/game.js +++ b/src/backend/game.js @@ -141,10 +141,11 @@ class Game { player: this.player.getState(), // Use Player's getState method monster: this.monster ? this.monster.getState() : null, // Use Monster's getState method visibleTiles: { ...this.visibleTiles }, // Send a copy of visible tiles + lighting: this.calculateLighting(), // Add lighting data entrance: this.dungeon ? this.dungeon.entrance : null, exit: this.dungeon ? this.dungeon.exit : null, treasure: this.dungeon ? this.dungeon.treasure : null, - torches: this.dungeon ? this.dungeon.torches : [], // Include torch positions + // Don't send torch positions - client doesn't need them since lighting is calculated server-side }; // Debug logging to verify entity data @@ -155,6 +156,67 @@ class Game { return state; } + + // Calculate lighting levels for all visible tiles based on distance from torches + calculateLighting() { + const lightingData = {}; + + // Get torch positions from dungeon + const torches = this.dungeon?.torches || []; + console.log("๐Ÿ”ฅ calculateLighting() - torch positions:", torches); + + // For each visible tile, calculate the lighting level + for (const yKey in this.visibleTiles) { + const y = parseInt(yKey); + lightingData[y] = {}; + + for (const xKey in this.visibleTiles[y]) { + const x = parseInt(xKey); + + // Calculate minimum distance to any torch + let minDistanceToTorch = Infinity; + + for (const torch of torches) { + const distance = Math.max(Math.abs(x - torch[0]), Math.abs(y - torch[1])); // Chebyshev distance (diagonal movement) + minDistanceToTorch = Math.min(minDistanceToTorch, distance); + } + + // Convert distance to alpha value for darkness overlay + let alpha = 0.0; + + if (minDistanceToTorch === 0) { + // On a torch - no darkness + alpha = 0.0; + } else if (minDistanceToTorch === 1) { + // Adjacent to torch - light darkness with flickering effect + alpha = 0.15 + Math.random() * 0.15; // 0.15-0.3 + } else if (minDistanceToTorch === 2) { + // Two tiles away - moderate darkness with flickering + alpha = 0.3 + Math.random() * 0.2; // 0.3-0.5 + } else if (minDistanceToTorch === 3) { + // Three tiles away - heavier darkness with flickering + alpha = 0.5 + Math.random() * 0.2; // 0.5-0.7 + } else { + // Far from torches - scale up to maximum darkness with flickering + const maxDistance = 6; // Beyond this distance, maximum darkness + const scaledDistance = Math.min(minDistanceToTorch, maxDistance); + alpha = 0.7 + (scaledDistance - 3) / (maxDistance - 3) * 0.2; // 0.7-0.9 + } + + // Clamp alpha to reasonable bounds + 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("๐Ÿ’ก LIGHTING DATA SAMPLE:", JSON.stringify(lightingData).substring(0, 200)); + + return lightingData; + } + } // Check if monster killed player diff --git a/src/backend/index.js b/src/backend/index.js index 0b858c7..2b16e0f 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -173,6 +173,11 @@ function checkGamesTimeout(currentHeight) { * @param {object} gameState - The game state to broadcast */ function sendGameUpdate(playerSocketId, gameState) { + // Debug logging for lighting data in updates + console.log(`๐Ÿ“ก sendGameUpdate to ${playerSocketId}:`); + console.log(` - Lighting data: ${!!gameState.lighting} (${gameState.lighting ? Object.keys(gameState.lighting).length : 0} rows)`); + console.log(` - Torch data: ${!!gameState.torches} (${gameState.torches ? gameState.torches.length : 0} torches)`); + // Send to the player io.to(playerSocketId).emit('game_update', gameState); @@ -383,8 +388,23 @@ io.on('connection', function(socket) { const updatedGameState = game.getState(); // Get the new state + // Debug logging for lighting data in game updates + console.log(`๐Ÿ” GAME UPDATE DEBUG for ${socket.id}:`); + console.log(` - Player position: (${updatedGameState.player?.x}, ${updatedGameState.player?.y})`); + console.log(` - Visible tiles keys: ${Object.keys(updatedGameState.visibleTiles || {}).length} rows`); + console.log(` - Lighting data included: ${!!updatedGameState.lighting}`); + if (updatedGameState.lighting) { + const lightingTileCount = Object.keys(updatedGameState.lighting).reduce((acc, yKey) => + acc + Object.keys(updatedGameState.lighting[yKey] || {}).length, 0); + console.log(` - Lighting tiles count: ${lightingTileCount}`); + } + console.log(` - Torch data included: ${!!updatedGameState.torches}`); + if (updatedGameState.torches) { + console.log(` - Torch count: ${updatedGameState.torches.length}`); + } + // 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 || {})); + console.log(`Sending game_update to ${socket.id} after player move.`); // Send game update to player (current implementation) // TODO: In future, also broadcast to spectators of this game diff --git a/src/backend/monster.js b/src/backend/monster.js index 0d048c7..5183381 100644 --- a/src/backend/monster.js +++ b/src/backend/monster.js @@ -27,10 +27,15 @@ class Monster { const dx = Math.sign(player.x - this.x); const dy = Math.sign(player.y - this.y); + // Check if a tile is passable (floor) + const isPassable = (tile) => { + return tile === "'1" || tile === "'2" || tile === 0; // Support both new and legacy floor types + }; + // Try horizontal move first if (dx !== 0) { const newX = this.x + dx; - if (dungeon.map[this.y] && dungeon.map[this.y][newX] === 0) { + if (dungeon.map[this.y] && isPassable(dungeon.map[this.y][newX])) { this.x = newX; return; } @@ -39,7 +44,7 @@ class Monster { // Try vertical move if (dy !== 0) { const newY = this.y + dy; - if (dungeon.map[newY] && dungeon.map[newY][this.x] === 0) { + if (dungeon.map[newY] && isPassable(dungeon.map[newY][this.x])) { this.y = newY; return; } @@ -52,7 +57,7 @@ class Monster { for (const [dx, dy] of shuffledDirs) { const nx = this.x + dx; const ny = this.y + dy; - if (dungeon.map[ny] && dungeon.map[ny][nx] === 0) { + if (dungeon.map[ny] && isPassable(dungeon.map[ny][nx])) { this.x = nx; this.y = ny; break; diff --git a/test/debug_client_lighting.html b/test/debug_client_lighting.html new file mode 100644 index 0000000..61c2add --- /dev/null +++ b/test/debug_client_lighting.html @@ -0,0 +1,163 @@ + + + + Debug Client Lighting + + + +

    Client-Side Lighting Debug

    + +
    +

    Game State Lighting Data

    +
    No data received yet...
    +
    + +
    +

    Lighting Visualization

    +
    +
    + +
    +

    Console Log

    +
    +
    + + + + + + diff --git a/test/debug_game_updates.html b/test/debug_game_updates.html new file mode 100644 index 0000000..edeb27c --- /dev/null +++ b/test/debug_game_updates.html @@ -0,0 +1,186 @@ + + + + Game Update Debugging + + + +

    Game Update Debugging

    + +
    + + + + +
    + +
    Not connected
    +
    + + + + + diff --git a/test/debug_lighting.html b/test/debug_lighting.html new file mode 100644 index 0000000..575ac14 --- /dev/null +++ b/test/debug_lighting.html @@ -0,0 +1,174 @@ + + + + Lighting Debug - Wowngeon + + + +

    Lighting System Debug

    + +
    +

    Alpha Value Tests

    +

    Testing different alpha values to see how they render:

    +
    +
    + +
    +

    Game Display

    +
    + + +
    + +
    +

    Live Lighting Data

    +
    No data captured yet
    +
    + +
    +

    Console Log

    +
    +
    + + + + + + + + + + + + + + + + diff --git a/test/test_lighting.html b/test/test_lighting.html new file mode 100644 index 0000000..5d6f45c --- /dev/null +++ b/test/test_lighting.html @@ -0,0 +1,132 @@ + + + + Torch Lighting Test + + + +

    Torch Lighting System Test

    +

    Open browser console to see detailed lighting data flow test.

    +
    + + + + diff --git a/test/test_lighting_verification.html b/test/test_lighting_verification.html new file mode 100644 index 0000000..915ae7d --- /dev/null +++ b/test/test_lighting_verification.html @@ -0,0 +1,216 @@ + + + + + + Torch Lighting Verification Test + + + +

    ๐Ÿ”ฅ Torch Lighting System Verification

    + +
    +

    Connection Test

    +
    Connecting...
    +
    + +
    +

    Data Reception Test

    +
    Waiting for lighting data...
    +
    Waiting for torch data...
    +
    Waiting for game state...
    +
    + +
    +

    Rendering Test

    +
    Waiting for render test...
    +
    Checking tileColorize setting...
    +
    + +
    +

    Live Data

    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + diff --git a/test_tile_system.js b/test_tile_system.js deleted file mode 100644 index 5eea13d..0000000 --- a/test_tile_system.js +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script to demonstrate the new tile system - * Shows floor variations, torch placement, and configurable assets - */ - -const DungeonGenerator = require('./src/backend/dungeon.js'); - -console.log("๐Ÿ›๏ธ WOWNGEON TILE SYSTEM TEST"); -console.log("=" * 50); - -// Test 1: Default Configuration -console.log("\n๐Ÿ”ง Test 1: Default Configuration"); -const defaultConfig = { - playerType: "@", - treasureType: "$W", - monsterType: "~", - primaryFloor: "'1", - secondaryFloor: "'2", - floorVariation: 0.01, - torchEnabled: true, - torchDensity: 0.15, - torchTile: "torch" -}; - -const dungeon1 = DungeonGenerator.generate(20, 15, defaultConfig); -console.log(`Generated dungeon: ${dungeon1.map.length}x${dungeon1.map[0].length}`); -console.log(`Entrance: [${dungeon1.entrance}]`); -console.log(`Exit: [${dungeon1.exit}]`); -console.log(`Treasure: [${dungeon1.treasure}]`); -console.log(`Torches: ${dungeon1.torches.length} placed`); - -// Count tile types -const tileCounts = {}; -for (let y = 0; y < dungeon1.map.length; y++) { - for (let x = 0; x < dungeon1.map[0].length; x++) { - const tile = dungeon1.map[y][x]; - tileCounts[tile] = (tileCounts[tile] || 0) + 1; - } -} - -console.log("\n๐Ÿ“Š Tile Distribution:"); -Object.keys(tileCounts).sort().forEach(tile => { - console.log(` ${tile}: ${tileCounts[tile]} tiles`); -}); - -if (tileCounts["'1"] && tileCounts["'2"]) { - const floorTotal = tileCounts["'1"] + tileCounts["'2"]; - const variationPercent = (tileCounts["'2"] / floorTotal * 100).toFixed(2); - console.log(` Floor variation: ${variationPercent}% secondary floors`); -} - -// Test 2: Custom Configuration -console.log("\n๐Ÿ”ง Test 2: Custom Configuration (Bitcoin theme)"); -const customConfig = { - playerType: "@2", - treasureType: "$B", - monsterType: "~2", - primaryFloor: "'1", - secondaryFloor: "'2", - floorVariation: 0.05, // 5% variation - torchEnabled: true, - torchDensity: 0.25, // 25% more torches - torchTile: "torch" -}; - -const dungeon2 = DungeonGenerator.generate(15, 10, customConfig); -console.log(`Generated custom dungeon: ${dungeon2.map.length}x${dungeon2.map[0].length}`); -console.log(`Torches in custom dungeon: ${dungeon2.torches.length}`); - -// Test 3: Show a small visual sample -console.log("\n๐Ÿ—บ๏ธ Sample Dungeon (10x8):"); -const sampleDungeon = DungeonGenerator.generate(10, 8, defaultConfig); - -console.log(" 0123456789"); -for (let y = 0; y < sampleDungeon.map.length; y++) { - let line = y.toString().padStart(2) + " "; - for (let x = 0; x < sampleDungeon.map[0].length; x++) { - let char = sampleDungeon.map[y][x]; - - // Add entities for display - if (sampleDungeon.entrance && sampleDungeon.entrance[0] === x && sampleDungeon.entrance[1] === y) { - char = '<'; - } else if (sampleDungeon.exit && sampleDungeon.exit[0] === x && sampleDungeon.exit[1] === y) { - char = '>'; - } else if (sampleDungeon.treasure && sampleDungeon.treasure[0] === x && sampleDungeon.treasure[1] === y) { - char = '$'; - } - - // Display character mapping - if (char === "'1") char = '.'; - else if (char === "'2") char = ':'; - else if (char === 'torch') char = 'T'; - else if (char === '#') char = '#'; - - line += char; - } - console.log(line); -} - -console.log("\nLegend:"); -console.log(" . = Primary floor ('1)"); -console.log(" : = Secondary floor ('2)"); -console.log(" # = Wall"); -console.log(" T = Torch"); -console.log(" < = Entrance"); -console.log(" > = Exit"); -console.log(" $ = Treasure"); - -console.log("\nโœ… Tile System Test Complete!"); -console.log("\nFeatures verified:"); -console.log(" โœ“ Configurable player/treasure/monster types"); -console.log(" โœ“ Floor tile variations (primary/secondary)"); -console.log(" โœ“ Configurable torch placement"); -console.log(" โœ“ Enhanced dungeon generation"); -console.log(" โœ“ Torch position tracking");