torch density fixed and some refactoring

This commit is contained in:
jwinterm
2025-05-26 19:23:07 -04:00
parent 65870420db
commit 54e58ee698
6 changed files with 440 additions and 668 deletions
-422
View File
@@ -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)}...`);
}
+74 -16
View File
@@ -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.`);
+107 -146
View File
@@ -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;
+38 -9
View File
@@ -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;
+127
View File
@@ -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;
+94 -75
View File
@@ -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
};