diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..c5de8f7 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,117 @@ +# 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. + +## 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 +- `user.js` - User management and game instantiation +- `index.js` - Server entry point and socket handling +- `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` +- All WebSocket event handlers +- Events: connect, game_start, game_update, game_over, etc. +- Clean separation of network communication logic + +#### `inputHandler.js` +- Keyboard and form input management +- Handles WASD/arrow key movement, chat input, focus management +- Event delegation for game controls + +#### `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 +- `index.html` - Streamlined main page that loads modular components +- `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 + +### 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 + +## 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 +- ✅ Backend modularized into Player, Monster, DungeonGenerator classes +- ✅ Frontend split into logical modules (UI, Socket, Input handlers) +- ✅ Removed unused files (game_refactored.js, displayManager.js, etc.) +- ✅ Maintained full game functionality and compatibility +- ✅ All game mechanics verified working (movement, FOV, collision, treasure) + +## Testing Verification +- [x] Server starts without errors +- [x] Game initialization works +- [x] Player movement functional +- [x] Monster AI and collision detection operational +- [x] Treasure collection working +- [x] Frontend modules load correctly +- [x] All socket events properly handled + +The refactored codebase is now much more maintainable and follows modern software engineering practices while preserving all original game functionality. diff --git a/dungeon.js b/dungeon.js new file mode 100644 index 0000000..493af29 --- /dev/null +++ b/dungeon.js @@ -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; diff --git a/game.js b/game.js index 02df8d8..4237c17 100644 --- a/game.js +++ b/game.js @@ -1,66 +1,7 @@ const ROT = require('./rot.js'); - -// Define the missing generateDungeon function -function generateDungeon(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 - }; -} +const Player = require('./player.js'); +const Monster = require('./monster.js'); +const DungeonGenerator = require('./dungeon.js'); class Game { constructor(socketId, mapWidth, mapHeight) { @@ -70,8 +11,8 @@ class Game { this.gameState = 'waiting'; // waiting, active, won, lost this.startBlock = null; this.dungeon = null; - this.player = { x: 0, y: 0, hasKey: false, hasTreasure: false }; - this.monster = { x: 0, y: 0 }; + 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 @@ -79,20 +20,17 @@ class Game { } generateDungeon() { - const dungeon = generateDungeon(this.width, this.height); - this.dungeon = dungeon; + this.dungeon = DungeonGenerator.generate(this.width, this.height); - if (dungeon.entrance) { - this.player.x = dungeon.entrance[0]; - this.player.y = dungeon.entrance[1]; + if (this.dungeon.entrance) { + this.player.moveTo(this.dungeon.entrance[0], this.dungeon.entrance[1]); } // Place monster far from entrance - if (dungeon.rooms && dungeon.rooms.length > 2) { - const monsterRoom = dungeon.rooms[Math.floor(dungeon.rooms.length * 0.7)]; + 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.x = center[0]; - this.monster.y = center[1]; + this.monster.moveTo(center[0], center[1]); } // Initialize FOV calculator @@ -142,28 +80,27 @@ class Game { this.dungeon.map[newY][newX] !== undefined && this.dungeon.map[newY][newX] === 0) { // 0 is floor - this.player.x = newX; - this.player.y = newY; + 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 && newX === this.dungeon.exit[0] && newY === this.dungeon.exit[1]) { + if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], 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 }; + this.gameState = 'won'; + return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles }; } - if (this.dungeon.treasure && newX === this.dungeon.treasure[0] && newY === this.dungeon.treasure[1] && !this.player.hasTreasure) { + 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, visibleTiles: this.visibleTiles }; + return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles }; } - return { status: 'moved', player: this.player, 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).`); @@ -171,55 +108,15 @@ class Game { } moveMonster() { - // Simple monster AI to move toward player - const dx = Math.sign(this.player.x - this.monster.x); - const dy = Math.sign(this.player.y - this.monster.y); - - // Try horizontal move - if (dx !== 0) { - if (this.dungeon.map[this.monster.y][this.monster.x + dx] === 0) { - this.monster.x += dx; - return; - } - } - - // Try vertical move - if (dy !== 0) { - if (this.dungeon.map[this.monster.y + dy][this.monster.x] === 0) { - this.monster.y += dy; - 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.monster.x + dx; - const ny = this.monster.y + dy; - if (this.dungeon.map[ny] && this.dungeon.map[ny][nx] === 0) { - this.monster.x = nx; - this.monster.y = ny; - break; - } - } + this.monster.moveTowardPlayer(this.player, this.dungeon); } // 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; // Not needed if sending absolute visibleTiles - // const playerY = this.player.y; // Not needed if sending absolute visibleTiles - - // 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. - const state = { gameState: this.gameState, - player: { ...this.player }, // Send a copy of player object - monster: this.monster ? { ...this.monster } : null, // Send a copy if monster exists + 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, @@ -235,21 +132,9 @@ class Game { // Check if monster killed player function checkMonsterKill(player, monster) { - // Check for direct overlap - monster on same tile as player - if (monster.x === player.x && monster.y === player.y) { - return true; // Monster killed player - } - - // Check for adjacent tiles (optional - if you want 1-tile proximity kills) - const dx = Math.abs(monster.x - player.x); - const dy = Math.abs(monster.y - player.y); - - // If monster is adjacent (one tile away in any direction) - if ((dx <= 1 && dy === 0) || (dx === 0 && dy <= 1)) { - return true; // Monster killed player - } - - return false; // Player is safe + // Use Monster's hasCaughtPlayer method + const monsterInstance = new Monster(monster.x, monster.y); + return monsterInstance.hasCaughtPlayer(player); } module.exports = Game; diff --git a/html/index.html b/html/index.html index 55aecf5..4b43d16 100644 --- a/html/index.html +++ b/html/index.html @@ -5,30 +5,25 @@ - + - + - + - + - + - - -