Source: utils/dungeon.js

import { clone } from "./clone";
import { areTwoRectsCollided } from "./collisionDetection";
import { getRandomFrom } from "./getRandomFrom";
import { coordToVector, vectorToCoord } from "./gridMapping";

/**
 * Generates a dungeon layout by randomly placing rooms and resolving collisions
 * @param {number} iterations - The number of rooms to attempt to place
 * @param {Array} sampleRooms - An array of room templates to use for placement
 * @returns {{width: number, height: number, data: Array}} The generated dungeon layout
 */
export const generateDungeon = (iterations, sampleRooms) => {
  const directions = [
    { x: 1, y: 0 },
    { x: 1, y: 1 },
    { x: 0, y: 1 },
    { x: -1, y: 1 },
    { x: -1, y: 0 },
    { x: -1, y: -1 },
    { x: 0, y: -1 },
  ];
  const rooms = [];

  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  for (let i = 0; i < iterations; i++) {
    const direction = getRandomFrom(directions);
    const randomRoom = clone(getRandomFrom(sampleRooms));

    if (Math.random() < 0.5) {
      randomRoom.data.reverse();
    }

    const room = { ...randomRoom, x: 0, y: 0 };
    let attempts = 100;

    while (attempts--) {
      let collision = false;

      for (const roomToTest of rooms) {
        if (
          areTwoRectsCollided(
            {
              x: roomToTest.x,
              y: roomToTest.y,
              width: roomToTest.x + roomToTest.width,
              height: roomToTest.y + roomToTest.height,
            },
            {
              x: room.x,
              y: room.y,
              width: room.x + room.width,
              height: room.y + room.height,
            },
          )
        ) {
          const rnd = Math.random() - 0.5;
          const randomPosition = Math.round((Math.random() - 0.5) * 2);

          room.x += direction.x ? (rnd >= 0 || !direction.y ? direction.x : randomPosition) : randomPosition;
          room.y += direction.y ? (rnd < 0 || !direction.x ? direction.y : randomPosition) : randomPosition;

          collision = true;
          break;
        }
      }

      if (!collision) {
        break;
      }
    }

    minX = Math.min(room.x, minX);
    minY = Math.min(room.y, minY);
    maxX = Math.max(room.x + room.width, maxX);
    maxY = Math.max(room.y + room.height, maxY);

    rooms.push(room);
  }

  const width = maxX - minX;
  const height = maxY - minY;
  const data = Array(width * height).fill(0);

  rooms.forEach(({ x, y, width: w, height: h, data: roomData }) => {
    const absX = x - minX;
    const absY = y - minY;

    roomData.forEach((value, index) => {
      const { x: px, y: py } = vectorToCoord(index, w);
      data[coordToVector(absX + px, absY + py, width)] = value;
    });
  });

  for (let i = 0, len = data.length - width; i < len; i++) {
    const { x: px, y: py } = vectorToCoord(i, width);

    if (px < width - 1 && data[i] > 0) {
      const bottomIdx = coordToVector(px, py + 1, width);
      const rightIdx = coordToVector(px + 1, py, width);
      const bottomRightIdx = coordToVector(px + 1, py + 1, width);
      const leftIdx = coordToVector(px - 1, py, width);
      const bottomLeftIdx = coordToVector(px - 1, py + 1, width);

      if (data[bottomIdx] === data[rightIdx] && data[i] === data[bottomRightIdx] && data[i] !== data[rightIdx]) {
        data[bottomIdx] = data[rightIdx] = 1;
      }

      if (px > 0 && data[bottomIdx] === data[leftIdx] && data[i] === data[bottomLeftIdx] && data[i] !== data[leftIdx]) {
        data[bottomIdx] = data[leftIdx] = 1;
      }
    }
  }

  return { width: width, height: height, data: data };
};