rot-js
Version:
A roguelike toolkit in JavaScript
349 lines (348 loc) • 11.9 kB
JavaScript
import Dungeon from "./dungeon.js";
import { Room, Corridor } from "./features.js";
import RNG from "../rng.js";
;
/**
* @class Dungeon generator which tries to fill the space evenly. Generates independent rooms and tries to connect them.
* @augments ROT.Map.Dungeon
*/
export default class Uniform extends Dungeon {
constructor(width, height, options) {
super(width, height);
this._options = {
roomWidth: [3, 9],
roomHeight: [3, 5],
roomDugPercentage: 0.1,
timeLimit: 1000 /* we stop after this much time has passed (msec) */
};
Object.assign(this._options, options);
this._map = [];
this._dug = 0;
this._roomAttempts = 20; /* new room is created N-times until is considered as impossible to generate */
this._corridorAttempts = 20; /* corridors are tried N-times until the level is considered as impossible to connect */
this._connected = []; /* list of already connected rooms */
this._unconnected = []; /* list of remaining unconnected rooms */
this._digCallback = this._digCallback.bind(this);
this._canBeDugCallback = this._canBeDugCallback.bind(this);
this._isWallCallback = this._isWallCallback.bind(this);
}
/**
* Create a map. If the time limit has been hit, returns null.
* @see ROT.Map#create
*/
create(callback) {
let t1 = Date.now();
while (1) {
let t2 = Date.now();
if (t2 - t1 > this._options.timeLimit) {
return null;
} /* time limit! */
this._map = this._fillMap(1);
this._dug = 0;
this._rooms = [];
this._unconnected = [];
this._generateRooms();
if (this._rooms.length < 2) {
continue;
}
if (this._generateCorridors()) {
break;
}
}
if (callback) {
for (let i = 0; i < this._width; i++) {
for (let j = 0; j < this._height; j++) {
callback(i, j, this._map[i][j]);
}
}
}
return this;
}
/**
* Generates a suitable amount of rooms
*/
_generateRooms() {
let w = this._width - 2;
let h = this._height - 2;
let room;
do {
room = this._generateRoom();
if (this._dug / (w * h) > this._options.roomDugPercentage) {
break;
} /* achieved requested amount of free space */
} while (room);
/* either enough rooms, or not able to generate more of them :) */
}
/**
* Try to generate one room
*/
_generateRoom() {
let count = 0;
while (count < this._roomAttempts) {
count++;
let room = Room.createRandom(this._width, this._height, this._options);
if (!room.isValid(this._isWallCallback, this._canBeDugCallback)) {
continue;
}
room.create(this._digCallback);
this._rooms.push(room);
return room;
}
/* no room was generated in a given number of attempts */
return null;
}
/**
* Generates connectors beween rooms
* @returns {bool} success Was this attempt successfull?
*/
_generateCorridors() {
let cnt = 0;
while (cnt < this._corridorAttempts) {
cnt++;
this._corridors = [];
/* dig rooms into a clear map */
this._map = this._fillMap(1);
for (let i = 0; i < this._rooms.length; i++) {
let room = this._rooms[i];
room.clearDoors();
room.create(this._digCallback);
}
this._unconnected = RNG.shuffle(this._rooms.slice());
this._connected = [];
if (this._unconnected.length) {
this._connected.push(this._unconnected.pop());
} /* first one is always connected */
while (1) {
/* 1. pick random connected room */
let connected = RNG.getItem(this._connected);
if (!connected) {
break;
}
/* 2. find closest unconnected */
let room1 = this._closestRoom(this._unconnected, connected);
if (!room1) {
break;
}
/* 3. connect it to closest connected */
let room2 = this._closestRoom(this._connected, room1);
if (!room2) {
break;
}
let ok = this._connectRooms(room1, room2);
if (!ok) {
break;
} /* stop connecting, re-shuffle */
if (!this._unconnected.length) {
return true;
} /* done; no rooms remain */
}
}
return false;
}
;
/**
* For a given room, find the closest one from the list
*/
_closestRoom(rooms, room) {
let dist = Infinity;
let center = room.getCenter();
let result = null;
for (let i = 0; i < rooms.length; i++) {
let r = rooms[i];
let c = r.getCenter();
let dx = c[0] - center[0];
let dy = c[1] - center[1];
let d = dx * dx + dy * dy;
if (d < dist) {
dist = d;
result = r;
}
}
return result;
}
_connectRooms(room1, room2) {
/*
room1.debug();
room2.debug();
*/
let center1 = room1.getCenter();
let center2 = room2.getCenter();
let diffX = center2[0] - center1[0];
let diffY = center2[1] - center1[1];
let start;
let end;
let dirIndex1, dirIndex2, min, max, index;
if (Math.abs(diffX) < Math.abs(diffY)) { /* first try connecting north-south walls */
dirIndex1 = (diffY > 0 ? 2 : 0);
dirIndex2 = (dirIndex1 + 2) % 4;
min = room2.getLeft();
max = room2.getRight();
index = 0;
}
else { /* first try connecting east-west walls */
dirIndex1 = (diffX > 0 ? 1 : 3);
dirIndex2 = (dirIndex1 + 2) % 4;
min = room2.getTop();
max = room2.getBottom();
index = 1;
}
start = this._placeInWall(room1, dirIndex1); /* corridor will start here */
if (!start) {
return false;
}
if (start[index] >= min && start[index] <= max) { /* possible to connect with straight line (I-like) */
end = start.slice();
let value = 0;
switch (dirIndex2) {
case 0:
value = room2.getTop() - 1;
break;
case 1:
value = room2.getRight() + 1;
break;
case 2:
value = room2.getBottom() + 1;
break;
case 3:
value = room2.getLeft() - 1;
break;
}
end[(index + 1) % 2] = value;
this._digLine([start, end]);
}
else if (start[index] < min - 1 || start[index] > max + 1) { /* need to switch target wall (L-like) */
let diff = start[index] - center2[index];
let rotation = 0;
switch (dirIndex2) {
case 0:
case 1:
rotation = (diff < 0 ? 3 : 1);
break;
case 2:
case 3:
rotation = (diff < 0 ? 1 : 3);
break;
}
dirIndex2 = (dirIndex2 + rotation) % 4;
end = this._placeInWall(room2, dirIndex2);
if (!end) {
return false;
}
let mid = [0, 0];
mid[index] = start[index];
let index2 = (index + 1) % 2;
mid[index2] = end[index2];
this._digLine([start, mid, end]);
}
else { /* use current wall pair, but adjust the line in the middle (S-like) */
let index2 = (index + 1) % 2;
end = this._placeInWall(room2, dirIndex2);
if (!end) {
return false;
}
let mid = Math.round((end[index2] + start[index2]) / 2);
let mid1 = [0, 0];
let mid2 = [0, 0];
mid1[index] = start[index];
mid1[index2] = mid;
mid2[index] = end[index];
mid2[index2] = mid;
this._digLine([start, mid1, mid2, end]);
}
room1.addDoor(start[0], start[1]);
room2.addDoor(end[0], end[1]);
index = this._unconnected.indexOf(room1);
if (index != -1) {
this._unconnected.splice(index, 1);
this._connected.push(room1);
}
index = this._unconnected.indexOf(room2);
if (index != -1) {
this._unconnected.splice(index, 1);
this._connected.push(room2);
}
return true;
}
_placeInWall(room, dirIndex) {
let start = [0, 0];
let dir = [0, 0];
let length = 0;
switch (dirIndex) {
case 0:
dir = [1, 0];
start = [room.getLeft(), room.getTop() - 1];
length = room.getRight() - room.getLeft() + 1;
break;
case 1:
dir = [0, 1];
start = [room.getRight() + 1, room.getTop()];
length = room.getBottom() - room.getTop() + 1;
break;
case 2:
dir = [1, 0];
start = [room.getLeft(), room.getBottom() + 1];
length = room.getRight() - room.getLeft() + 1;
break;
case 3:
dir = [0, 1];
start = [room.getLeft() - 1, room.getTop()];
length = room.getBottom() - room.getTop() + 1;
break;
}
let avail = [];
let lastBadIndex = -2;
for (let i = 0; i < length; i++) {
let x = start[0] + i * dir[0];
let y = start[1] + i * dir[1];
avail.push(null);
let isWall = (this._map[x][y] == 1);
if (isWall) {
if (lastBadIndex != i - 1) {
avail[i] = [x, y];
}
}
else {
lastBadIndex = i;
if (i) {
avail[i - 1] = null;
}
}
}
for (let i = avail.length - 1; i >= 0; i--) {
if (!avail[i]) {
avail.splice(i, 1);
}
}
return (avail.length ? RNG.getItem(avail) : null);
}
/**
* Dig a polyline.
*/
_digLine(points) {
for (let i = 1; i < points.length; i++) {
let start = points[i - 1];
let end = points[i];
let corridor = new Corridor(start[0], start[1], end[0], end[1]);
corridor.create(this._digCallback);
this._corridors.push(corridor);
}
}
_digCallback(x, y, value) {
this._map[x][y] = value;
if (value == 0) {
this._dug++;
}
}
_isWallCallback(x, y) {
if (x < 0 || y < 0 || x >= this._width || y >= this._height) {
return false;
}
return (this._map[x][y] == 1);
}
_canBeDugCallback(x, y) {
if (x < 1 || y < 1 || x + 1 >= this._width || y + 1 >= this._height) {
return false;
}
return (this._map[x][y] == 1);
}
}