435 lines
9.8 KiB
JavaScript
435 lines
9.8 KiB
JavaScript
class Cell {
|
|
character;
|
|
characterColor;
|
|
backgroundColor;
|
|
|
|
constructor(char, charColor) {
|
|
this.character = char;
|
|
}
|
|
|
|
floor() { this.character = "."; }
|
|
upStair() { this.character = "<"; }
|
|
downStair() { this.character = ">"; }
|
|
cooridor() { this.character = "#"; }
|
|
}
|
|
|
|
class Point {
|
|
x = 0;
|
|
y = 0;
|
|
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
}
|
|
|
|
class Size {
|
|
width = 0;
|
|
height = 0;
|
|
|
|
constructor(width, height) {
|
|
this.width = width;
|
|
this.height = height;
|
|
}
|
|
}
|
|
|
|
class Rect {
|
|
origin = new Point();
|
|
size = new Size();
|
|
|
|
static fromCoordinates(x, y, w, h) {
|
|
return new Rect(new Point(x, y), new Size(w, h));
|
|
}
|
|
|
|
constructor(origin, size) {
|
|
this.origin = origin;
|
|
this.size = size;
|
|
}
|
|
|
|
get minX() { return this.origin.x; }
|
|
get minY() { return this.origin.y; }
|
|
get maxX() { return this.origin.x + this.size.width; }
|
|
get maxY() { return this.origin.y + this.size.height; }
|
|
get area() { return this.size.width * this.size.height; }
|
|
|
|
insetRect(inset) {
|
|
const twiceInset = 2 * inset;
|
|
|
|
return Rect.fromCoordinates(
|
|
this.origin.x + inset,
|
|
this.origin.y + inset,
|
|
this.size.width - twiceInset,
|
|
this.size.height - twiceInset
|
|
);
|
|
}
|
|
|
|
intersects(otherRect) {
|
|
if (otherRect.minX > this.maxX)
|
|
return false;
|
|
|
|
if (otherRect.maxX < this.minX)
|
|
return false;
|
|
|
|
if (otherRect.minY > this.maxY)
|
|
return false;
|
|
|
|
if (otherRect.maxY < this.minY)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class Grid {
|
|
#size;
|
|
#cells = [];
|
|
|
|
#rooms = [];
|
|
|
|
constructor(width, height) {
|
|
this.#size = new Size(width, height);
|
|
|
|
this.#cells = new Array(width * height);
|
|
for (let i = 0; i < this.#cells.length; i++) {
|
|
this.#cells[i] = new Cell(" ");
|
|
}
|
|
}
|
|
|
|
get width() {
|
|
return this.#size.width;
|
|
}
|
|
|
|
get height() {
|
|
return this.#size.height;
|
|
}
|
|
|
|
generate(p, roomGenerator) {
|
|
this.#generateRooms(p, roomGenerator);
|
|
this.#placeStairs();
|
|
}
|
|
|
|
#generateRooms(p, generator) {
|
|
this.#rooms = generator.rooms;
|
|
|
|
for (let room of this.#rooms) {
|
|
for (let y = room.minY; y <= room.maxY; y++) {
|
|
for (let x = room.minX; x <= room.maxX; x++) {
|
|
let charAtXY = room.charAt(x, y);
|
|
if (!charAtXY) {
|
|
continue;
|
|
}
|
|
|
|
let cell = this.cellAt(x, y);
|
|
cell.character = charAtXY;
|
|
cell.characterColor = p.color(255);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#placeStairs() {
|
|
const indexOfRoomWithUpStairs = randomInt(this.#rooms.length);
|
|
const coordinateOfUpStair = this.#rooms[indexOfRoomWithUpStairs].randomPoint();
|
|
this.cellAt(coordinateOfUpStair.x, coordinateOfUpStair.y).upStair();
|
|
|
|
while (true) {
|
|
let indexOfRoomForDownStair = randomInt(this.#rooms.length);
|
|
if (indexOfRoomForDownStair == indexOfRoomWithUpStairs) {
|
|
continue;
|
|
}
|
|
|
|
const coordinateOfDownStair = this.#rooms[indexOfRoomForDownStair].randomPoint();
|
|
this.cellAt(coordinateOfDownStair.x, coordinateOfDownStair.y).downStair();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
cellAt(x, y) {
|
|
return this.#cells[y * this.width + x];
|
|
}
|
|
}
|
|
|
|
class Room {
|
|
#rect;
|
|
|
|
constructor(rect) {
|
|
this.#rect = rect;
|
|
}
|
|
|
|
get minX() { return this.#rect.minX; }
|
|
get minY() { return this.#rect.minY; }
|
|
get maxX() { return this.#rect.maxX; }
|
|
get maxY() { return this.#rect.maxY; }
|
|
|
|
randomPoint() {
|
|
return new Point(
|
|
this.#rect.minX + 1 + randomInt(this.#rect.size.width - 2),
|
|
this.#rect.minY + 1 + randomInt(this.#rect.size.height - 2)
|
|
);
|
|
}
|
|
|
|
charAt(x, y) {
|
|
const minX = this.minX;
|
|
const minY = this.minY;
|
|
const maxX = this.maxX;
|
|
const maxY = this.maxY;
|
|
|
|
if (y == minY && x == minX) { return "┌"; }
|
|
if (y == minY && x == maxX) { return "┐"; }
|
|
if (y == maxY && x == minX) { return "└"; }
|
|
if (y == maxY && x == maxX) { return "┘"; }
|
|
if (y == minY || y == maxY) { return "─"; }
|
|
if (x == minX || x == maxX) { return "│"; }
|
|
|
|
if ((x > minX && x < maxX) && (y > minY && y < maxY)) {
|
|
return ".";
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class BSPNode {
|
|
static MIN_AREA = 36;
|
|
static MIN_ROOM_DIMENSION = 5;
|
|
|
|
x;
|
|
y;
|
|
width;
|
|
height;
|
|
leftChild;
|
|
rightChild;
|
|
room;
|
|
#done = false;
|
|
|
|
constructor(x, y, w, h) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.width = w;
|
|
this.height = h;
|
|
|
|
this.leftChild;
|
|
this.rightChild;
|
|
this.room;
|
|
}
|
|
|
|
get maxX() {
|
|
return this.x + this.width;
|
|
}
|
|
|
|
get maxY() {
|
|
return this.y + this.height;
|
|
}
|
|
|
|
get rooms() {
|
|
let rooms = new Array();
|
|
|
|
if (this.room) {
|
|
rooms.push(this.room);
|
|
return rooms;
|
|
}
|
|
|
|
if (this.leftChild) {
|
|
rooms = rooms.concat(this.leftChild.rooms);
|
|
}
|
|
|
|
if (this.rightChild) {
|
|
rooms = rooms.concat(this.rightChild.rooms);
|
|
}
|
|
|
|
return rooms;
|
|
}
|
|
|
|
divide() {
|
|
if (this.#done) {
|
|
return;
|
|
}
|
|
|
|
const area = this.width * this.height;
|
|
if (area < BSPNode.MIN_AREA) {
|
|
if (!this.#done && Math.random() > 0.8) {
|
|
this.#createRoom();
|
|
}
|
|
this.#done = true;
|
|
return;
|
|
}
|
|
|
|
if (area < 100 && Math.random() > 0.9) {
|
|
this.#createRoom();
|
|
this.#done = true;
|
|
return;
|
|
}
|
|
|
|
let shouldSplitVertically = Math.random() < 0.5;
|
|
console.debug("Should split vertically:", shouldSplitVertically);
|
|
|
|
if (shouldSplitVertically) {
|
|
let xCoordinateOfDivision = this.#randomIntBetween(this.x, this.maxX);
|
|
if (xCoordinateOfDivision) {
|
|
this.leftChild = new BSPNode(this.x, this.y, xCoordinateOfDivision - this.x, this.height);
|
|
this.rightChild = new BSPNode(xCoordinateOfDivision, this.y, this.maxX - xCoordinateOfDivision, this.height);
|
|
}
|
|
} else {
|
|
let yCoordinateOfDivision = this.#randomIntBetween(this.y, this.maxY);
|
|
if (yCoordinateOfDivision) {
|
|
this.leftChild = new BSPNode(this.x, this.y, this.width, yCoordinateOfDivision - this.y);
|
|
this.rightChild = new BSPNode(this.x, yCoordinateOfDivision, this.width, this.maxY - yCoordinateOfDivision);
|
|
}
|
|
}
|
|
|
|
if (!this.leftChild && !this.rightChild) {
|
|
if (!this.#done && Math.random() > 0.5) {
|
|
this.#createRoom();
|
|
}
|
|
this.#done = true;
|
|
}
|
|
}
|
|
|
|
divideRecursively() {
|
|
this.divide();
|
|
|
|
if (this.room) {
|
|
return;
|
|
}
|
|
|
|
if (this.leftChild) {
|
|
this.leftChild.divideRecursively();
|
|
}
|
|
if (this.rightChild) {
|
|
this.rightChild.divideRecursively();
|
|
}
|
|
}
|
|
|
|
#createRoom() {
|
|
this.room = new Room(this.x + 1, this.y + 1, this.width - 2, this.height - 2);
|
|
console.log("Created a room:", this.room);
|
|
}
|
|
|
|
#randomIntBetween(lower, upper) {
|
|
const randomMin = lower + BSPNode.MIN_ROOM_DIMENSION;
|
|
const randomMax = upper - BSPNode.MIN_ROOM_DIMENSION;
|
|
|
|
console.debug(`Random int between: ${lower} -> ${randomMin} and ${upper} -> ${randomMax}`);
|
|
|
|
const result = randomMax > randomMin ? randomMin + Math.floor(Math.random() * (randomMax - randomMin + 1)) : null;
|
|
console.debug("Result:", result);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class NRandomRoomsGenerator {
|
|
static MIN_ROOM_DIMENSION = 7;
|
|
static MAX_ROOM_DIMENSION = 12;
|
|
|
|
#numberOfRooms = 12;
|
|
#rooms;
|
|
|
|
#bounds;
|
|
|
|
constructor(bounds, numberOfRooms) {
|
|
if (bounds) {
|
|
this.#bounds = bounds;
|
|
}
|
|
|
|
if (numberOfRooms) {
|
|
this.#numberOfRooms = numberOfRooms;
|
|
}
|
|
}
|
|
|
|
get numberOfRooms() {
|
|
return this.#numberOfRooms;
|
|
}
|
|
|
|
get rooms() {
|
|
if (!this.#rooms) {
|
|
this.#generateRooms();
|
|
}
|
|
|
|
return this.#rooms;
|
|
}
|
|
|
|
#generateRooms() {
|
|
let rects = new Array();
|
|
|
|
const sizeRange = NRandomRoomsGenerator.MAX_ROOM_DIMENSION - NRandomRoomsGenerator.MIN_ROOM_DIMENSION;
|
|
|
|
while (rects.length < this.#numberOfRooms) {
|
|
const randomSize = new Size(
|
|
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange),
|
|
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange)
|
|
);
|
|
|
|
const randomOrigin = new Point(
|
|
this.#bounds.minX + randomInt(this.#bounds.maxX - randomSize.width),
|
|
this.#bounds.minY + randomInt(this.#bounds.maxY - randomSize.height)
|
|
);
|
|
|
|
const proposedRoomRect = new Rect(randomOrigin, randomSize);
|
|
|
|
// Check that the rect doesn't intersect with any other rects.
|
|
if (rects.some(e => e.intersects(proposedRoomRect))) {
|
|
continue;
|
|
}
|
|
|
|
rects.push(proposedRoomRect);
|
|
}
|
|
|
|
this.#rooms = rects.map(r => new Room(r.insetRect(1)));
|
|
}
|
|
}
|
|
|
|
function randomInt(n) {
|
|
max = Math.floor(n);
|
|
return Math.floor(Math.random() * max);
|
|
}
|
|
|
|
let grid;
|
|
|
|
new p5(p => {
|
|
const CELL_WIDTH = 20;
|
|
const CELL_HEIGHT = Math.floor(CELL_WIDTH * 1.3);
|
|
|
|
p.setup = () => {
|
|
const container = document.querySelector('#dungeon-background');
|
|
canvasWidth = parseFloat(getComputedStyle(container).width);
|
|
canvasHeight = parseFloat(getComputedStyle(container).height);
|
|
|
|
let canvas = p.createCanvas(canvasWidth, canvasHeight);
|
|
canvas.canvas.removeAttribute('style');
|
|
container.appendChild(canvas.canvas);
|
|
|
|
p.pixelDensity(p.displayDensity());
|
|
p.textFont("Courier");
|
|
|
|
const gridBounds = Rect.fromCoordinates(
|
|
0, 0,
|
|
Math.ceil(canvasWidth / CELL_WIDTH) - 1, Math.ceil(canvasHeight / CELL_HEIGHT) - 1
|
|
);
|
|
|
|
console.log(`Generating grid with size ${gridBounds.size.width} x ${gridBounds.size.height}`);
|
|
|
|
grid = new Grid(gridBounds.size.width, gridBounds.size.height);
|
|
grid.generate(p, new NRandomRoomsGenerator(gridBounds));
|
|
}
|
|
|
|
p.draw = () => {
|
|
p.textSize(CELL_HEIGHT);
|
|
|
|
for (let y = 0; y < grid.height; y++) {
|
|
for (let x = 0; x < grid.width; x++) {
|
|
let cell = grid.cellAt(x, y);
|
|
|
|
let fillColor = cell.characterColor ? cell.characterColor : p.color(255);
|
|
p.fill(fillColor);
|
|
|
|
p.textAlign(p.CENTER, p.CENTER);
|
|
p.text(cell.character, x * CELL_WIDTH, y * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT);
|
|
}
|
|
}
|
|
|
|
p.noLoop();
|
|
};
|
|
|
|
}, '#dungeon-background');
|