Use rot-js to create dungeon layout
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
// Extract room information from the generated dungeon
|
||||||
|
const roomData = (dungeon as any).getRooms();
|
||||||
|
|
||||||
if (!doesOverlap(newRoom, rooms)) {
|
for (const room of roomData) {
|
||||||
carveRoom(newRoom, tiles, fakeWorldForIdx);
|
rooms.push({
|
||||||
|
x: room.getLeft(),
|
||||||
if (rooms.length > 0) {
|
y: room.getTop(),
|
||||||
carveCorridor(rooms[rooms.length - 1], newRoom, tiles, fakeWorldForIdx, random);
|
width: room.getRight() - room.getLeft() + 1,
|
||||||
|
height: room.getBottom() - room.getTop() + 1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
rooms.push(newRoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user