diff --git a/README.md b/README.md
index 6d5f6c0..44268b6 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,15 @@ A blockchain-based roguelike dungeon crawler built on the Wownero network.
## Project Overview
-Wowngeon is a web-based roguelike game that integrates with the Wownero blockchain for a unique gaming experience. Players enter a randomly generated dungeon with the goal of escaping before the next block is discovered on the network.
+Wowngeon is a web-based roguelike game that integrates with the Wownero blockchain for a unique gaming experience. Players enter a randomly generated dungeon with the goal of escaping before the next block is discovered on the network. The player requests to enter (and optionally pay an entrance fee), and then waits for the next block to be mined. When the next block is found, the player is placed in the dungeon and must escape before the next block is found. If they fail to escape, they die and lose their entrance fee. There is a singular monster and treasure in the dungeon. If the monster catches the player, they die. If the player finds the treasure and manages to escape, they could win 3x or 5x instead of just 2x for escaping sans treasure. The game is designed to be played in a web browser, making it accessible to a wide audience.
+The game is built using Node.js for the backend, with Express and Socket.io for real-time communication. The frontend is developed using HTML5, CSS, and JavaScript, with ROT.js for rendering the dungeon and managing game mechanics. The Wownero blockchain is integrated via RPC calls to monitor block height and handle transactions.
### Key Features:
- **Blockchain-linked gameplay**: Players enter when one block is found and must escape before the next block (approximately 5 minutes in Wownero)
- **Permadeath mechanics**: If a player fails to escape before the next block is found, they die
- **Random dungeons**: Each game features procedurally generated maps using ROT.js
-- **Monster AI**: Evade monsters as you navigate the dungeon
+- **Monster AI**: Evade monster as you navigate the dungeon
- **Treasure hunting**: Find treasure for bonus rewards
- **Wownero integration**: Players pay an entrance fee (10-100 WOW) and can win rewards
diff --git a/game.js b/game.js
index a564041..2e68745 100644
--- a/game.js
+++ b/game.js
@@ -115,51 +115,56 @@ class Game {
this.visibleTiles = {};
// Calculate new visible tiles
- this.fov.compute(this.player.x, this.player.y, 10, (x, y) => {
- if (!this.visibleTiles[y]) this.visibleTiles[y] = {};
- this.visibleTiles[y][x] = this.dungeon.map[y][x];
+ 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];
+ } 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));
}
movePlayer(dx, dy) {
const newX = this.player.x + dx;
const newY = this.player.y + dy;
- // Check if the move is valid (not into a wall)
- if (this.dungeon.map[newY] && this.dungeon.map[newY][newX] === 0) {
+ // Check if the move is valid (not into a wall and within map bounds)
+ if (this.dungeon &&
+ this.dungeon.map[newY] &&
+ this.dungeon.map[newY][newX] !== undefined &&
+ this.dungeon.map[newY][newX] === 0) { // 0 is floor
+
this.player.x = newX;
this.player.y = newY;
+ console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`);
- // Update FOV after movement
+ // Update FOV after moving
this.updateFOV();
- // Check if player found treasure
- if (this.dungeon.treasure &&
- newX === this.dungeon.treasure[0] &&
- newY === this.dungeon.treasure[1]) {
+ // Check for game events like finding treasure or exit
+ if (this.dungeon.exit && newX === this.dungeon.exit[0] && newY === this.dungeon.exit[1]) {
+ // Handle win condition (e.g., escaped)
+ this.gameState = 'won'; // Or some other state
+ return { status: 'moved', event: 'escaped', player: this.player, visibleTiles: this.visibleTiles };
+ }
+ if (this.dungeon.treasure && newX === this.dungeon.treasure[0] && newY === this.dungeon.treasure[1] && !this.player.hasTreasure) {
this.player.hasTreasure = true;
+ // Remove treasure from map or mark as collected
+ return { status: 'moved', event: 'treasure_found', player: this.player, visibleTiles: this.visibleTiles };
}
- // Check if player reached exit
- if (this.dungeon.exit &&
- newX === this.dungeon.exit[0] &&
- newY === this.dungeon.exit[1]) {
- this.gameState = 'won';
- return { status: 'won', hasTreasure: this.player.hasTreasure };
- }
-
- // Move monster after player moves
- this.moveMonster();
-
- // Check if monster caught player
- if (checkMonsterKill(this.player, this.monster)) {
- this.gameState = 'lost';
- return { status: 'lost', reason: 'caught' };
- }
-
- return { status: 'moved' };
+ return { status: 'moved', player: this.player, visibleTiles: this.visibleTiles };
}
+ console.log(`Player move to ${newX},${newY} for socket ${this.socketId} is invalid (wall or out of bounds).`);
return { status: 'invalid' };
}
@@ -202,77 +207,26 @@ class Game {
// Update the getState() method to include all entities with relative positions
getState() {
// Calculate relative position of all entities from player's perspective
- const playerX = this.player.x;
- const playerY = this.player.y;
+ // const playerX = this.player.x; // Not needed if sending absolute visibleTiles
+ // const playerY = this.player.y; // Not needed if sending absolute visibleTiles
- // Convert all visible tiles to relative coordinates
- const relativeVisibleTiles = {};
- for (const y in this.visibleTiles) {
- for (const x in this.visibleTiles[y]) {
- const relX = parseInt(x) - playerX;
- const relY = parseInt(y) - playerY;
-
- // Create row if needed
- if (!relativeVisibleTiles[relY]) {
- relativeVisibleTiles[relY] = {};
- }
-
- // Store tile type at relative position
- relativeVisibleTiles[relY][relX] = this.visibleTiles[y][x];
- }
- }
-
- // Create the state object
- const state = {
+ // The client-side Game.js expects absolute coordinates for visibleTiles.
+ // So, no conversion to relative coordinates is needed here for visibleTiles.
+ // Just ensure this.visibleTiles is correctly populated by updateFOV.
+
+ // console.log("getState called. Player:", this.player, "Visible tiles keys:", Object.keys(this.visibleTiles || {}).length);
+
+ return {
gameState: this.gameState,
- player: {
- x: 0, // Player is always at (0,0) in relative coordinates
- y: 0,
- hasKey: this.player.hasKey || false,
- hasTreasure: this.player.hasTreasure || false
- },
- visibleTiles: relativeVisibleTiles
+ player: { ...this.player }, // Send a copy of player object
+ monster: this.monster ? { ...this.monster } : null, // Send a copy if monster exists
+ // items: { ...this.items }, // If you have items, send them too
+ visibleTiles: { ...this.visibleTiles }, // Send a copy of visible tiles
+ // You might also want to send entrance/exit if they are always known or become known
+ // entrance: this.dungeon ? this.dungeon.entrance : null,
+ // exit: this.dungeon ? this.dungeon.exit : null,
+ // treasure: this.dungeon ? this.dungeon.treasure : null
};
-
- // Add monster if in view
- if (this.monster && this.visibleTiles[this.monster.y] &&
- this.visibleTiles[this.monster.y][this.monster.x] !== undefined) {
- state.monster = {
- x: this.monster.x - playerX, // Relative position
- y: this.monster.y - playerY
- };
- }
-
- // Add entrance if defined
- if (this.dungeon && this.dungeon.entrance) {
- // Use array format for consistency
- state.entrance = [
- this.dungeon.entrance.x - playerX,
- this.dungeon.entrance.y - playerY
- ];
- }
-
- // Add exit if in view
- if (this.dungeon && this.dungeon.exit &&
- this.visibleTiles[this.dungeon.exit.y] &&
- this.visibleTiles[this.dungeon.exit.y][this.dungeon.exit.x] !== undefined) {
- state.exit = [
- this.dungeon.exit.x - playerX,
- this.dungeon.exit.y - playerY
- ];
- }
-
- // Add treasure if in view
- if (this.dungeon && this.dungeon.treasure &&
- this.visibleTiles[this.dungeon.treasure.y] &&
- this.visibleTiles[this.dungeon.treasure.y][this.dungeon.treasure.x] !== undefined) {
- state.treasure = [
- this.dungeon.treasure.x - playerX,
- this.dungeon.treasure.y - playerY
- ];
- }
-
- return state;
}
}
diff --git a/html/index.html b/html/index.html
index a32ff62..8de62d3 100644
--- a/html/index.html
+++ b/html/index.html
@@ -18,6 +18,7 @@
+
@@ -36,7 +37,7 @@
-
+
@@ -59,89 +60,6 @@
-
-
-
@@ -373,34 +373,25 @@ $(function() {
diff --git a/html/js/game.js b/html/js/game.js
index 30bd294..0f1db06 100644
--- a/html/js/game.js
+++ b/html/js/game.js
@@ -8,850 +8,713 @@ if (typeof window !== 'undefined' && !window.socket) {
}
var Game = {
- // Change dimensions to match the display options (25 × 19 tiles)
- _screenWidth: 25, // Used for drawing coordinates (columns)
- _screenHeight: 19, // Used for drawing coordinates (rows)
- _mapWidth: 25, // Logical map width (should match display)
- _mapHeight: 19, // Logical map height (should match display)
_display: null,
- _map: null,
+ _screenWidth: options.width,
+ _screenHeight: options.height,
+ _isTileMode: false, // Determined after checking tileset
+ _displayReady: false, // Flag to indicate if _display is initialized
_player: null,
+ defaultFg: "#FFF", // Define defaultFg here
+ // _defaultBg: null, // Standard for transparent/display default
_monster: null,
- _exit: null,
- _entrance: null,
- _treasure: null,
- _gameActive: false,
- _message: "", // To display messages like 'waiting'
- _visibleTiles: {}, // Store currently visible tiles
- _exploredTiles: {}, // Store all tiles the player has seen
- _isTileMode: false, // Track if we're in tile mode
+ _map: {},
+ _items: {},
+ _gameInProgress: false,
+ _inputEnabled: true,
+ _messageLog: [],
+ _maxLogMessages: 5,
+
+ init: function() {
+ console.log("Game.init called. 'this' is Game:", this === Game);
+ console.log("Is this.checkTileset a function?", typeof this.checkTileset);
+
+ if (typeof this.checkTileset !== 'function') {
+ console.error("CRITICAL: Game.checkTileset is not a function. Halting initialization.");
+ alert("Error: Game initialization failed (checkTileset not found). Please check game.js.");
+ return;
+ }
+
+ this.checkTileset(function(tilesetAvailable) {
+ console.log("Game.checkTileset callback. Tileset available: " + tilesetAvailable);
+ this._isTileMode = tilesetAvailable;
+
+ var displayOptions = {
+ width: this._screenWidth,
+ height: this._screenHeight,
+ forceSquareRatio: true, // Good for both modes generally
+ };
+
+ if (this._isTileMode) {
+ console.log("Initializing display in TILE mode.");
+ displayOptions.layout = "tile";
+ displayOptions.bg = window.options.bg; // Should be "transparent"
+ displayOptions.fg = window.options.fg || "#FFF"; // Default foreground for text not in tileMap
+ displayOptions.tileWidth = window.options.tileWidth;
+ displayOptions.tileHeight = window.options.tileHeight;
+ displayOptions.tileSet = window.options.tileSet;
+ displayOptions.tileMap = window.options.tileMap;
+ displayOptions.tileColorize = false; // Render tiles as-is from the tileset
+ } else {
+ console.log("Initializing display in ASCII mode.");
+ displayOptions.layout = "rect";
+ displayOptions.bg = "#000000"; // Black background for ASCII
+ displayOptions.fg = window.options.fg || "#FFF"; // Default foreground
+ displayOptions.fontSize = 18; // Adjust as needed for your font/preference
+ // Ensure no tile-specific options are passed if they might cause errors
+ delete displayOptions.tileWidth;
+ delete displayOptions.tileHeight;
+ delete displayOptions.tileSet;
+ delete displayOptions.tileMap;
+ }
+
+ try {
+ var gameDisplayContainer = document.getElementById("game-display");
+ if (!gameDisplayContainer) {
+ console.error("Game display container #game-display not found in HTML!");
+ alert("Error: Game display container #game-display not found. Cannot start game.");
+ return;
+ }
+ this._display = new ROT.Display(displayOptions);
+ gameDisplayContainer.innerHTML = ''; // Clear any previous content (e.g., loading messages)
+ gameDisplayContainer.appendChild(this._display.getContainer());
+ console.log("ROT.Display initialized and appended. Display object:", this._display);
+ this._displayReady = true; // Set flag AFTER successful initialization
+ console.log("Game display initialized. _displayReady set to true.");
+
+ // Now that the display is ready, other things can happen,
+ // like drawing the welcome screen or enabling socket event handlers fully.
+ this._drawWelcomeScreen(); // Example: draw initial screen
+
+ } catch (e) {
+ console.error("Error initializing ROT.Display:", e);
+ alert("Failed to initialize game display. Error: " + e.message + ". Check console for details.");
+ this._displayReady = false; // Explicitly false on error
+ }
+ }.bind(this)); // CRUCIAL: bind 'this' for the callback to refer to the Game object
+ },
+
+ checkTileset: function(callback) {
+ console.log("Game.checkTileset called to determine mode.");
+ // This function relies on options.js to have attempted loading the tileset
+ // and set window.tileSet, window.tileMap, and window.globalTileSetStatus.
+
+ // Give a very short moment for options.js to potentially finish initial load attempts.
+ setTimeout(function() {
+ if (window.globalTileSetStatus === "error") {
+ console.warn("Tileset loading previously failed (window.globalTileSetStatus is 'error'). Using ASCII mode.");
+ callback(false); // Tileset not available
+ } else if (window.tileSet && window.tileSet.complete && window.tileSet.naturalHeight !== 0 && window.tileMap && Object.keys(window.tileMap).length > 0) {
+ console.log("Tileset and tileMap appear to be loaded and valid. Using TILE mode.");
+ callback(true); // Tileset available
+ } else {
+ // If not immediately ready, and no definitive error, try waiting a bit longer.
+ // options.js has its own timeouts. This is a secondary check.
+ console.log("Tileset not immediately confirmed. Waiting up to 3 seconds for options.js to complete loading...");
+ let attempts = 0;
+ const maxAttempts = 6; // Try for 3 seconds (6 * 500ms)
+ const interval = setInterval(function() {
+ attempts++;
+ if (window.globalTileSetStatus === "error") {
+ clearInterval(interval);
+ console.warn("Tileset loading failed during interval check in Game.checkTileset. Using ASCII mode.");
+ callback(false);
+ } else if (window.tileSet && window.tileSet.complete && window.tileSet.naturalHeight !== 0 && window.tileMap && Object.keys(window.tileMap).length > 0) {
+ clearInterval(interval);
+ console.log("Tileset and tileMap confirmed after delay in Game.checkTileset. Using TILE mode.");
+ callback(true);
+ } else if (attempts >= maxAttempts) {
+ clearInterval(interval);
+ console.error("Tileset did not become available after extended wait in Game.checkTileset. Assuming not available. Using ASCII mode.");
+ callback(false); // Assume not available
+ }
+ }.bind(this), 500); // Check every 500ms
+ }
+ }.bind(this), 50); // Small initial delay for options.js to run
+ },
+
+ _ensureDisplay: function() {
+ if (!this._display || !this._displayReady) {
+ console.warn("Display not ready or not initialized. Aborting drawing/game operation.");
+ return false;
+ }
+ return true;
+ },
+
+ _forceClearToBlack: function() {
+ if (!this._ensureDisplay()) {
+ console.warn("Game._forceClearToBlack: Display not ready, aborting.");
+ return;
+ }
+ // console.log("Game: _forceClearToBlack called at " + new Date().toLocaleTimeString());
+
+ const display = this._display;
+
+ // Step 1: Clear ROT.js's internal data cache and dirty flags.
+ // This ensures that ROT.js starts with a clean slate for what it thinks is on screen.
+ display._data = {};
+ display._dirty = {};
+ // console.log("Game._forceClearToBlack: Cleared display._data and display._dirty.");
+
+ // Step 2: Directly clear the visual canvas to black.
+ // This bypasses ROT.js's drawing for the initial clear, ensuring it's black.
+ var canvas = display.getContainer(); // This is typically the