diff --git a/src/engine/__tests__/simulation.test.ts b/src/engine/__tests__/simulation.test.ts index 189aa31..834f158 100644 --- a/src/engine/__tests__/simulation.test.ts +++ b/src/engine/__tests__/simulation.test.ts @@ -85,7 +85,16 @@ describe('Combat Simulation', () => { it("should attack if player is adjacent", () => { const actors = new Map(); const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 3 }, stats: createTestStats() } as any; - const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats() } as any; + const enemy = { + id: 2, + category: "combatant", + isPlayer: false, + pos: { x: 3, y: 3 }, + stats: createTestStats(), + // Set AI state to pursuing so the enemy will attack when adjacent + aiState: "pursuing", + lastKnownPlayerPos: { x: 4, y: 3 } + } as any; actors.set(1, player); actors.set(2, enemy); diff --git a/src/engine/world/generator.ts b/src/engine/world/generator.ts index 9509396..979f55f 100644 --- a/src/engine/world/generator.ts +++ b/src/engine/world/generator.ts @@ -2,6 +2,7 @@ import { type World, type EntityId, type RunState, type Tile, type Actor, type V import { idx } from "./world-logic"; import { GAME_CONFIG } from "../../core/config/GameConfig"; import { seededRandom } from "../../core/math"; +import * as ROT from "rot-js"; interface Room { x: number; @@ -11,7 +12,7 @@ interface Room { } /** - * Generates a procedural dungeon world with rooms and corridors + * Generates a procedural dungeon world with rooms and corridors using rot-js Uniform algorithm * @param floor The floor number (affects difficulty) * @param runState Player's persistent state across floors * @returns Generated world and player ID @@ -23,7 +24,10 @@ export function generateWorld(floor: number, runState: RunState): { world: World const random = seededRandom(floor * 12345); - const rooms = generateRooms(width, height, tiles, random); + // Set ROT's RNG seed for consistent dungeon generation + ROT.RNG.setSeed(floor * 12345); + + const rooms = generateRooms(width, height, tiles); // Place player in first room const firstRoom = rooms[0]; @@ -62,80 +66,39 @@ export function generateWorld(floor: number, runState: RunState): { world: World } -function generateRooms(width: number, height: number, tiles: Tile[], random: () => number): Room[] { +function generateRooms(width: number, height: number, tiles: Tile[]): Room[] { const rooms: Room[] = []; - const numRooms = GAME_CONFIG.map.minRooms + Math.floor(random() * (GAME_CONFIG.map.maxRooms - GAME_CONFIG.map.minRooms + 1)); - const fakeWorldForIdx = { width, height }; + // Create rot-js Uniform dungeon generator + const dungeon = new ROT.Map.Uniform(width, height, { + roomWidth: [GAME_CONFIG.map.roomMinWidth, GAME_CONFIG.map.roomMaxWidth], + roomHeight: [GAME_CONFIG.map.roomMinHeight, GAME_CONFIG.map.roomMaxHeight], + roomDugPercentage: 0.3, // 30% of the map should be rooms/corridors + }); - for (let i = 0; i < numRooms; i++) { - const roomWidth = GAME_CONFIG.map.roomMinWidth + Math.floor(random() * (GAME_CONFIG.map.roomMaxWidth - GAME_CONFIG.map.roomMinWidth + 1)); - const roomHeight = GAME_CONFIG.map.roomMinHeight + Math.floor(random() * (GAME_CONFIG.map.roomMaxHeight - GAME_CONFIG.map.roomMinHeight + 1)); - const roomX = 1 + Math.floor(random() * (width - roomWidth - 2)); - const roomY = 1 + Math.floor(random() * (height - roomHeight - 2)); - - const newRoom: Room = { x: roomX, y: roomY, width: roomWidth, height: roomHeight }; - - if (!doesOverlap(newRoom, rooms)) { - carveRoom(newRoom, tiles, fakeWorldForIdx); - - if (rooms.length > 0) { - carveCorridor(rooms[rooms.length - 1], newRoom, tiles, fakeWorldForIdx, random); - } - - rooms.push(newRoom); + // Generate the dungeon + dungeon.create((x, y, value) => { + if (value === 0) { + // 0 = floor, 1 = wall + tiles[y * width + x] = GAME_CONFIG.terrain.empty; } + }); + + // Extract room information from the generated dungeon + const roomData = (dungeon as any).getRooms(); + + for (const room of roomData) { + rooms.push({ + x: room.getLeft(), + y: room.getTop(), + width: room.getRight() - room.getLeft() + 1, + height: room.getBottom() - room.getTop() + 1 + }); } + return rooms; } -function doesOverlap(newRoom: Room, rooms: Room[]): boolean { - for (const room of rooms) { - if ( - newRoom.x < room.x + room.width + 1 && - newRoom.x + newRoom.width + 1 > room.x && - newRoom.y < room.y + room.height + 1 && - newRoom.y + newRoom.height + 1 > room.y - ) { - return true; - } - } - return false; -} - -function carveRoom(room: Room, tiles: Tile[], world: any): void { - for (let x = room.x; x < room.x + room.width; x++) { - for (let y = room.y; y < room.y + room.height; y++) { - tiles[idx(world, x, y)] = GAME_CONFIG.terrain.empty; - } - } -} - -function carveCorridor(room1: Room, room2: Room, tiles: Tile[], world: any, random: () => number): void { - const x1 = Math.floor(room1.x + room1.width / 2); - const y1 = Math.floor(room1.y + room1.height / 2); - const x2 = Math.floor(room2.x + room2.width / 2); - const y2 = Math.floor(room2.y + room2.height / 2); - - if (random() < 0.5) { - // Horizontal then vertical - for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) { - tiles[idx(world, x, y1)] = GAME_CONFIG.terrain.empty; - } - for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) { - tiles[idx(world, x2, y)] = GAME_CONFIG.terrain.empty; - } - } else { - // Vertical then horizontal - for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) { - tiles[idx(world, x1, y)] = GAME_CONFIG.terrain.empty; - } - for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) { - tiles[idx(world, x, y2)] = GAME_CONFIG.terrain.empty; - } - } -} - function decorate(width: number, height: number, tiles: Tile[], random: () => number, exit: Vec2): void { const world = { width, height };