UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

226 lines (225 loc) 7.58 kB
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); } } }