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", () => {
const actors = new Map<EntityId, Actor>();
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);

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 { 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 };