rot-js
Version:
A roguelike toolkit in JavaScript
226 lines (225 loc) • 7.58 kB
JavaScript
import Dungeon from "./dungeon.js";
import { Room, Corridor } from "./features.js";
import RNG from "../rng.js";
import { DIRS } from "../constants.js";
const FEATURES = {
"room": Room,
"corridor": Corridor
};
/**
* Random dungeon generator using human-like digging patterns.
* Heavily based on Mike Anderson's ideas from the "Tyrant" algo, mentioned at
* http://roguebasin.com/index.php/Dungeon-Building_Algorithm
*/
export default class Digger extends Dungeon {
constructor(width, height, options = {}) {
super(width, height);
this._options = Object.assign({
roomWidth: [3, 9],
roomHeight: [3, 5],
corridorLength: [3, 10],
dugPercentage: 0.2,
timeLimit: 1000 /* we stop after this much time has passed (msec) */
}, options);
this._features = {
"room": 4,
"corridor": 4
};
this._map = [];
this._featureAttempts = 20; /* how many times do we try to create a feature on a suitable wall */
this._walls = {}; /* these are available for digging */
this._dug = 0;
this._digCallback = this._digCallback.bind(this);
this._canBeDugCallback = this._canBeDugCallback.bind(this);
this._isWallCallback = this._isWallCallback.bind(this);
this._priorityWallCallback = this._priorityWallCallback.bind(this);
}
create(callback) {
this._rooms = [];
this._corridors = [];
this._map = this._fillMap(1);
this._walls = {};
this._dug = 0;
let area = (this._width - 2) * (this._height - 2);
this._firstRoom();
let t1 = Date.now();
let priorityWalls;
do {
priorityWalls = 0;
let t2 = Date.now();
if (t2 - t1 > this._options.timeLimit) {
break;
}
/* find a good wall */
let wall = this._findWall();
if (!wall) {
break;
} /* no more walls */
let parts = wall.split(",");
let x = parseInt(parts[0]);
let y = parseInt(parts[1]);
let dir = this._getDiggingDirection(x, y);
if (!dir) {
continue;
} /* this wall is not suitable */
// console.log("wall", x, y);
/* try adding a feature */
let featureAttempts = 0;
do {
featureAttempts++;
if (this._tryFeature(x, y, dir[0], dir[1])) { /* feature added */
//if (this._rooms.length + this._corridors.length == 2) { this._rooms[0].addDoor(x, y); } /* first room oficially has doors */
this._removeSurroundingWalls(x, y);
this._removeSurroundingWalls(x - dir[0], y - dir[1]);
break;
}
} while (featureAttempts < this._featureAttempts);
for (let id in this._walls) {
if (this._walls[id] > 1) {
priorityWalls++;
}
}
} while (this._dug / area < this._options.dugPercentage || priorityWalls); /* fixme number of priority walls */
this._addDoors();
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]);
}
}
}
this._walls = {};
this._map = [];
return this;
}
_digCallback(x, y, value) {
if (value == 0 || value == 2) { /* empty */
this._map[x][y] = 0;
this._dug++;
}
else { /* wall */
this._walls[x + "," + y] = 1;
}
}
_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);
}
_priorityWallCallback(x, y) { this._walls[x + "," + y] = 2; }
;
_firstRoom() {
let cx = Math.floor(this._width / 2);
let cy = Math.floor(this._height / 2);
let room = Room.createRandomCenter(cx, cy, this._options);
this._rooms.push(room);
room.create(this._digCallback);
}
/**
* Get a suitable wall
*/
_findWall() {
let prio1 = [];
let prio2 = [];
for (let id in this._walls) {
let prio = this._walls[id];
if (prio == 2) {
prio2.push(id);
}
else {
prio1.push(id);
}
}
let arr = (prio2.length ? prio2 : prio1);
if (!arr.length) {
return null;
} /* no walls :/ */
let id = RNG.getItem(arr.sort()); // sort to make the order deterministic
delete this._walls[id];
return id;
}
/**
* Tries adding a feature
* @returns {bool} was this a successful try?
*/
_tryFeature(x, y, dx, dy) {
let featureName = RNG.getWeightedValue(this._features);
let ctor = FEATURES[featureName];
let feature = ctor.createRandomAt(x, y, dx, dy, this._options);
if (!feature.isValid(this._isWallCallback, this._canBeDugCallback)) {
// console.log("not valid");
// feature.debug();
return false;
}
feature.create(this._digCallback);
// feature.debug();
if (feature instanceof Room) {
this._rooms.push(feature);
}
if (feature instanceof Corridor) {
feature.createPriorityWalls(this._priorityWallCallback);
this._corridors.push(feature);
}
return true;
}
_removeSurroundingWalls(cx, cy) {
let deltas = DIRS[4];
for (let i = 0; i < deltas.length; i++) {
let delta = deltas[i];
let x = cx + delta[0];
let y = cy + delta[1];
delete this._walls[x + "," + y];
x = cx + 2 * delta[0];
y = cy + 2 * delta[1];
delete this._walls[x + "," + y];
}
}
/**
* Returns vector in "digging" direction, or false, if this does not exist (or is not unique)
*/
_getDiggingDirection(cx, cy) {
if (cx <= 0 || cy <= 0 || cx >= this._width - 1 || cy >= this._height - 1) {
return null;
}
let result = null;
let deltas = DIRS[4];
for (let i = 0; i < deltas.length; i++) {
let delta = deltas[i];
let x = cx + delta[0];
let y = cy + delta[1];
if (!this._map[x][y]) { /* there already is another empty neighbor! */
if (result) {
return null;
}
result = delta;
}
}
/* no empty neighbor */
if (!result) {
return null;
}
return [-result[0], -result[1]];
}
/**
* Find empty spaces surrounding rooms, and apply doors.
*/
_addDoors() {
let data = this._map;
function isWallCallback(x, y) {
return (data[x][y] == 1);
}
;
for (let i = 0; i < this._rooms.length; i++) {
let room = this._rooms[i];
room.clearDoors();
room.addDoors(isWallCallback);
}
}
}