Use rot-js to create dungeon layout

This commit is contained in:
Peter Stockings
2026-01-05 14:58:07 +11:00
parent 45a1ed2253
commit 50a922ca85
2 changed files with 41 additions and 69 deletions

View File

@@ -85,7 +85,16 @@ describe('Combat Simulation', () => {
it("should attack if player is adjacent", () => { it("should attack if player is adjacent", () => {
const actors = new Map<EntityId, Actor>(); const actors = new Map<EntityId, Actor>();
const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 3 }, stats: createTestStats() } as any; 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(1, player);
actors.set(2, enemy); actors.set(2, enemy);

View File

@@ -2,6 +2,7 @@ import { type World, type EntityId, type RunState, type Tile, type Actor, type V
import { idx } from "./world-logic"; import { idx } from "./world-logic";
import { GAME_CONFIG } from "../../core/config/GameConfig"; import { GAME_CONFIG } from "../../core/config/GameConfig";
import { seededRandom } from "../../core/math"; import { seededRandom } from "../../core/math";
import * as ROT from "rot-js";
interface Room { interface Room {
x: number; 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 floor The floor number (affects difficulty)
* @param runState Player's persistent state across floors * @param runState Player's persistent state across floors
* @returns Generated world and player ID * @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 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 // Place player in first room
const firstRoom = rooms[0]; 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 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++) { // Generate the dungeon
const roomWidth = GAME_CONFIG.map.roomMinWidth + Math.floor(random() * (GAME_CONFIG.map.roomMaxWidth - GAME_CONFIG.map.roomMinWidth + 1)); dungeon.create((x, y, value) => {
const roomHeight = GAME_CONFIG.map.roomMinHeight + Math.floor(random() * (GAME_CONFIG.map.roomMaxHeight - GAME_CONFIG.map.roomMinHeight + 1)); if (value === 0) {
const roomX = 1 + Math.floor(random() * (width - roomWidth - 2)); // 0 = floor, 1 = wall
const roomY = 1 + Math.floor(random() * (height - roomHeight - 2)); tiles[y * width + x] = GAME_CONFIG.terrain.empty;
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);
} }
});
// 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; 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 { function decorate(width: number, height: number, tiles: Tile[], random: () => number, exit: Vec2): void {
const world = { width, height }; const world = { width, height };