package me.lethunderhawk.dungeon.generation; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import java.util.*; public class DungeonGridGenerator { private final World world; private final int gridWidth; private final int gridHeight; private final int roomSize; private final int roomHeight; private final int baseY = 99; private DungeonRoom[][] grid; private final Random random = new Random(); // Track main path and branches separately private final List mainPathRooms = new ArrayList<>(); private final Set branchRooms = new HashSet<>(); // Probability that a branch spawns from a room initially private static final double INITIAL_BRANCH_CHANCE = 0.4; public DungeonGridGenerator(World world, int gridWidth, int gridHeight, int roomSize, int roomHeight) { this.world = world; this.gridWidth = gridWidth; this.gridHeight = gridHeight; this.roomSize = roomSize; this.roomHeight = roomHeight; } public void generate() { initGrid(); generateRoomTypes(); carveRooms(); carveDoors(); carvePaths(); } /* ========================= INITIALIZE GRID ========================= */ private void initGrid() { grid = new DungeonRoom[gridWidth][gridHeight]; for (int x = 0; x < gridWidth; x++) for (int z = 0; z < gridHeight; z++) grid[x][z] = new DungeonRoom(x, z, DungeonRoomType.REGULAR); mainPathRooms.clear(); branchRooms.clear(); } /* ========================= GENERATE ROOM TYPES + PATHS ========================= */ private void generateRoomTypes() { // 1) RESET for (int x = 0; x < gridWidth; x++) for (int z = 0; z < gridHeight; z++) grid[x][z].type = DungeonRoomType.REGULAR; mainPathRooms.clear(); branchRooms.clear(); DungeonRoom start = grid[0][0]; DungeonRoom end = grid[gridWidth - 1][gridHeight - 1]; // 2) ERZEUGE EINEN SPANNBAUM ÜBER DAS GANZE GRID generateSpanningTree(start); // 3) FINDE DEN EINDEUTIGEN WEG VON START → END List mainPath = findPathInTree(start, end); enforceSingleEntranceForEnd(end, mainPath); if (mainPath.isEmpty()) throw new IllegalStateException("Tree path failed"); mainPathRooms.addAll(mainPath); // 4) ROOM TYPES mainPath.get(0).type = DungeonRoomType.START; mainPath.get(mainPath.size() - 1).type = DungeonRoomType.BLOOD; if (mainPath.size() > 2) { int fairyIndex = 1 + random.nextInt(mainPath.size() - 2); mainPath.get(fairyIndex).type = DungeonRoomType.FAIRY; } } private void enforceSingleEntranceForEnd(DungeonRoom end, List mainPath) { // The room that is actually BEFORE the end on the main path DungeonRoom realParent = mainPath.get(mainPath.size() - 2); // For every neighbor of the end room: for (DungeonRoom n : getNeighbors(end)) { // If this neighbor is NOT the real main-path predecessor, // cut the connection. if (n != realParent && areConnected(end, n)) { disconnectRooms(end, n); } } } private void disconnectRooms(DungeonRoom a, DungeonRoom b) { int dx = b.gridX - a.gridX; int dz = b.gridZ - a.gridZ; if (dx == 1) { a.doors[Direction.EAST.ordinal()] = false; b.doors[Direction.WEST.ordinal()] = false; } else if (dx == -1) { a.doors[Direction.WEST.ordinal()] = false; b.doors[Direction.EAST.ordinal()] = false; } else if (dz == 1) { a.doors[Direction.SOUTH.ordinal()] = false; b.doors[Direction.NORTH.ordinal()] = false; } else if (dz == -1) { a.doors[Direction.NORTH.ordinal()] = false; b.doors[Direction.SOUTH.ordinal()] = false; } } private void generateSpanningTree(DungeonRoom start) { Set visited = new HashSet<>(); Deque stack = new ArrayDeque<>(); stack.push(start); visited.add(start); while (!stack.isEmpty()) { DungeonRoom current = stack.peek(); List neighbors = getNeighbors(current); Collections.shuffle(neighbors, random); DungeonRoom next = null; for (DungeonRoom n : neighbors) { if (!visited.contains(n)) { next = n; break; } } if (next == null) { stack.pop(); } else { visited.add(next); connectRooms(current, next); stack.push(next); } } } private List findPathInTree(DungeonRoom start, DungeonRoom end) { Map parent = new HashMap<>(); Deque queue = new ArrayDeque<>(); queue.add(start); parent.put(start, null); while (!queue.isEmpty()) { DungeonRoom cur = queue.poll(); if (cur == end) break; for (DungeonRoom n : getNeighbors(cur)) { if (!parent.containsKey(n) && areConnected(cur, n)) { parent.put(n, cur); queue.add(n); } } } List path = new ArrayList<>(); DungeonRoom cur = end; while (cur != null) { path.add(cur); cur = parent.get(cur); } Collections.reverse(path); return path; } private boolean areConnected(DungeonRoom a, DungeonRoom b) { int dx = b.gridX - a.gridX; int dz = b.gridZ - a.gridZ; if (dx == 1) return a.doors[Direction.EAST.ordinal()]; if (dx == -1) return a.doors[Direction.WEST.ordinal()]; if (dz == 1) return a.doors[Direction.SOUTH.ordinal()]; if (dz == -1) return a.doors[Direction.NORTH.ordinal()]; return false; } /* ========================= GENERATE MAIN PATH (biased random walk) ========================= */ private List generateMainPathRandomWalk(DungeonRoom start, DungeonRoom end) { List path = new ArrayList<>(); DungeonRoom current = start; path.add(current); while (current != end) { List candidates = new ArrayList<>(); for (DungeonRoom neighbor : getNeighbors(current)) { int distCurrent = manhattanDistance(current, end); int distNeighbor = manhattanDistance(neighbor, end); // Only allow steps that don't increase distance to end and are not already in path if (distNeighbor <= distCurrent && !path.contains(neighbor)) { candidates.add(neighbor); } } if (candidates.isEmpty()) { // Dead end encountered, restart generation to avoid stuck return Collections.emptyList(); } current = candidates.get(random.nextInt(candidates.size())); path.add(current); } return path; } private int manhattanDistance(DungeonRoom a, DungeonRoom b) { return Math.abs(a.gridX - b.gridX) + Math.abs(a.gridZ - b.gridZ); } /* ========================= CREATE BRANCHES FROM MAIN PATH ========================= */ private void createBranches(DungeonRoom from, double chance) { if (chance < 0.1) return; // Stop recursion List neighbors = getNeighbors(from); Collections.shuffle(neighbors, random); for (DungeonRoom neighbor : neighbors) { if (mainPathRooms.contains(neighbor) || branchRooms.contains(neighbor)) continue; if (random.nextDouble() < chance) { branchRooms.add(neighbor); neighbor.type = DungeonRoomType.REGULAR; connectRooms(from, neighbor); createBranches(neighbor, chance * 0.6); } } } /* ========================= GET GRID NEIGHBORS (N, E, S, W) ========================= */ private List getNeighbors(DungeonRoom room) { List neighbors = new ArrayList<>(); int x = room.gridX; int z = room.gridZ; if (x > 0) neighbors.add(grid[x - 1][z]); if (x < gridWidth - 1) neighbors.add(grid[x + 1][z]); if (z > 0) neighbors.add(grid[x][z - 1]); if (z < gridHeight - 1) neighbors.add(grid[x][z + 1]); return neighbors; } /* ========================= CONNECT ROOMS BY SETTING DOORS ========================= */ private enum Direction { NORTH, EAST, SOUTH, WEST } private void connectRooms(DungeonRoom a, DungeonRoom b) { int dx = b.gridX - a.gridX; int dz = b.gridZ - a.gridZ; if (dx == 1) { a.doors[Direction.EAST.ordinal()] = true; b.doors[Direction.WEST.ordinal()] = true; } else if (dx == -1) { a.doors[Direction.WEST.ordinal()] = true; b.doors[Direction.EAST.ordinal()] = true; } else if (dz == 1) { a.doors[Direction.SOUTH.ordinal()] = true; b.doors[Direction.NORTH.ordinal()] = true; } else if (dz == -1) { a.doors[Direction.NORTH.ordinal()] = true; b.doors[Direction.SOUTH.ordinal()] = true; } } /* ========================= CARVE ROOMS ========================= */ private void carveRooms() { for (int x = 0; x < gridWidth; x++) { for (int z = 0; z < gridHeight; z++) { DungeonRoom room = grid[x][z]; Location center = gridToWorld(x, z); carveRoom(center, room.type); } } } private void carveRoom(Location center, DungeonRoomType type) { int half = roomSize / 2; for (int dx = -half; dx <= half; dx++) { for (int dz = -half; dz <= half; dz++) { for (int y = 0; y < roomHeight; y++) { Location loc = center.clone().add(dx, y, dz); if (y == 0 || y == roomHeight - 1) { loc.getBlock().setType(Material.STONE); continue; } if (dx == -half || dx == half || dz == -half || dz == half) { loc.getBlock().setType(Material.STONE); continue; } loc.getBlock().setType(Material.AIR); } } } // Place visible marker block on top center (one block above floor) Location markerLoc = center.clone().add(0, roomHeight + 3, 0); if (type == DungeonRoomType.START) { markerLoc.getBlock().setType(Material.GREEN_CONCRETE); } else if (type == DungeonRoomType.BLOOD) { markerLoc.getBlock().setType(Material.RED_CONCRETE); } } /* ========================= CARVE DOORS BETWEEN CONNECTED ROOMS ========================= */ private void carveDoors() { for (int x = 0; x < gridWidth; x++) { for (int z = 0; z < gridHeight; z++) { DungeonRoom room = grid[x][z]; if (x < gridWidth - 1 && room.doors[Direction.EAST.ordinal()]) { DungeonRoom eastRoom = grid[x + 1][z]; carveDoorBetween(room, eastRoom, Direction.EAST); } if (z < gridHeight - 1 && room.doors[Direction.SOUTH.ordinal()]) { DungeonRoom southRoom = grid[x][z + 1]; carveDoorBetween(room, southRoom, Direction.SOUTH); } } } } private void carveDoorBetween(DungeonRoom from, DungeonRoom to, Direction dir) { int y = baseY + 1; Location fromCenter = gridToWorld(from.gridX, from.gridZ); Location toCenter = gridToWorld(to.gridX, to.gridZ); int doorX = fromCenter.getBlockX(); int doorZ = fromCenter.getBlockZ(); switch (dir) { case NORTH -> doorZ -= roomSize / 2; case EAST -> doorX += roomSize / 2; case SOUTH -> doorZ += roomSize / 2; case WEST -> doorX -= roomSize / 2; } world.getBlockAt(doorX, y, doorZ).setType(Material.AIR); world.getBlockAt(doorX, y + 1, doorZ).setType(Material.AIR); } /* ========================= CARVE PATHS BETWEEN CONNECTED ROOMS ========================= */ private void carvePaths() { for (int x = 0; x < gridWidth; x++) { for (int z = 0; z < gridHeight; z++) { DungeonRoom room = grid[x][z]; if (x < gridWidth - 1 && room.doors[Direction.EAST.ordinal()]) { DungeonRoom eastRoom = grid[x + 1][z]; carvePathBetweenRooms(room, eastRoom); } if (z < gridHeight - 1 && room.doors[Direction.SOUTH.ordinal()]) { DungeonRoom southRoom = grid[x][z + 1]; carvePathBetweenRooms(room, southRoom); } } } } private void carvePathBetweenRooms(DungeonRoom from, DungeonRoom to) { Location start = gridToWorld(from.gridX, from.gridZ); Location end = gridToWorld(to.gridX, to.gridZ); int y = baseY; int x1 = start.getBlockX(); int z1 = start.getBlockZ(); int x2 = end.getBlockX(); int z2 = end.getBlockZ(); for (int x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) { placePathBlock(x, y, z1); placePathBlock(x, y + roomHeight + 10, z1); } for (int z = Math.min(z1, z2); z <= Math.max(z1, z2); z++) { placePathBlock(x2, y, z); placePathBlock(x2, y + roomHeight + 10, z); } } private void placePathBlock(int x, int y, int z) { world.getBlockAt(x, y, z).setType(Material.STONE_BRICKS); world.getBlockAt(x, y + 1, z).setType(Material.AIR); world.getBlockAt(x, y + 2, z).setType(Material.AIR); } /* ========================= HELPERS ========================= */ private Location gridToWorld(int gridX, int gridZ) { int worldX = gridX * (roomSize + 2); int worldZ = gridZ * (roomSize + 2); return new Location(world, worldX, baseY, worldZ); } /* ========================= INNER ROOM CLASS + ROOM TYPES ========================= */ public enum DungeonRoomType { START, FAIRY, PUZZLE, MINIBOSS, TRAP, BLOOD, REGULAR } public static class DungeonRoom { public final int gridX, gridZ; public DungeonRoomType type; public final boolean[] doors = new boolean[4]; // N, E, S, W public DungeonRoom(int gridX, int gridZ, DungeonRoomType type) { this.gridX = gridX; this.gridZ = gridZ; this.type = type; } } }