mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-19 23:28:32 +02:00
torch density fixed and some refactoring
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user