mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 14:51:26 +02:00
ok game is working-ish
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# Wowngeon
|
||||
|
||||
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. 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 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 monster as you navigate the dungeon
|
||||
- **Treasure hunting**: Find treasure for bonus rewards
|
||||
- **Real-time multiplayer**: Multiple players can play simultaneously
|
||||
- **Chat system**: Players can communicate in real-time during gameplay
|
||||
- **Field of view**: Limited visibility adds challenge and atmosphere
|
||||
- **HTML START button**: Visible button in status area for game entry
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: Node.js with Express and Socket.io
|
||||
- **Frontend**: HTML5, CSS, JavaScript with ROT.js for tile-based rendering
|
||||
- **Blockchain**: Wownero RPC integration for block monitoring
|
||||
- **Database**: SQLite for user data and game history
|
||||
- **Real-time Communication**: WebSocket connections via Socket.io
|
||||
|
||||
## Architecture
|
||||
|
||||
The project has been refactored into a clean, modular architecture with clear separation of concerns:
|
||||
|
||||
### Backend Structure (`src/backend/`)
|
||||
|
||||
- **index.js** - Main server file handling HTTP requests, WebSocket connections, game lifecycle, and blockchain monitoring
|
||||
- **game.js** - Game class with dungeon generation, player movement, monster AI, and field-of-view calculations
|
||||
- **user.js** - User registration, authentication, game association, and payment tracking
|
||||
- **player.js** - Player state management and movement logic
|
||||
- **monster.js** - Monster AI and behavior
|
||||
- **dungeon.js** - Dungeon generation and map management
|
||||
- **dbcalls.js** - Database operations for user data and game history
|
||||
- **rpccalls.js** - Wownero blockchain RPC integration
|
||||
|
||||
### Frontend Structure (`html/`)
|
||||
|
||||
#### Modular JavaScript Architecture:
|
||||
|
||||
1. **DisplayManager.js** - Display & Canvas Management
|
||||
- ROT.Display initialization and setup
|
||||
- Display state management and clearing
|
||||
- Tile-based rendering system
|
||||
|
||||
2. **ScreenManager.js** - Screen State Management
|
||||
- Welcome screen with block simulation (30-second intervals)
|
||||
- Win/lose screens and waiting screens
|
||||
- Centered text drawing utilities
|
||||
- HTML START button visibility control
|
||||
|
||||
3. **RenderEngine.js** - Game Rendering Engine
|
||||
- Game screen rendering and object drawing
|
||||
- Field of view rendering and visibility checks
|
||||
- Drawing game objects (player, monsters, items, terrain)
|
||||
|
||||
4. **GameState.js** - Game State Management
|
||||
- Player, map, monster, item data management
|
||||
- Game state updates and validation
|
||||
- Field of view computation fixes
|
||||
- Player movement logic
|
||||
|
||||
5. **game.js** - Main Game Controller
|
||||
- Coordinates all modules with clean APIs
|
||||
- Provides backward-compatible interface
|
||||
- Handles game initialization and flow
|
||||
|
||||
6. **Additional Modules**:
|
||||
- **inputHandler.js** - Keyboard/mouse input with movement throttling and HTML button handling
|
||||
- **socketHandlers.js** - WebSocket communication with initialization protection
|
||||
- **ui.js** - User interface management and chat functionality
|
||||
|
||||
## Recent Major Improvements
|
||||
|
||||
### 🎯 Movement System Overhaul
|
||||
- **Fixed**: Players skipping over tiles in hallways
|
||||
- **Solution**: Added movement throttling (100ms) and prevented duplicate event handlers
|
||||
- **Result**: Precise one-tile-per-keypress movement
|
||||
|
||||
### 🎮 START Button Fix
|
||||
- **Fixed**: START button invisible (cut off in game display area)
|
||||
- **Solution**: Moved to HTML status area with proper styling and visibility control
|
||||
- **Result**: Clearly visible green "🎮 START GAME" button
|
||||
|
||||
### ⏱️ Auto-Entry Timing Fix
|
||||
- **Fixed**: Players dying after 2+ blocks instead of 1 block in debug mode
|
||||
- **Solution**: Fixed backend timing logic to set `currentUser.blockRec = currentBlock + 1`
|
||||
- **Result**: Consistent 3-block timing for all entry methods
|
||||
|
||||
### 🚀 Auto-Start System
|
||||
- **Added**: Immediate game start for development and testing
|
||||
- **Features**: Debug mode detection, 'D' key shortcut, proper SocketHandlers initialization
|
||||
- **Result**: Games start instantly in debug mode for faster development
|
||||
|
||||
### 🏗️ Modular Architecture
|
||||
- **Completed**: Broke down 786-line monolithic game.js into focused modules
|
||||
- **Benefits**: Better maintainability, reusability, clean APIs, separation of concerns
|
||||
- **Result**: Much cleaner and more maintainable codebase
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone [repository-url]
|
||||
cd wowngeon
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
cd src/backend
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start the backend server**:
|
||||
```bash
|
||||
cd src/backend
|
||||
node index.js
|
||||
```
|
||||
|
||||
4. **Access the game**:
|
||||
- Open your browser to `http://localhost:3000`
|
||||
- In debug mode (localhost), press 'D' to start immediately
|
||||
- In production mode, wait for block simulation and click "🎮 START GAME"
|
||||
|
||||
## Game Controls
|
||||
|
||||
- **WASD** or **Arrow Keys**: Move player
|
||||
- **Enter** or **START Button**: Enter dungeon (when available)
|
||||
- **D Key**: Debug instant start (localhost only)
|
||||
- **Chat**: Type commands and communicate with other players
|
||||
|
||||
## Debug Mode
|
||||
|
||||
When running locally (localhost/127.0.0.1), the game includes debug features:
|
||||
|
||||
- **Immediate Start**: Press 'D' to bypass block timing
|
||||
- **Debug Logs**: Enhanced console logging with timing information
|
||||
- **Block Simulation**: 30-second intervals instead of real Wownero blocks
|
||||
- **Auto-Entry**: Instant game start for testing
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
wowngeon/
|
||||
├── README.md # This file
|
||||
├── src/backend/ # Backend server code
|
||||
│ ├── index.js # Main server file
|
||||
│ ├── game.js # Game logic and mechanics
|
||||
│ ├── user.js # User management
|
||||
│ ├── player.js # Player state management
|
||||
│ ├── monster.js # Monster AI
|
||||
│ ├── dungeon.js # Dungeon generation
|
||||
│ ├── dbcalls.js # Database operations
|
||||
│ └── rpccalls.js # Blockchain RPC calls
|
||||
├── html/ # Frontend web interface
|
||||
│ ├── index.html # Main game page
|
||||
│ ├── js/ # Modular JavaScript
|
||||
│ │ ├── displayManager.js # Display management
|
||||
│ │ ├── screenManager.js # Screen states
|
||||
│ │ ├── renderEngine.js # Game rendering
|
||||
│ │ ├── gameState.js # Game state management
|
||||
│ │ ├── game.js # Main game controller
|
||||
│ │ ├── inputHandler.js # Input handling
|
||||
│ │ ├── socketHandlers.js # WebSocket communication
|
||||
│ │ ├── ui.js # User interface
|
||||
│ │ └── options.js # Game configuration
|
||||
│ ├── styles/ # CSS styling
|
||||
│ └── tiles.png # Game tile graphics
|
||||
└── test/ # Test files
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes with proper commit messages
|
||||
4. Test thoroughly in both debug and production modes
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
[Add your license information here]
|
||||
|
||||
## Support
|
||||
|
||||
For questions, issues, or contributions, please refer to the project's issue tracker or contact the development team.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Wownero Roguelike - Complete
|
||||
|
||||
## Status: ✅ PRODUCTION READY
|
||||
|
||||
### Fixed Issues
|
||||
- **Undefined character error** - Replaced emoji characters with ASCII equivalents
|
||||
- **ROT.js tileMap errors** - Added character validation and filtering
|
||||
- **Game restart loops** - Fixed character rendering pipeline
|
||||
- **Transparent tile rendering** - Implemented proper tile stack system
|
||||
|
||||
### Architecture
|
||||
```
|
||||
js/
|
||||
├── displayManager.js # ROT.js display management
|
||||
├── screenManager.js # UI screens and block timing
|
||||
├── renderEngine.js # Game rendering with tile stacks
|
||||
├── gameState.js # Game state management
|
||||
├── game.js # Main controller
|
||||
├── inputHandler.js # Input handling
|
||||
├── socketHandlers.js # Socket communication
|
||||
├── ui.js # UI utilities
|
||||
└── options.js # Configuration
|
||||
```
|
||||
|
||||
### Features
|
||||
- **Block-based timing**: 30-second intervals with 5-second entry windows
|
||||
- **Debug mode**: Press 'D' on localhost to skip timing
|
||||
- **Tile rendering**: Clean tile-based graphics with transparent colors
|
||||
- **Error handling**: Robust validation and fallback rendering
|
||||
|
||||
### Usage
|
||||
- **Production**: Wait for block, type "ENTER" to play
|
||||
- **Development**: Open localhost, press 'D' to start immediately
|
||||
- **Controls**: Arrow keys to move
|
||||
|
||||
Game is fully functional and production-ready.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Wownero Roguelike
|
||||
|
||||
A tile-based dungeon crawler game using ROT.js with modular architecture.
|
||||
|
||||
## Features
|
||||
- Tile-based rendering with ROT.js
|
||||
- Block-based game timing (30-second intervals)
|
||||
- Field of view computation
|
||||
- Debug mode for development
|
||||
|
||||
## Usage
|
||||
|
||||
### Production
|
||||
1. Wait for "NEW BLOCK FOUND!" message (every 30 seconds)
|
||||
2. Type "ENTER" in chat during 5-second window
|
||||
3. Use arrow keys to move
|
||||
|
||||
### Development (localhost only)
|
||||
- Press 'D' on welcome screen to start immediately
|
||||
|
||||
## Architecture
|
||||
- `DisplayManager.js` - ROT.js display management
|
||||
- `ScreenManager.js` - Screen states and UI
|
||||
- `RenderEngine.js` - Game rendering with tile stacks
|
||||
- `GameState.js` - Game state management
|
||||
- `game.js` - Main game controller
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Wowngeon Debug</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #000;
|
||||
color: #0f0;
|
||||
padding: 20px;
|
||||
}
|
||||
.module {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.status-ok { color: #0f0; }
|
||||
.status-error { color: #f00; }
|
||||
.status-warning { color: #ff0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Wowngeon Module Debug</h1>
|
||||
|
||||
<div id="socket-status" class="module">
|
||||
<h3>Socket.io Status</h3>
|
||||
<div id="socket-info">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="modules-status" class="module">
|
||||
<h3>Module Status</h3>
|
||||
<div id="modules-info">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="game-status" class="module">
|
||||
<h3>Game Display Status</h3>
|
||||
<div id="game-info">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="console-log" class="module">
|
||||
<h3>Console Messages</h3>
|
||||
<div id="console-output"></div>
|
||||
</div>
|
||||
|
||||
<!-- Socket.io -->
|
||||
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
||||
|
||||
<!-- Initialize socket -->
|
||||
<script>
|
||||
window.socket = io();
|
||||
</script>
|
||||
|
||||
<!-- Core libraries -->
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="js/rot.min.js"></script>
|
||||
<script src="js/options.js"></script>
|
||||
<script src="js/teletype.js"></script>
|
||||
|
||||
<!-- Game modules -->
|
||||
<script src="js/game.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/socketHandlers.js"></script>
|
||||
<script src="js/inputHandler.js"></script>
|
||||
|
||||
<script>
|
||||
// Capture console messages
|
||||
const consoleOutput = document.getElementById('console-output');
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
function addToConsole(type, message) {
|
||||
const div = document.createElement('div');
|
||||
div.className = type === 'error' ? 'status-error' :
|
||||
type === 'warn' ? 'status-warning' : 'status-ok';
|
||||
div.textContent = `[${type.toUpperCase()}] ${message}`;
|
||||
consoleOutput.appendChild(div);
|
||||
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
||||
}
|
||||
|
||||
console.log = function(...args) {
|
||||
originalLog.apply(console, args);
|
||||
addToConsole('log', args.join(' '));
|
||||
};
|
||||
|
||||
console.error = function(...args) {
|
||||
originalError.apply(console, args);
|
||||
addToConsole('error', args.join(' '));
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
originalWarn.apply(console, args);
|
||||
addToConsole('warn', args.join(' '));
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log("Debug page ready - checking module status...");
|
||||
|
||||
// Check socket status
|
||||
$('#socket-info').html(`
|
||||
<div class="${window.socket ? 'status-ok' : 'status-error'}">
|
||||
Socket Available: ${!!window.socket}
|
||||
</div>
|
||||
<div class="${window.socket && window.socket.connected ? 'status-ok' : 'status-warning'}">
|
||||
Socket Connected: ${window.socket ? window.socket.connected : 'N/A'}
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Check modules
|
||||
const modules = ['Game', 'UI', 'SocketHandlers', 'InputHandler'];
|
||||
let moduleStatus = '';
|
||||
modules.forEach(module => {
|
||||
const available = typeof window[module] !== 'undefined';
|
||||
const hasInit = available && typeof window[module].init === 'function';
|
||||
moduleStatus += `
|
||||
<div class="${available ? 'status-ok' : 'status-error'}">
|
||||
${module}: ${available ? 'Available' : 'Missing'}
|
||||
${hasInit ? ' (has init)' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
$('#modules-info').html(moduleStatus);
|
||||
|
||||
// Check ROT.js
|
||||
$('#game-info').html(`
|
||||
<div class="${typeof ROT !== 'undefined' ? 'status-ok' : 'status-error'}">
|
||||
ROT.js Available: ${typeof ROT !== 'undefined'}
|
||||
</div>
|
||||
<div class="${typeof window.options !== 'undefined' ? 'status-ok' : 'status-error'}">
|
||||
Options Available: ${typeof window.options !== 'undefined'}
|
||||
</div>
|
||||
<div class="${document.getElementById('game-display') ? 'status-ok' : 'status-error'}">
|
||||
Game Display Container: ${!!document.getElementById('game-display')}
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Try to initialize modules
|
||||
if (typeof Game !== 'undefined') {
|
||||
console.log("Attempting to initialize Game...");
|
||||
try {
|
||||
Game.init();
|
||||
console.log("Game.init() completed successfully");
|
||||
} catch (e) {
|
||||
console.error("Game.init() failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof InputHandler !== 'undefined') {
|
||||
console.log("Attempting to initialize InputHandler...");
|
||||
try {
|
||||
InputHandler.init();
|
||||
console.log("InputHandler.init() completed successfully");
|
||||
} catch (e) {
|
||||
console.error("InputHandler.init() failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Socket event listeners
|
||||
if (window.socket) {
|
||||
window.socket.on('connect', function() {
|
||||
console.log('Socket connected successfully');
|
||||
$('#socket-info').append('<div class="status-ok">Socket connected event received</div>');
|
||||
});
|
||||
|
||||
window.socket.on('disconnect', function() {
|
||||
console.log('Socket disconnected');
|
||||
$('#socket-info').append('<div class="status-warning">Socket disconnected</div>');
|
||||
});
|
||||
|
||||
window.socket.on('welcome', function(data) {
|
||||
console.log('Welcome message received:', data);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Add a minimal game display area for testing -->
|
||||
<div id="game-display" style="width: 800px; height: 400px; border: 1px solid #333; margin: 20px 0;">
|
||||
<!-- Game display will be created here -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,6 +44,9 @@
|
||||
<div class="status">
|
||||
<div id="blockHeight">Block Height: <span id="heightValue">Loading...</span></div>
|
||||
<div id="connectionStatus">Status: <span id="statusValue">Connecting...</span></div>
|
||||
<button id="startButton" style="background: #0a5c0a; color: #ff0; border: 2px solid #0f0; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-top: 5px; display: none;">
|
||||
🎮 START GAME
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Game display -->
|
||||
|
||||
@@ -59,21 +59,26 @@ var DisplayManager = {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
forceClearToBlack: function() {
|
||||
}, forceClearToBlack: function() {
|
||||
if (!this.ensureDisplay()) {
|
||||
console.warn("DisplayManager.forceClearToBlack: Display not ready, aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const display = this._display;
|
||||
|
||||
// Step 1: Clear ROT.js's internal data cache and dirty flags
|
||||
display._data = {};
|
||||
display._dirty = {};
|
||||
|
||||
// Step 2: Directly clear the visual canvas to black
|
||||
// Step 2: Force redraw entire screen with blank characters
|
||||
for (let x = 0; x < this._screenWidth; x++) {
|
||||
for (let y = 0; y < this._screenHeight; y++) {
|
||||
display.draw(x, y, ' ', '#000', '#000');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Directly clear the visual canvas to black
|
||||
var canvas = display.getContainer();
|
||||
if (canvas && typeof canvas.getContext === 'function') {
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
+44
-5
@@ -16,10 +16,12 @@ var Game = {
|
||||
_maxLogMessages: 5,
|
||||
|
||||
init: function() {
|
||||
console.log("Game.init called - Modular version");
|
||||
|
||||
// Update screen dimensions from options
|
||||
if (typeof window.options !== 'undefined') {
|
||||
this._screenWidth = window.options.width || 25;
|
||||
this._screenHeight = window.options.height || 19;
|
||||
if (typeof options !== 'undefined') {
|
||||
this._screenWidth = options.width || 25;
|
||||
this._screenHeight = options.height || 19;
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
@@ -31,8 +33,7 @@ var Game = {
|
||||
RenderEngine.init(this._screenWidth, this._screenHeight);
|
||||
GameState.init();
|
||||
|
||||
// Start block simulation and draw welcome screen
|
||||
ScreenManager.startBlockSimulation();
|
||||
// Draw welcome screen
|
||||
ScreenManager.drawWelcomeScreen();
|
||||
|
||||
return true;
|
||||
@@ -48,13 +49,39 @@ var Game = {
|
||||
|
||||
startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles) {
|
||||
if (!this._ensureDisplay()) {
|
||||
console.error("Game: Cannot start game, display not ready.");
|
||||
return false;
|
||||
}
|
||||
console.log("Game: startGame called. PlayerData:", playerData, "MapData:", mapData, "InitialVisibleTiles:", initialVisibleTiles);
|
||||
|
||||
DisplayManager.clearDisplay();
|
||||
|
||||
// Force clear all display caches and artifacts
|
||||
if (typeof DisplayManager !== 'undefined' && DisplayManager.forceClearToBlack) {
|
||||
console.log("✅ Force clearing display to prevent artifacts...");
|
||||
DisplayManager.forceClearToBlack();
|
||||
}
|
||||
|
||||
// Stop the block simulation to prevent welcome screen from interfering
|
||||
if (typeof ScreenManager !== 'undefined' && ScreenManager.stopBlockSimulation) {
|
||||
console.log("✅ Stopping block simulation...");
|
||||
ScreenManager.stopBlockSimulation();
|
||||
}
|
||||
|
||||
try {
|
||||
GameState.setGameActive(true);
|
||||
|
||||
// Clear any previous game state but preserve what we need for initialization
|
||||
console.log("✅ Clearing previous game state (but preserving server data)...");
|
||||
GameState._exploredTiles = {};
|
||||
// Don't clear _visibleTiles yet - let initializeMap handle it properly
|
||||
GameState._map = {};
|
||||
GameState._items = {};
|
||||
GameState._monster = null;
|
||||
GameState._entrance = null;
|
||||
GameState._exit = null;
|
||||
GameState._treasure = null;
|
||||
|
||||
// Initialize player
|
||||
GameState.initializePlayer(playerData);
|
||||
|
||||
@@ -91,6 +118,7 @@ var Game = {
|
||||
}
|
||||
|
||||
this._drawGameScreen();
|
||||
console.log("Game started successfully and initial screen drawn.");
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
@@ -115,6 +143,7 @@ var Game = {
|
||||
|
||||
// Redraw the game screen if any relevant data changed
|
||||
if (needsRedraw) {
|
||||
console.log("Redrawing game screen due to game update.");
|
||||
this._drawGameScreen();
|
||||
}
|
||||
},
|
||||
@@ -150,6 +179,15 @@ var Game = {
|
||||
ScreenManager.stopWaitingScreen();
|
||||
},
|
||||
|
||||
// Display mode switching - delegate to DisplayManager
|
||||
switchToAsciiMode: function() {
|
||||
DisplayManager.switchToAsciiMode();
|
||||
},
|
||||
|
||||
switchToTileMode: function() {
|
||||
DisplayManager.switchToTileMode();
|
||||
},
|
||||
|
||||
// Player movement - delegate to GameState
|
||||
movePlayer: function(dx, dy) {
|
||||
const moved = GameState.movePlayer(dx, dy, this._screenWidth, this._screenHeight);
|
||||
@@ -204,6 +242,7 @@ var Game = {
|
||||
// Debug utility functions (keep as-is for compatibility)
|
||||
const GameDebug = {
|
||||
log: function(message) {
|
||||
console.log(message);
|
||||
this.updateDebugDisplay(message);
|
||||
this.sendToServer(message);
|
||||
},
|
||||
|
||||
+10
-4
@@ -161,9 +161,14 @@ var GameState = {
|
||||
},
|
||||
|
||||
computeFieldOfView: function() {
|
||||
// Initialize FOV
|
||||
// Initialize FOV with corrected wall detection callback
|
||||
var fov = new ROT.FOV.PreciseShadowcasting(function(x, y) {
|
||||
return (this._map[y] && this._map[y][x] !== undefined) ? (this._map[y][x] === 0) : false;
|
||||
// Return true if the tile is transparent (walkable), false if it blocks light
|
||||
if (!this._map[y] || this._map[y][x] === undefined) {
|
||||
return false; // Unknown tiles block light
|
||||
}
|
||||
// Tile type 0 = floor (transparent), 1 = wall (blocks light)
|
||||
return this._map[y][x] === 0;
|
||||
}.bind(this));
|
||||
|
||||
// If map was NOT built from initialVisibleTiles, compute FOV now
|
||||
@@ -177,7 +182,8 @@ var GameState = {
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.updateExploredTiles();
|
||||
// NOTE: Don't call updateExploredTiles() here on initial game load
|
||||
// Explored tiles should only be updated when player moves, not on first FOV computation
|
||||
console.log("Initial FOV computed/processed. Visible tiles count:", Object.keys(this._visibleTiles).reduce((acc, yKey) => acc + Object.keys(this._visibleTiles[yKey]).length, 0));
|
||||
},
|
||||
|
||||
@@ -418,7 +424,7 @@ var GameState = {
|
||||
needsRedraw = true;
|
||||
}
|
||||
|
||||
if (data.treasure) {
|
||||
if (data.treasure !== undefined) {
|
||||
this._treasure = data.treasure;
|
||||
needsRedraw = true;
|
||||
}
|
||||
|
||||
+61
-3
@@ -17,6 +17,7 @@ const InputHandler = {
|
||||
this.setupChatForm();
|
||||
this.setupFocusHandlers();
|
||||
this.setupKeyboardControls();
|
||||
this.setupClickHandlers();
|
||||
this.setupModeToggle();
|
||||
this._initialized = true;
|
||||
},
|
||||
@@ -150,13 +151,70 @@ const InputHandler = {
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'Enter' && document.activeElement !== $('#chatInput')[0]) {
|
||||
// If Enter is pressed and chat is not focused, focus chat
|
||||
$('#chatInput').focus();
|
||||
UI.updateFocusIndicator();
|
||||
// If Enter is pressed and chat is not focused
|
||||
if (typeof Game !== 'undefined' && !Game._gameActive) {
|
||||
// On welcome screen - start the game
|
||||
console.log("🔑 Enter key pressed on welcome screen - starting game");
|
||||
e.preventDefault();
|
||||
|
||||
const isDebugMode = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.protocol === 'file:';
|
||||
|
||||
socket.emit('chat message', 'enter');
|
||||
|
||||
if (isDebugMode || ScreenManager.canEnterGame()) {
|
||||
console.log("✅ Can enter immediately - showing waiting screen");
|
||||
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
|
||||
Game.drawWaitingScreen();
|
||||
}
|
||||
} else {
|
||||
console.log("⏳ Will be queued - showing queue message");
|
||||
$('#messages').append($('<li style="color: #ff0;">').text("* You have been added to the queue! Game will start after next block."));
|
||||
UI.scrollChat();
|
||||
}
|
||||
|
||||
// Add visual feedback to chat
|
||||
$('#messages').append($('<li style="color: #0f0;">').text("🔑 Game start requested..."));
|
||||
UI.scrollChat();
|
||||
} else {
|
||||
// Game is active or in other state - focus chat
|
||||
$('#chatInput').focus();
|
||||
UI.updateFocusIndicator();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupClickHandlers: function() {
|
||||
// Add click handling for the HTML START button
|
||||
$('#startButton').click(function(e) {
|
||||
console.log("🖱️ START button clicked - starting game");
|
||||
|
||||
// Trigger the same logic as typing 'enter'
|
||||
const isDebugMode = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.protocol === 'file:';
|
||||
|
||||
socket.emit('chat message', 'enter');
|
||||
|
||||
if (isDebugMode || ScreenManager.canEnterGame()) {
|
||||
console.log("✅ Can enter immediately - showing waiting screen");
|
||||
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
|
||||
Game.drawWaitingScreen();
|
||||
}
|
||||
} else {
|
||||
console.log("⏳ Will be queued - showing queue message");
|
||||
$('#messages').append($('<li style="color: #ff0;">').text("* You have been added to the queue! Game will start after next block."));
|
||||
UI.scrollChat();
|
||||
}
|
||||
|
||||
// Add visual feedback to chat
|
||||
$('#messages').append($('<li style="color: #0f0;">').text("🖱️ Game start requested..."));
|
||||
UI.scrollChat();
|
||||
});
|
||||
},
|
||||
|
||||
setupModeToggle: function() {
|
||||
var toggleButton = document.getElementById('toggle-mode');
|
||||
if (toggleButton) {
|
||||
|
||||
+15
-6
@@ -14,6 +14,9 @@ var RenderEngine = {
|
||||
drawGameScreen: function(gameState) {
|
||||
if (!DisplayManager.ensureDisplay()) return;
|
||||
|
||||
// Ensure we have a clean display first
|
||||
DisplayManager.clearDisplay();
|
||||
|
||||
const display = DisplayManager.getDisplay();
|
||||
const player = gameState.player;
|
||||
const monster = gameState.monster;
|
||||
@@ -21,8 +24,8 @@ var RenderEngine = {
|
||||
const entrance = gameState.entrance;
|
||||
const exit = gameState.exit;
|
||||
const treasure = gameState.treasure;
|
||||
const visibleTiles = gameState.visibleTiles;
|
||||
const exploredTiles = gameState.exploredTiles;
|
||||
const visibleTiles = gameState.visibleTiles || {};
|
||||
const exploredTiles = gameState.exploredTiles || {};
|
||||
const message = gameState.message;
|
||||
|
||||
const playerWX = player.x;
|
||||
@@ -51,7 +54,6 @@ var RenderEngine = {
|
||||
tileType = visibleTiles[wy][wx];
|
||||
baseFg = this.defaultFg;
|
||||
|
||||
// Fix: Handle all possible tile values explicitly and ensure they map to valid characters
|
||||
if (tileType === 1) {
|
||||
baseChar = '#'; // Wall
|
||||
} else if (tileType === 0) {
|
||||
@@ -60,11 +62,11 @@ var RenderEngine = {
|
||||
baseChar = "'"; // Default to floor for safety
|
||||
tileType = 0;
|
||||
}
|
||||
|
||||
} else if (exploredTiles[wy] && exploredTiles[wy][wx] !== undefined) {
|
||||
tileType = exploredTiles[wy][wx];
|
||||
baseFg = this.defaultFg;
|
||||
|
||||
// Fix: Handle all possible tile values explicitly for explored tiles too
|
||||
if (tileType === 1) {
|
||||
baseChar = '#'; // Wall
|
||||
} else if (tileType === 0) {
|
||||
@@ -82,8 +84,15 @@ var RenderEngine = {
|
||||
// Only add to stacks if we have a valid character
|
||||
if (baseChar && baseChar !== ' ') {
|
||||
charStack.push(baseChar);
|
||||
fgStack.push("transparent"); // Use transparent for all base tiles
|
||||
bgStack.push("transparent"); // Use transparent for all base tiles
|
||||
|
||||
// Use black transparent overlay for explored tiles, normal colors for currently visible tiles
|
||||
if (this.isVisible(wx, wy, visibleTiles)) {
|
||||
fgStack.push("transparent"); // Currently visible - use normal tile colors
|
||||
} else {
|
||||
fgStack.push("rgba(0, 0, 0, 0.6)"); // Explored but not visible - use dark transparent overlay
|
||||
}
|
||||
|
||||
bgStack.push("transparent"); // Always transparent background for terrain
|
||||
}
|
||||
|
||||
// Render items
|
||||
|
||||
+47
-24
@@ -3,6 +3,7 @@ var ScreenManager = {
|
||||
defaultFg: "#FFF",
|
||||
_currentBlockHeight: 0,
|
||||
_blockTimer: null,
|
||||
_updateInterval: null, // Track the update interval separately
|
||||
_waitingForBlock: false,
|
||||
|
||||
init: function(screenWidth, screenHeight) {
|
||||
@@ -10,6 +11,7 @@ var ScreenManager = {
|
||||
this._screenHeight = screenHeight;
|
||||
this._currentBlockHeight = 0;
|
||||
this._waitingForBlock = false;
|
||||
this._updateInterval = null; // Initialize update interval tracker
|
||||
},
|
||||
|
||||
drawCenteredText: function(y, text, color) {
|
||||
@@ -58,14 +60,24 @@ var ScreenManager = {
|
||||
y = this.drawCenteredText(y + 3, "Waiting for next block...", this.defaultFg);
|
||||
y = this.drawCenteredText(y + 1, `Block Height: ${this._currentBlockHeight}`, "#0f0");
|
||||
|
||||
if (this._waitingForBlock) {
|
||||
y = this.drawCenteredText(y + 2, "Mining in progress...", "#ff0");
|
||||
} else {
|
||||
y = this.drawCenteredText(y + 2, "Type 'ENTER' to queue for game", this.defaultFg);
|
||||
const isDebugMode = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.protocol === 'file:';
|
||||
|
||||
// Control HTML START button visibility
|
||||
const startButton = document.getElementById('startButton');
|
||||
if (startButton) {
|
||||
if (this._waitingForBlock && !isDebugMode) {
|
||||
startButton.style.display = 'none';
|
||||
y = this.drawCenteredText(y + 2, "Mining in progress...", "#ff0");
|
||||
} else {
|
||||
startButton.style.display = 'block';
|
||||
y = this.drawCenteredText(y + 2, "Press ENTER or click START to queue", "#0f0");
|
||||
}
|
||||
}
|
||||
|
||||
// Debug button (shown only in development)
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:') {
|
||||
if (isDebugMode) {
|
||||
this.drawCenteredText(y + 3, "[DEBUG: Press 'D' to start immediately]", "#666");
|
||||
}
|
||||
|
||||
@@ -79,43 +91,54 @@ var ScreenManager = {
|
||||
this._waitingForBlock = true;
|
||||
|
||||
// Update welcome screen every second during mining
|
||||
const updateInterval = setInterval(() => {
|
||||
if (this._waitingForBlock) {
|
||||
this._updateInterval = setInterval(() => {
|
||||
if (this._waitingForBlock && !GameState.isGameActive()) {
|
||||
this.drawWelcomeScreen();
|
||||
} else {
|
||||
clearInterval(updateInterval);
|
||||
if (this._updateInterval) {
|
||||
clearInterval(this._updateInterval);
|
||||
this._updateInterval = null;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Simulate new block every 30 seconds
|
||||
this._blockTimer = setInterval(() => {
|
||||
this._currentBlockHeight++;
|
||||
this._waitingForBlock = false;
|
||||
this.drawWelcomeScreen();
|
||||
|
||||
// Show block found message briefly
|
||||
setTimeout(() => {
|
||||
if (!DisplayManager.ensureDisplay()) return;
|
||||
let y = Math.floor(this._screenHeight / 2);
|
||||
this.drawCenteredText(y, "🎉 NEW BLOCK FOUND! 🎉", "#0f0");
|
||||
this.drawCenteredText(y + 1, "You may now enter the dungeon!", "#0f0");
|
||||
if (!GameState.isGameActive()) { // Only continue if game is not active
|
||||
this._currentBlockHeight++;
|
||||
this._waitingForBlock = false;
|
||||
this.drawWelcomeScreen();
|
||||
|
||||
// Allow entry for 5 seconds
|
||||
// Show block found message briefly
|
||||
setTimeout(() => {
|
||||
this._waitingForBlock = true;
|
||||
this.drawWelcomeScreen();
|
||||
}, 5000);
|
||||
}, 500);
|
||||
|
||||
if (!DisplayManager.ensureDisplay() || GameState.isGameActive()) return;
|
||||
let y = Math.floor(this._screenHeight / 2);
|
||||
this.drawCenteredText(y, "*** NEW BLOCK FOUND! ***", "#0f0");
|
||||
this.drawCenteredText(y + 1, "You may now enter the dungeon!", "#0f0");
|
||||
|
||||
// Allow entry for 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!GameState.isGameActive()) {
|
||||
this._waitingForBlock = true;
|
||||
this.drawWelcomeScreen();
|
||||
}
|
||||
}, 5000);
|
||||
}, 500);
|
||||
}
|
||||
}, 30000); // 30 second intervals
|
||||
},
|
||||
|
||||
// Stop block simulation
|
||||
stopBlockSimulation: function() {
|
||||
console.log("Stopping block simulation and clearing all intervals...");
|
||||
if (this._blockTimer) {
|
||||
clearInterval(this._blockTimer);
|
||||
this._blockTimer = null;
|
||||
}
|
||||
if (this._updateInterval) {
|
||||
clearInterval(this._updateInterval);
|
||||
this._updateInterval = null;
|
||||
}
|
||||
this._waitingForBlock = false;
|
||||
},
|
||||
|
||||
|
||||
@@ -46,11 +46,7 @@ const SocketHandlers = {
|
||||
userAgent: navigator.userAgent
|
||||
});
|
||||
|
||||
// Auto-start for testing (timeout issue has been fixed)
|
||||
setTimeout(() => {
|
||||
console.log("🚀 AUTO-STARTING GAME for faster testing");
|
||||
socket.emit('chat message', 'enter');
|
||||
}, 500);
|
||||
// Auto-start removed - user must manually start game
|
||||
},
|
||||
|
||||
onWelcome: function(msg) {
|
||||
@@ -79,6 +75,13 @@ const SocketHandlers = {
|
||||
|
||||
onGameStart: function(data) {
|
||||
console.log("🎮 Game start received with data:", data);
|
||||
console.log("🔍 DETAILED DATA CHECK:");
|
||||
console.log(" - Player:", data?.player);
|
||||
console.log(" - Map keys:", data?.map ? Object.keys(data.map).length : "NO MAP");
|
||||
console.log(" - Monster:", data?.monster);
|
||||
console.log(" - Items:", data?.items);
|
||||
console.log(" - VisibleTiles keys:", data?.visibleTiles ? Object.keys(data.visibleTiles).length : "NO VISIBLE TILES");
|
||||
|
||||
$('#messages').append($('<li class="game-start">').text("Starting game..."));
|
||||
|
||||
if (typeof Game !== 'undefined' && Game.stopWaitingScreen) {
|
||||
@@ -98,6 +101,7 @@ const SocketHandlers = {
|
||||
|
||||
if (!success) {
|
||||
$('#messages').append($('<li class="error">').text("Game start failed. Check console for details."));
|
||||
console.log("❌ GAME START FAILED - reverting to welcome screen");
|
||||
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen();
|
||||
} else {
|
||||
// Draw the initial game screen - the game draws itself after startGame()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
body {
|
||||
color: green;
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
# Wowngeon Code Architecture
|
||||
|
||||
## Overview
|
||||
The Wowngeon roguelike game has been refactored into a modular architecture with clear separation of concerns between frontend and backend components. Recent major improvements include fixing critical movement bugs, implementing auto-start functionality, and resolving duplicate event handler issues.
|
||||
|
||||
## Recent Major Bug Fixes and Improvements
|
||||
|
||||
### 🎯 Movement System Overhaul
|
||||
**Problem**: Players were skipping over tiles when moving through hallways, especially when approaching from certain directions.
|
||||
|
||||
**Root Cause**: Double event handler registration caused duplicate keydown events, leading to rapid-fire movement commands.
|
||||
|
||||
**Solution**:
|
||||
- Added `_initialized` flags to prevent multiple initializations of InputHandler and SocketHandlers
|
||||
- Implemented 100ms movement throttling on both client and server sides
|
||||
- Enhanced movement validation and debugging with comprehensive logging
|
||||
- Fixed module initialization order in `index.html`
|
||||
|
||||
**Result**: Players now move exactly one tile per keypress with no skipping.
|
||||
|
||||
### 🚀 Auto-Start System Implementation
|
||||
**Problem**: Game was hanging on "waiting for server..." screen instead of starting immediately for testing.
|
||||
|
||||
**Root Cause**: Missing `SocketHandlers.init()` call in `index.html` meant client couldn't receive `game_start` events.
|
||||
|
||||
**Solution**:
|
||||
- Added proper SocketHandlers initialization to HTML document ready function
|
||||
- Implemented auto-start that sends "enter" command 500ms after connection
|
||||
- Enhanced server-side auto-start to immediately begin games in DEBUG_MODE
|
||||
- Improved error handling and connection flow
|
||||
|
||||
**Result**: Games now start instantly for faster development and testing.
|
||||
|
||||
### 🔧 Code Architecture Improvements
|
||||
**Problem**: Monolithic code structure made debugging and maintenance difficult.
|
||||
|
||||
**Solution**:
|
||||
- Split frontend JavaScript into focused modules (ui.js, socketHandlers.js, inputHandler.js)
|
||||
- Added initialization protection to prevent duplicate module setup
|
||||
- Enhanced logging with emojis and timing information for easier debugging
|
||||
- Maintained backward compatibility while improving code organization
|
||||
|
||||
**Result**: Much cleaner, more maintainable codebase with improved debugging capabilities.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
### Core Classes
|
||||
|
||||
#### `Player` (/player.js)
|
||||
- Manages player state and position
|
||||
- Methods: `moveTo()`, `isAt()`, `getState()`, `setState()`, `reset()`
|
||||
- Properties: x, y, hasKey, hasTreasure
|
||||
|
||||
#### `Monster` (/monster.js)
|
||||
- Manages monster state and AI behavior
|
||||
- Methods: `moveTowardPlayer()`, `hasCaughtPlayer()`, `getState()`, `setState()`
|
||||
- AI Features: Smart pathfinding toward player, collision detection
|
||||
|
||||
#### `DungeonGenerator` (/dungeon.js)
|
||||
- Handles procedural dungeon generation using ROT.js
|
||||
- Methods: `generate()`, `getRandomRoomCenter()`, `isFloorTile()`, `isWallTile()`
|
||||
- Features: Room placement, entrance/exit/treasure positioning
|
||||
|
||||
#### `Game` (/game.js)
|
||||
- Main game logic coordinator
|
||||
- Uses Player, Monster, and DungeonGenerator classes
|
||||
- Manages game state, FOV calculation, and entity interactions
|
||||
- Methods: `movePlayer()`, `moveMonster()`, `getState()`, `updateFOV()`
|
||||
|
||||
### Supporting Files *(ENHANCED)*
|
||||
- `user.js` - User management and game instantiation with improved socket ID mapping
|
||||
- `index.js` - Server entry point with movement throttling, auto-start, and enhanced debugging
|
||||
- `dbcalls.js` - Database operations
|
||||
- `rpccalls.js` - Wownero blockchain integration
|
||||
|
||||
## Frontend Architecture
|
||||
|
||||
### Modular JavaScript Components
|
||||
|
||||
#### `ui.js`
|
||||
- UI utilities and visual helpers
|
||||
- Functions: `scrollChat()`, `updateBlockHeight()`, `updateFocusIndicator()`
|
||||
- Manages teletype animations and browser detection
|
||||
|
||||
#### `socketHandlers.js` *(MAJOR UPDATES)*
|
||||
- All WebSocket event handlers with initialization protection
|
||||
- Events: connect, game_start, game_update, game_over, etc.
|
||||
- **NEW**: Auto-start functionality for testing (sends "enter" after 500ms)
|
||||
- **NEW**: `_initialized` flag prevents duplicate handler registration
|
||||
- **FIXED**: Proper initialization order ensures events are received
|
||||
- Clean separation of network communication logic
|
||||
|
||||
#### `inputHandler.js` *(MAJOR UPDATES)*
|
||||
- Keyboard and form input management with movement throttling
|
||||
- **NEW**: 100ms movement cooldown prevents rapid-fire commands
|
||||
- **NEW**: `_pendingMove` system prevents movement overlap
|
||||
- **FIXED**: Duplicate event handler prevention
|
||||
- Handles WASD/arrow key movement, chat input, focus management
|
||||
- Enhanced debugging with movement timing logs
|
||||
|
||||
#### `ui.js`
|
||||
- UI utilities and visual helpers
|
||||
- Functions: `scrollChat()`, `updateBlockHeight()`, `updateFocusIndicator()`
|
||||
- Manages teletype animations and browser detection
|
||||
|
||||
#### `game.js` (frontend)
|
||||
- Core game rendering and client-side state management
|
||||
- ROT.js display integration, FOV rendering, entity drawing
|
||||
- Maintained original structure for compatibility
|
||||
|
||||
### HTML Structure *(UPDATED)*
|
||||
- `index.html` - Streamlined main page with proper module initialization order
|
||||
- **FIXED**: Added missing `SocketHandlers.init()` call to prevent auto-start hanging
|
||||
- **IMPROVED**: Clean document ready structure ensures proper initialization sequence
|
||||
- `index_original.html` - Backup of original monolithic structure
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### Backend Benefits
|
||||
1. **Modularity**: Clear separation of Player, Monster, and Dungeon logic
|
||||
2. **Maintainability**: Each class has single responsibility
|
||||
3. **Testability**: Individual components can be unit tested
|
||||
4. **Extensibility**: Easy to add new entity types or game mechanics
|
||||
5. **Movement Validation**: Server-side throttling and validation prevents cheating
|
||||
6. **Debug Capabilities**: Comprehensive logging for troubleshooting
|
||||
|
||||
### Frontend Benefits
|
||||
1. **Reduced Complexity**: 471-line index.html reduced to ~80 lines
|
||||
2. **Code Organization**: Related functionality grouped into logical modules
|
||||
3. **Reusability**: UI and input handlers can be reused/extended
|
||||
4. **Debugging**: Easier to locate and fix issues in specific modules
|
||||
5. **Movement Reliability**: Throttling prevents duplicate commands and tile skipping
|
||||
6. **Auto-Start Testing**: Instant game start for development and testing
|
||||
|
||||
## File Structure
|
||||
```
|
||||
/wowngeon/
|
||||
├── Backend Classes
|
||||
│ ├── player.js # Player entity and state
|
||||
│ ├── monster.js # Monster AI and behavior
|
||||
│ ├── dungeon.js # Procedural generation
|
||||
│ └── game.js # Main game coordinator
|
||||
├── Backend Support
|
||||
│ ├── index.js # Server and socket handling
|
||||
│ ├── user.js # User management
|
||||
│ └── ...other files
|
||||
└── Frontend
|
||||
├── index.html # Clean modular HTML
|
||||
└── js/
|
||||
├── game.js # Core game rendering
|
||||
├── ui.js # UI utilities
|
||||
├── socketHandlers.js # Network communication
|
||||
├── inputHandler.js # Input management
|
||||
└── ...libraries
|
||||
```
|
||||
|
||||
## Migration Summary *(UPDATED)*
|
||||
- ✅ Backend modularized into Player, Monster, DungeonGenerator classes
|
||||
- ✅ Frontend split into logical modules (UI, Socket, Input handlers)
|
||||
- ✅ **CRITICAL BUG FIXES**: Movement skipping and auto-start hanging resolved
|
||||
- ✅ **MOVEMENT SYSTEM**: Added throttling and duplicate prevention
|
||||
- ✅ **INITIALIZATION**: Fixed module loading order and event handler registration
|
||||
- ✅ **DEBUGGING**: Enhanced logging with emojis and timing information
|
||||
- ✅ Removed unused files and maintained full compatibility
|
||||
- ✅ All game mechanics verified working (movement, FOV, collision, treasure)
|
||||
|
||||
## Testing Verification *(COMPREHENSIVE)*
|
||||
- [x] Server starts without errors
|
||||
- [x] Game initialization works with proper module loading
|
||||
- [x] **NEW**: Auto-start functionality working (500ms delay → instant game start)
|
||||
- [x] **FIXED**: Player movement functional with no tile skipping
|
||||
- [x] **VERIFIED**: Single-tile movement precision confirmed via server logs
|
||||
- [x] Monster AI and collision detection operational
|
||||
- [x] Treasure collection working
|
||||
- [x] **ENHANCED**: Frontend modules load with initialization protection
|
||||
- [x] **CONFIRMED**: All socket events properly handled and received
|
||||
- [x] **TESTED**: Movement throttling prevents rapid-fire commands
|
||||
- [x] **VALIDATED**: Server-side movement validation working correctly
|
||||
|
||||
## Current Status: ✅ MAJOR BUGS RESOLVED
|
||||
The Wowngeon game is now in excellent working condition with all critical movement and initialization bugs fixed. The codebase is much more maintainable and follows modern software engineering practices while preserving all original game functionality.
|
||||
@@ -1,141 +0,0 @@
|
||||
# Wowngeon
|
||||
|
||||
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. 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 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
|
||||
- **Chat system**: Players can communicate in real-time during gameplay
|
||||
- **View other players' game states**: Spectate other players' dungeons in real-time
|
||||
|
||||
### Technology Stack:
|
||||
- Backend: Node.js with Express and Socket.io
|
||||
- Frontend: HTML5, CSS, and JavaScript with ROT.js for rendering
|
||||
- Blockchain: Wownero RPC integration for block monitoring and transactions
|
||||
|
||||
## File Structure
|
||||
|
||||
### Server Files
|
||||
|
||||
**index.js**
|
||||
The main server file that handles HTTP requests, WebSocket connections, game lifecycle management, and blockchain monitoring. Coordinates the entire application. Now includes movement throttling, auto-start functionality, and improved socket ID handling.
|
||||
|
||||
**game.js**
|
||||
Contains the Game class and logic for dungeon generation, player movement, monster AI, field-of-view calculations, and game state management. Enhanced with better debugging and state synchronization.
|
||||
|
||||
**user.js**
|
||||
Manages user registration, authentication, game association, and payment tracking. Maintains a registry of all active users with improved socket ID mapping.
|
||||
|
||||
**dbcalls.js**
|
||||
Handles database operations for storing user data, game history, and payment records.
|
||||
|
||||
**rpccalls.js**
|
||||
Contains functions for interacting with the Wownero blockchain via RPC, including checking block height and verifying transactions.
|
||||
|
||||
### Client Files
|
||||
|
||||
**html/index.html**
|
||||
The main HTML file served to clients, containing the structure for the game UI and chat interface. Now includes proper module initialization order to prevent duplicate event handlers.
|
||||
|
||||
**html/js/game.js**
|
||||
Client-side game logic handling rendering the game state using ROT.js and managing core game functionality. Maintained original structure for compatibility.
|
||||
|
||||
**html/js/socketHandlers.js** *(NEW MODULAR STRUCTURE)*
|
||||
Dedicated module for all WebSocket event handling including connect, game_start, game_update, and game_over events. Includes auto-start functionality and initialization protection.
|
||||
|
||||
**html/js/inputHandler.js** *(NEW MODULAR STRUCTURE)*
|
||||
Handles all keyboard input (WASD/arrow keys) and form submissions. Includes movement throttling (100ms cooldown) and duplicate event prevention.
|
||||
|
||||
**html/js/ui.js** *(NEW MODULAR STRUCTURE)*
|
||||
UI utilities including chat scrolling, block height updates, teletype animations, and browser detection.
|
||||
|
||||
**html/styles/stylesheet.css**
|
||||
Styling for the game interface, ensuring proper layout and visual appeal.
|
||||
|
||||
**html/js/rot.js**
|
||||
The Roguelike Toolkit library that provides dungeon generation, field-of-view calculations, and display capabilities.
|
||||
|
||||
## Recent Bug Fixes and Improvements
|
||||
|
||||
### ✅ Movement System Overhaul (Completed)
|
||||
- **Fixed movement skipping bug**: Players no longer skip over tiles when moving through hallways
|
||||
- **Implemented movement throttling**: Added 100ms cooldown on both client and server to prevent rapid-fire commands
|
||||
- **Resolved duplicate event handlers**: Fixed double initialization of InputHandler and SocketHandlers that caused movement issues
|
||||
- **Enhanced debugging**: Added comprehensive logging with emojis and timing information for movement events
|
||||
|
||||
### ✅ Auto-Start System (Completed)
|
||||
- **Instant game start**: Players automatically enter games without waiting for blockchain blocks (for testing)
|
||||
- **Fixed socket initialization**: Resolved missing SocketHandlers.init() call that prevented game_start events from being received
|
||||
- **Improved connection flow**: Streamlined the connection → registration → game start process
|
||||
- **Enhanced error handling**: Better feedback when auto-start fails or hangs
|
||||
|
||||
### ✅ Client-Side Architecture (Completed)
|
||||
- **Modular JavaScript structure**: Split monolithic code into focused modules (ui.js, socketHandlers.js, inputHandler.js)
|
||||
- **Initialization protection**: Added flags to prevent multiple module initializations
|
||||
- **Clean separation of concerns**: Network, input, and UI logic properly separated
|
||||
- **Backward compatibility**: Maintained all original game functionality while improving code organization
|
||||
|
||||
### ✅ Server-Side Improvements (Completed)
|
||||
- **Movement validation**: Server-side movement throttling and validation
|
||||
- **Socket ID mapping**: Proper handling of client-to-server socket ID relationships
|
||||
- **Disconnect cleanup**: Proper cleanup of player data when clients disconnect
|
||||
- **Debug mode enhancements**: Improved block simulation for faster testing
|
||||
|
||||
## To-Do List
|
||||
|
||||
### High Priority
|
||||
- [x] ~~Fix movement skipping bug~~ **COMPLETED**
|
||||
- [x] ~~Fix auto-start hanging issue~~ **COMPLETED**
|
||||
- [x] ~~Resolve duplicate event handler registration~~ **COMPLETED**
|
||||
- [ ] Implement proper payment verification through Wownero RPC
|
||||
- [ ] Add visual indicators for block time status (when you're in danger)
|
||||
- [ ] Add score tracking and leaderboard functionality
|
||||
|
||||
### Medium Priority
|
||||
- [ ] Improve monster AI with more sophisticated pathfinding
|
||||
- [ ] Add multiple dungeon layouts and difficulty levels
|
||||
- [ ] Implement different monster types with varying behaviors
|
||||
- [ ] Create persistent user accounts with game history
|
||||
- [ ] Add sound effects and basic animations
|
||||
- [ ] Optimize chat system for better message broadcasting
|
||||
|
||||
### Low Priority
|
||||
- [ ] Implement additional power-ups or items in the dungeon
|
||||
- [ ] Add achievements system
|
||||
- [ ] Create detailed statistics tracking
|
||||
- [ ] Optimize performance for mobile devices
|
||||
- [ ] Add difficulty settings with corresponding reward multipliers
|
||||
|
||||
## Running the Game
|
||||
|
||||
1. Install dependencies: `npm install`
|
||||
2. Configure Wownero RPC settings in `config.js` or environment variables
|
||||
3. Start the server: `node index.js`
|
||||
4. Access the game at `http://localhost:3000`
|
||||
|
||||
## Development Mode
|
||||
|
||||
The game includes a DEBUG_MODE setting that simulates block discovery every 30 seconds for easier testing. Set `DEBUG_MODE = true` in `index.js` to enable this feature.
|
||||
|
||||
### Auto-Start Testing Mode
|
||||
When DEBUG_MODE is enabled, the game automatically:
|
||||
- Connects players and sends "enter" command after 500ms
|
||||
- Starts games immediately without waiting for blockchain blocks
|
||||
- Provides instant feedback for testing movement and game mechanics
|
||||
- Includes comprehensive debug logging with emojis for easy tracking
|
||||
|
||||
### Debug Features
|
||||
- **Movement Logging**: Track every player movement with timing information
|
||||
- **Socket Communication**: Monitor all WebSocket events and data flow
|
||||
- **FOV Debugging**: Detailed field-of-view calculation logs
|
||||
- **Game State Tracking**: Real-time monitoring of player, monster, and treasure states
|
||||
@@ -102,8 +102,10 @@ class Game {
|
||||
}
|
||||
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}`);
|
||||
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
}
|
||||
@@ -135,6 +137,9 @@ class Game {
|
||||
|
||||
// Debug logging to verify entity data
|
||||
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!");
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
+22
-6
@@ -1,4 +1,5 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
var app = express();
|
||||
var http = require('http').Server(app);
|
||||
var io = require('socket.io')(http);
|
||||
@@ -38,9 +39,11 @@ 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'));
|
||||
// Use absolute path to html directory to ensure static files are served correctly
|
||||
const htmlPath = path.join(__dirname, '../../html');
|
||||
app.use(express.static(htmlPath));
|
||||
app.get('/', function(req, res) {
|
||||
res.sendFile('index.html', { root: '../../html' });
|
||||
res.sendFile('index.html', { root: htmlPath });
|
||||
});
|
||||
|
||||
// Replace your existing block check interval with this conditional version
|
||||
@@ -89,7 +92,7 @@ if (DEBUG_MODE) {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Start games for waiting players
|
||||
// Start games for waiting players (QUEUE ENTRY METHOD)
|
||||
function startGamesForWaiting(blockHeight) {
|
||||
console.log(`Starting games for ${WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`);
|
||||
|
||||
@@ -102,7 +105,9 @@ function startGamesForWaiting(blockHeight) {
|
||||
const currentUser = getUserBySocket(serverId);
|
||||
|
||||
if (currentUser) {
|
||||
// For queue entry, player gets 3 blocks starting from their entry block
|
||||
currentUser.blockRec = blockHeight;
|
||||
console.log(`🕒 QUEUE ENTRY: Player starts on block ${currentUser.blockRec}, will die after block ${currentUser.blockRec + 2}`);
|
||||
|
||||
try {
|
||||
const game = currentUser.startGame(80, 40);
|
||||
@@ -135,8 +140,9 @@ function checkGamesTimeout(currentHeight) {
|
||||
|
||||
// Player has 3 blocks to complete the dungeon
|
||||
// If they started on block 2, they must escape before block 5 is found
|
||||
// Note: Auto-entry players start timing from next block for consistent behavior
|
||||
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})`);
|
||||
console.log(`💀 GAME TIMEOUT for player ${socketId}: started timing on block ${user.blockRec}, current block ${currentHeight} (died after block ${user.blockRec + 2})`);
|
||||
|
||||
// Game has timed out - player didn't escape in time
|
||||
game.gameState = 'lost';
|
||||
@@ -180,6 +186,11 @@ io.on('connection', function(socket) {
|
||||
'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.');
|
||||
}
|
||||
// ====== GAME ENTRY HANDLERS ======
|
||||
// There are two ways to enter a game:
|
||||
// 1. QUEUE ENTRY: Type "enter" and wait for next block (players get 3 blocks from entry block)
|
||||
// 2. AUTO ENTRY: Type "enter" and start immediately in debug mode (players get 3 blocks from next block)
|
||||
|
||||
// Update the 'enter' command handler to start game immediately for testing
|
||||
if (msg.toLowerCase() == 'enter') {
|
||||
console.log(`Player ${socket.id} requested to enter the dungeon - STARTING IMMEDIATELY`);
|
||||
@@ -192,12 +203,17 @@ io.on('connection', function(socket) {
|
||||
|
||||
// Start game immediately for faster testing
|
||||
try {
|
||||
currentUser.blockRec = debugBlockHeight || lastBlockHeight || 1;
|
||||
// For auto-entry in debug mode, give player 3 blocks starting from NEXT block
|
||||
// This ensures consistent death timing regardless of entry method
|
||||
const currentBlock = debugBlockHeight || lastBlockHeight || 1;
|
||||
currentUser.blockRec = currentBlock + 1; // Start timing from NEXT block
|
||||
console.log(`🕒 AUTO-ENTRY: Player starts on block ${currentUser.blockRec}, will die after block ${currentUser.blockRec + 2}`);
|
||||
|
||||
const game = currentUser.startGame(80, 40);
|
||||
activeGames.set(socket.id, game);
|
||||
|
||||
const gameState = game.getState();
|
||||
gameState.blockHeight = debugBlockHeight || lastBlockHeight || 1;
|
||||
gameState.blockHeight = currentBlock;
|
||||
|
||||
console.log(`🎮 SENDING IMMEDIATE GAME_START to ${socket.id}`);
|
||||
io.to(socket.id).emit('game_start', gameState);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
const ROT = require('./rot.js');
|
||||
|
||||
/**
|
||||
* Dungeon generation utilities
|
||||
*/
|
||||
class DungeonGenerator {
|
||||
static generate(width, height) {
|
||||
// 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
|
||||
});
|
||||
|
||||
// Initialize the empty map with walls (1)
|
||||
const map = Array(height).fill().map(() => Array(width).fill(1));
|
||||
|
||||
// Fill in floors (0) using digger's callback
|
||||
digger.create((x, y, value) => {
|
||||
// value: 0 = floor, 1 = wall
|
||||
map[y][x] = value;
|
||||
});
|
||||
|
||||
// Get rooms that were created
|
||||
const rooms = digger.getRooms();
|
||||
|
||||
// Place entrance in the first room
|
||||
const entranceRoom = rooms[0];
|
||||
const entranceCenter = entranceRoom.getCenter();
|
||||
const entrance = [entranceCenter[0], entranceCenter[1]];
|
||||
|
||||
// Place exit in the last room
|
||||
const exitRoom = rooms[rooms.length - 1];
|
||||
const exitCenter = exitRoom.getCenter();
|
||||
const exit = [exitCenter[0], exitCenter[1]];
|
||||
|
||||
// Place treasure in a middle room (not first or last)
|
||||
let treasureRoom;
|
||||
if (rooms.length > 2) {
|
||||
treasureRoom = rooms[Math.floor(rooms.length / 2)];
|
||||
} else {
|
||||
// Fallback if we have fewer than 3 rooms
|
||||
treasureRoom = rooms[0];
|
||||
}
|
||||
const treasureCenter = treasureRoom.getCenter();
|
||||
// Place treasure slightly off center to make it more interesting
|
||||
const treasure = [
|
||||
treasureCenter[0] + Math.floor(Math.random() * 3) - 1,
|
||||
treasureCenter[1] + Math.floor(Math.random() * 3) - 1
|
||||
];
|
||||
|
||||
// Ensure the treasure is on a floor tile
|
||||
if (map[treasure[1]][treasure[0]] !== 0) {
|
||||
treasure[0] = treasureCenter[0];
|
||||
treasure[1] = treasureCenter[1];
|
||||
}
|
||||
|
||||
return {
|
||||
map: map,
|
||||
rooms: rooms,
|
||||
entrance: entrance,
|
||||
exit: exit,
|
||||
treasure: treasure
|
||||
};
|
||||
}
|
||||
|
||||
static getRandomRoomCenter(rooms, excludeFirst = false, excludeLast = false) {
|
||||
if (!rooms || rooms.length === 0) return null;
|
||||
|
||||
let availableRooms = [...rooms];
|
||||
if (excludeFirst && availableRooms.length > 1) {
|
||||
availableRooms = availableRooms.slice(1);
|
||||
}
|
||||
if (excludeLast && availableRooms.length > 1) {
|
||||
availableRooms = availableRooms.slice(0, -1);
|
||||
}
|
||||
|
||||
const randomRoom = availableRooms[Math.floor(Math.random() * availableRooms.length)];
|
||||
return randomRoom.getCenter();
|
||||
}
|
||||
|
||||
static isFloorTile(map, x, y) {
|
||||
return map[y] && map[y][x] === 0;
|
||||
}
|
||||
|
||||
static isWallTile(map, x, y) {
|
||||
return map[y] && map[y][x] === 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DungeonGenerator;
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
const ROT = require('./rot.js');
|
||||
const Player = require('./player.js');
|
||||
const Monster = require('./monster.js');
|
||||
const DungeonGenerator = require('./dungeon.js');
|
||||
|
||||
class Game {
|
||||
constructor(socketId, mapWidth, mapHeight) {
|
||||
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.dungeon = 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.generateDungeon();
|
||||
}
|
||||
|
||||
generateDungeon() {
|
||||
this.dungeon = DungeonGenerator.generate(this.width, this.height);
|
||||
|
||||
if (this.dungeon.entrance) {
|
||||
this.player.moveTo(this.dungeon.entrance[0], this.dungeon.entrance[1]);
|
||||
}
|
||||
|
||||
// Place monster far from entrance
|
||||
if (this.dungeon.rooms && this.dungeon.rooms.length > 2) {
|
||||
const monsterRoom = this.dungeon.rooms[Math.floor(this.dungeon.rooms.length * 0.7)];
|
||||
const center = monsterRoom.getCenter();
|
||||
this.monster.moveTo(center[0], center[1]);
|
||||
}
|
||||
|
||||
// Initialize FOV calculator
|
||||
this.fov = new ROT.FOV.PreciseShadowcasting(
|
||||
(x, y) => {
|
||||
// Return true if the tile is transparent (can see through it)
|
||||
return this.dungeon.map[y] && this.dungeon.map[y][x] === 0;
|
||||
}
|
||||
);
|
||||
|
||||
// Calculate initial FOV
|
||||
this.updateFOV();
|
||||
|
||||
// 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];
|
||||
} 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;
|
||||
|
||||
// Debug logging for movement validation
|
||||
console.log(`[DEBUG] Move attempt: from (${this.player.x},${this.player.y}) to (${newX},${newY}) with dx=${dx},dy=${dy}`);
|
||||
console.log(`[DEBUG] Map bounds: width=${this.width}, height=${this.height}`);
|
||||
console.log(`[DEBUG] Dungeon exists: ${!!this.dungeon}`);
|
||||
if (this.dungeon && this.dungeon.map) {
|
||||
console.log(`[DEBUG] Map row ${newY} exists: ${!!this.dungeon.map[newY]}`);
|
||||
if (this.dungeon.map[newY]) {
|
||||
console.log(`[DEBUG] Map cell [${newY}][${newX}] value: ${this.dungeon.map[newY][newX]} (0=floor, 1=wall)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.moveTo(newX, newY);
|
||||
console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`);
|
||||
|
||||
// Update FOV after moving
|
||||
this.updateFOV();
|
||||
|
||||
// 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;
|
||||
// Remove treasure from dungeon so it won't be sent in future updates
|
||||
this.dungeon.treasure = null;
|
||||
console.log(`Player ${this.socketId} collected treasure! hasTreasure: ${this.player.hasTreasure}`);
|
||||
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
}
|
||||
|
||||
return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
}
|
||||
|
||||
|
||||
console.log(`Player move to ${newX},${newY} for socket ${this.socketId} is invalid (wall or out of bounds).`);
|
||||
|
||||
// Debug: Show map around player
|
||||
console.log(`[DEBUG] Map around player position:`);
|
||||
for (let y = this.player.y - 2; y <= this.player.y + 2; y++) {
|
||||
let row = '';
|
||||
for (let x = this.player.x - 2; x <= this.player.x + 2; x++) {
|
||||
if (x === this.player.x && y === this.player.y) {
|
||||
row += '@'; // Player position
|
||||
} else if (x === newX && y === newY) {
|
||||
row += 'X'; // Attempted move position
|
||||
} else if (this.dungeon && this.dungeon.map[y] && this.dungeon.map[y][x] !== undefined) {
|
||||
row += this.dungeon.map[y][x] === 0 ? '.' : '#';
|
||||
} else {
|
||||
row += '?'; // Out of bounds or undefined
|
||||
}
|
||||
}
|
||||
console.log(`[DEBUG] Y=${y}: ${row}`);
|
||||
}
|
||||
|
||||
return { status: 'invalid' };
|
||||
}
|
||||
|
||||
moveMonster() {
|
||||
this.monster.moveTowardPlayer(this.player, this.dungeon);
|
||||
}
|
||||
|
||||
// Update the getState() method to include all entities with relative positions
|
||||
getState() {
|
||||
const state = {
|
||||
gameState: this.gameState,
|
||||
player: this.player.getState(), // Use Player's getState method
|
||||
monster: this.monster ? this.monster.getState() : null, // Use Monster's getState method
|
||||
visibleTiles: { ...this.visibleTiles }, // Send a copy of visible tiles
|
||||
entrance: this.dungeon ? this.dungeon.entrance : null,
|
||||
exit: this.dungeon ? this.dungeon.exit : null,
|
||||
treasure: this.dungeon ? this.dungeon.treasure : null,
|
||||
};
|
||||
|
||||
// Debug logging to verify entity data
|
||||
console.log("🎮 getState() sending entities - Monster:", state.monster, "Entrance:", state.entrance, "Exit:", state.exit, "Treasure:", state.treasure);
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if monster killed player
|
||||
function checkMonsterKill(player, monster) {
|
||||
// Use Monster's hasCaughtPlayer method
|
||||
const monsterInstance = new Monster(monster.x, monster.y);
|
||||
return monsterInstance.hasCaughtPlayer(player);
|
||||
}
|
||||
|
||||
module.exports = Game;
|
||||
module.exports.checkMonsterKill = checkMonsterKill;
|
||||
@@ -0,0 +1,99 @@
|
||||
const ROT = require('./rot.js');
|
||||
|
||||
/**
|
||||
* Monster class - Manages monster state and AI
|
||||
*/
|
||||
class Monster {
|
||||
constructor(x = 0, y = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// Move monster to new position
|
||||
moveTo(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// Check if monster is at given coordinates
|
||||
isAt(x, y) {
|
||||
return this.x === x && this.y === y;
|
||||
}
|
||||
|
||||
// Simple AI to move toward player
|
||||
moveTowardPlayer(player, dungeon) {
|
||||
if (!player || !dungeon || !dungeon.map) return;
|
||||
|
||||
const dx = Math.sign(player.x - this.x);
|
||||
const dy = Math.sign(player.y - this.y);
|
||||
|
||||
// Try horizontal move first
|
||||
if (dx !== 0) {
|
||||
const newX = this.x + dx;
|
||||
if (dungeon.map[this.y] && dungeon.map[this.y][newX] === 0) {
|
||||
this.x = newX;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try vertical move
|
||||
if (dy !== 0) {
|
||||
const newY = this.y + dy;
|
||||
if (dungeon.map[newY] && dungeon.map[newY][this.x] === 0) {
|
||||
this.y = newY;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try random direction if direct path is blocked
|
||||
const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
|
||||
const shuffledDirs = ROT.RNG.shuffle(dirs.slice());
|
||||
|
||||
for (const [dx, dy] of shuffledDirs) {
|
||||
const nx = this.x + dx;
|
||||
const ny = this.y + dy;
|
||||
if (dungeon.map[ny] && dungeon.map[ny][nx] === 0) {
|
||||
this.x = nx;
|
||||
this.y = ny;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if monster has caught the player
|
||||
hasCaughtPlayer(player) {
|
||||
if (!player) return false;
|
||||
|
||||
// Check for direct overlap - monster on same tile as player
|
||||
if (this.x === player.x && this.y === player.y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for adjacent tiles (optional - if you want 1-tile proximity kills)
|
||||
const dx = Math.abs(this.x - player.x);
|
||||
const dy = Math.abs(this.y - player.y);
|
||||
|
||||
// If monster is adjacent (one tile away in any direction)
|
||||
if ((dx <= 1 && dy === 0) || (dx === 0 && dy <= 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get monster state as object
|
||||
getState() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y
|
||||
};
|
||||
}
|
||||
|
||||
// Set monster state from object
|
||||
setState(state) {
|
||||
if (state.x !== undefined) this.x = state.x;
|
||||
if (state.y !== undefined) this.y = state.y;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monster;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Player class - Manages player state and actions
|
||||
*/
|
||||
class Player {
|
||||
constructor(x = 0, y = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.hasKey = false;
|
||||
this.hasTreasure = false;
|
||||
}
|
||||
|
||||
// Move player to new position
|
||||
moveTo(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// Check if player is at given coordinates
|
||||
isAt(x, y) {
|
||||
return this.x === x && this.y === y;
|
||||
}
|
||||
|
||||
// Reset player state
|
||||
reset() {
|
||||
this.hasKey = false;
|
||||
this.hasTreasure = false;
|
||||
}
|
||||
|
||||
// Get player state as object
|
||||
getState() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
hasKey: this.hasKey,
|
||||
hasTreasure: this.hasTreasure
|
||||
};
|
||||
}
|
||||
|
||||
// Set player state from object
|
||||
setState(state) {
|
||||
if (state.x !== undefined) this.x = state.x;
|
||||
if (state.y !== undefined) this.y = state.y;
|
||||
if (state.hasKey !== undefined) this.hasKey = state.hasKey;
|
||||
if (state.hasTreasure !== undefined) this.hasTreasure = state.hasTreasure;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Player;
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Wowngeon Fixes Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #000;
|
||||
color: #0f0;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-result {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.pass { border-color: #0f0; }
|
||||
.fail { border-color: #f00; }
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #666;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎮 Wowngeon Fixes Verification</h1>
|
||||
|
||||
<div class="test-result">
|
||||
<h2>🔧 Fix #1: START Button Visibility</h2>
|
||||
<p><strong>Issue:</strong> START button was invisible (cut off in game display)</p>
|
||||
<p><strong>Fix:</strong> Moved to HTML status area with proper styling</p>
|
||||
<p><strong>Test:</strong> Check if START button is visible in the status area below</p>
|
||||
</div>
|
||||
|
||||
<div class="test-result">
|
||||
<h2>⏱️ Fix #2: Auto-Entry Death Timing</h2>
|
||||
<p><strong>Issue:</strong> Players died after 2+ blocks instead of 1 block in debug mode</p>
|
||||
<p><strong>Fix:</strong> Set currentUser.blockRec = currentBlock + 1 for consistent timing</p>
|
||||
<p><strong>Test:</strong> Use "enter" command and check death timing logs in console</p>
|
||||
</div>
|
||||
|
||||
<h2>🎯 Live Game Test</h2>
|
||||
<iframe src="http://localhost:3000"></iframe>
|
||||
|
||||
<div class="test-result">
|
||||
<h3>📋 Test Instructions:</h3>
|
||||
<ol>
|
||||
<li><strong>START Button Test:</strong> Look for the green "🎮 START GAME" button in the status area (should be visible)</li>
|
||||
<li><strong>Death Timing Test:</strong> Click START or type "enter" in chat, then check browser console for timing logs</li>
|
||||
<li><strong>Expected Logs:</strong> Should see "🕒 AUTO-ENTRY: Player starts on block X, will die after block X+2"</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Test the START button visibility
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(function() {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe && iframe.contentDocument) {
|
||||
const startButton = iframe.contentDocument.getElementById('startButton');
|
||||
if (startButton) {
|
||||
console.log('✅ START button found in iframe');
|
||||
const styles = window.getComputedStyle(startButton);
|
||||
console.log('START button styles:', {
|
||||
display: styles.display,
|
||||
visibility: styles.visibility,
|
||||
background: styles.backgroundColor
|
||||
});
|
||||
} else {
|
||||
console.log('❌ START button not found in iframe');
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user