UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

349 lines (348 loc) 11.9 kB
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); } }