DungeonGridGenerator.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. package me.lethunderhawk.dungeon.generation;
  2. import org.bukkit.Location;
  3. import org.bukkit.Material;
  4. import org.bukkit.World;
  5. import java.util.*;
  6. public class DungeonGridGenerator {
  7. private final World world;
  8. private final int gridWidth;
  9. private final int gridHeight;
  10. private final int roomSize;
  11. private final int roomHeight;
  12. private final int baseY = 99;
  13. private DungeonRoom[][] grid;
  14. private final Random random = new Random();
  15. // Track main path and branches separately
  16. private final List<DungeonRoom> mainPathRooms = new ArrayList<>();
  17. private final Set<DungeonRoom> branchRooms = new HashSet<>();
  18. // Probability that a branch spawns from a room initially
  19. private static final double INITIAL_BRANCH_CHANCE = 0.4;
  20. public DungeonGridGenerator(World world, int gridWidth, int gridHeight, int roomSize, int roomHeight) {
  21. this.world = world;
  22. this.gridWidth = gridWidth;
  23. this.gridHeight = gridHeight;
  24. this.roomSize = roomSize;
  25. this.roomHeight = roomHeight;
  26. }
  27. public void generate() {
  28. initGrid();
  29. generateRoomTypes();
  30. carveRooms();
  31. carveDoors();
  32. carvePaths();
  33. }
  34. /* =========================
  35. INITIALIZE GRID
  36. ========================= */
  37. private void initGrid() {
  38. grid = new DungeonRoom[gridWidth][gridHeight];
  39. for (int x = 0; x < gridWidth; x++)
  40. for (int z = 0; z < gridHeight; z++)
  41. grid[x][z] = new DungeonRoom(x, z, DungeonRoomType.REGULAR);
  42. mainPathRooms.clear();
  43. branchRooms.clear();
  44. }
  45. /* =========================
  46. GENERATE ROOM TYPES + PATHS
  47. ========================= */
  48. private void generateRoomTypes() {
  49. // 1) RESET
  50. for (int x = 0; x < gridWidth; x++)
  51. for (int z = 0; z < gridHeight; z++)
  52. grid[x][z].type = DungeonRoomType.REGULAR;
  53. mainPathRooms.clear();
  54. branchRooms.clear();
  55. DungeonRoom start = grid[0][0];
  56. DungeonRoom end = grid[gridWidth - 1][gridHeight - 1];
  57. // 2) ERZEUGE EINEN SPANNBAUM ÜBER DAS GANZE GRID
  58. generateSpanningTree(start);
  59. // 3) FINDE DEN EINDEUTIGEN WEG VON START → END
  60. List<DungeonRoom> mainPath = findPathInTree(start, end);
  61. enforceSingleEntranceForEnd(end, mainPath);
  62. if (mainPath.isEmpty())
  63. throw new IllegalStateException("Tree path failed");
  64. mainPathRooms.addAll(mainPath);
  65. // 4) ROOM TYPES
  66. mainPath.get(0).type = DungeonRoomType.START;
  67. mainPath.get(mainPath.size() - 1).type = DungeonRoomType.BLOOD;
  68. if (mainPath.size() > 2) {
  69. int fairyIndex = 1 + random.nextInt(mainPath.size() - 2);
  70. mainPath.get(fairyIndex).type = DungeonRoomType.FAIRY;
  71. }
  72. }
  73. private void enforceSingleEntranceForEnd(DungeonRoom end, List<DungeonRoom> mainPath) {
  74. // The room that is actually BEFORE the end on the main path
  75. DungeonRoom realParent = mainPath.get(mainPath.size() - 2);
  76. // For every neighbor of the end room:
  77. for (DungeonRoom n : getNeighbors(end)) {
  78. // If this neighbor is NOT the real main-path predecessor,
  79. // cut the connection.
  80. if (n != realParent && areConnected(end, n)) {
  81. disconnectRooms(end, n);
  82. }
  83. }
  84. }
  85. private void disconnectRooms(DungeonRoom a, DungeonRoom b) {
  86. int dx = b.gridX - a.gridX;
  87. int dz = b.gridZ - a.gridZ;
  88. if (dx == 1) {
  89. a.doors[Direction.EAST.ordinal()] = false;
  90. b.doors[Direction.WEST.ordinal()] = false;
  91. }
  92. else if (dx == -1) {
  93. a.doors[Direction.WEST.ordinal()] = false;
  94. b.doors[Direction.EAST.ordinal()] = false;
  95. }
  96. else if (dz == 1) {
  97. a.doors[Direction.SOUTH.ordinal()] = false;
  98. b.doors[Direction.NORTH.ordinal()] = false;
  99. }
  100. else if (dz == -1) {
  101. a.doors[Direction.NORTH.ordinal()] = false;
  102. b.doors[Direction.SOUTH.ordinal()] = false;
  103. }
  104. }
  105. private void generateSpanningTree(DungeonRoom start) {
  106. Set<DungeonRoom> visited = new HashSet<>();
  107. Deque<DungeonRoom> stack = new ArrayDeque<>();
  108. stack.push(start);
  109. visited.add(start);
  110. while (!stack.isEmpty()) {
  111. DungeonRoom current = stack.peek();
  112. List<DungeonRoom> neighbors = getNeighbors(current);
  113. Collections.shuffle(neighbors, random);
  114. DungeonRoom next = null;
  115. for (DungeonRoom n : neighbors) {
  116. if (!visited.contains(n)) {
  117. next = n;
  118. break;
  119. }
  120. }
  121. if (next == null) {
  122. stack.pop();
  123. } else {
  124. visited.add(next);
  125. connectRooms(current, next);
  126. stack.push(next);
  127. }
  128. }
  129. }
  130. private List<DungeonRoom> findPathInTree(DungeonRoom start, DungeonRoom end) {
  131. Map<DungeonRoom, DungeonRoom> parent = new HashMap<>();
  132. Deque<DungeonRoom> queue = new ArrayDeque<>();
  133. queue.add(start);
  134. parent.put(start, null);
  135. while (!queue.isEmpty()) {
  136. DungeonRoom cur = queue.poll();
  137. if (cur == end) break;
  138. for (DungeonRoom n : getNeighbors(cur)) {
  139. if (!parent.containsKey(n) && areConnected(cur, n)) {
  140. parent.put(n, cur);
  141. queue.add(n);
  142. }
  143. }
  144. }
  145. List<DungeonRoom> path = new ArrayList<>();
  146. DungeonRoom cur = end;
  147. while (cur != null) {
  148. path.add(cur);
  149. cur = parent.get(cur);
  150. }
  151. Collections.reverse(path);
  152. return path;
  153. }
  154. private boolean areConnected(DungeonRoom a, DungeonRoom b) {
  155. int dx = b.gridX - a.gridX;
  156. int dz = b.gridZ - a.gridZ;
  157. if (dx == 1) return a.doors[Direction.EAST.ordinal()];
  158. if (dx == -1) return a.doors[Direction.WEST.ordinal()];
  159. if (dz == 1) return a.doors[Direction.SOUTH.ordinal()];
  160. if (dz == -1) return a.doors[Direction.NORTH.ordinal()];
  161. return false;
  162. }
  163. /* =========================
  164. GENERATE MAIN PATH (biased random walk)
  165. ========================= */
  166. private List<DungeonRoom> generateMainPathRandomWalk(DungeonRoom start, DungeonRoom end) {
  167. List<DungeonRoom> path = new ArrayList<>();
  168. DungeonRoom current = start;
  169. path.add(current);
  170. while (current != end) {
  171. List<DungeonRoom> candidates = new ArrayList<>();
  172. for (DungeonRoom neighbor : getNeighbors(current)) {
  173. int distCurrent = manhattanDistance(current, end);
  174. int distNeighbor = manhattanDistance(neighbor, end);
  175. // Only allow steps that don't increase distance to end and are not already in path
  176. if (distNeighbor <= distCurrent && !path.contains(neighbor)) {
  177. candidates.add(neighbor);
  178. }
  179. }
  180. if (candidates.isEmpty()) {
  181. // Dead end encountered, restart generation to avoid stuck
  182. return Collections.emptyList();
  183. }
  184. current = candidates.get(random.nextInt(candidates.size()));
  185. path.add(current);
  186. }
  187. return path;
  188. }
  189. private int manhattanDistance(DungeonRoom a, DungeonRoom b) {
  190. return Math.abs(a.gridX - b.gridX) + Math.abs(a.gridZ - b.gridZ);
  191. }
  192. /* =========================
  193. CREATE BRANCHES FROM MAIN PATH
  194. ========================= */
  195. private void createBranches(DungeonRoom from, double chance) {
  196. if (chance < 0.1) return; // Stop recursion
  197. List<DungeonRoom> neighbors = getNeighbors(from);
  198. Collections.shuffle(neighbors, random);
  199. for (DungeonRoom neighbor : neighbors) {
  200. if (mainPathRooms.contains(neighbor) || branchRooms.contains(neighbor))
  201. continue;
  202. if (random.nextDouble() < chance) {
  203. branchRooms.add(neighbor);
  204. neighbor.type = DungeonRoomType.REGULAR;
  205. connectRooms(from, neighbor);
  206. createBranches(neighbor, chance * 0.6);
  207. }
  208. }
  209. }
  210. /* =========================
  211. GET GRID NEIGHBORS (N, E, S, W)
  212. ========================= */
  213. private List<DungeonRoom> getNeighbors(DungeonRoom room) {
  214. List<DungeonRoom> neighbors = new ArrayList<>();
  215. int x = room.gridX;
  216. int z = room.gridZ;
  217. if (x > 0) neighbors.add(grid[x - 1][z]);
  218. if (x < gridWidth - 1) neighbors.add(grid[x + 1][z]);
  219. if (z > 0) neighbors.add(grid[x][z - 1]);
  220. if (z < gridHeight - 1) neighbors.add(grid[x][z + 1]);
  221. return neighbors;
  222. }
  223. /* =========================
  224. CONNECT ROOMS BY SETTING DOORS
  225. ========================= */
  226. private enum Direction {
  227. NORTH, EAST, SOUTH, WEST
  228. }
  229. private void connectRooms(DungeonRoom a, DungeonRoom b) {
  230. int dx = b.gridX - a.gridX;
  231. int dz = b.gridZ - a.gridZ;
  232. if (dx == 1) {
  233. a.doors[Direction.EAST.ordinal()] = true;
  234. b.doors[Direction.WEST.ordinal()] = true;
  235. } else if (dx == -1) {
  236. a.doors[Direction.WEST.ordinal()] = true;
  237. b.doors[Direction.EAST.ordinal()] = true;
  238. } else if (dz == 1) {
  239. a.doors[Direction.SOUTH.ordinal()] = true;
  240. b.doors[Direction.NORTH.ordinal()] = true;
  241. } else if (dz == -1) {
  242. a.doors[Direction.NORTH.ordinal()] = true;
  243. b.doors[Direction.SOUTH.ordinal()] = true;
  244. }
  245. }
  246. /* =========================
  247. CARVE ROOMS
  248. ========================= */
  249. private void carveRooms() {
  250. for (int x = 0; x < gridWidth; x++) {
  251. for (int z = 0; z < gridHeight; z++) {
  252. DungeonRoom room = grid[x][z];
  253. Location center = gridToWorld(x, z);
  254. carveRoom(center, room.type);
  255. }
  256. }
  257. }
  258. private void carveRoom(Location center, DungeonRoomType type) {
  259. int half = roomSize / 2;
  260. for (int dx = -half; dx <= half; dx++) {
  261. for (int dz = -half; dz <= half; dz++) {
  262. for (int y = 0; y < roomHeight; y++) {
  263. Location loc = center.clone().add(dx, y, dz);
  264. if (y == 0 || y == roomHeight - 1) {
  265. loc.getBlock().setType(Material.STONE);
  266. continue;
  267. }
  268. if (dx == -half || dx == half || dz == -half || dz == half) {
  269. loc.getBlock().setType(Material.STONE);
  270. continue;
  271. }
  272. loc.getBlock().setType(Material.AIR);
  273. }
  274. }
  275. }
  276. // Place visible marker block on top center (one block above floor)
  277. Location markerLoc = center.clone().add(0, roomHeight + 3, 0);
  278. if (type == DungeonRoomType.START) {
  279. markerLoc.getBlock().setType(Material.GREEN_CONCRETE);
  280. } else if (type == DungeonRoomType.BLOOD) {
  281. markerLoc.getBlock().setType(Material.RED_CONCRETE);
  282. }
  283. }
  284. /* =========================
  285. CARVE DOORS BETWEEN CONNECTED ROOMS
  286. ========================= */
  287. private void carveDoors() {
  288. for (int x = 0; x < gridWidth; x++) {
  289. for (int z = 0; z < gridHeight; z++) {
  290. DungeonRoom room = grid[x][z];
  291. if (x < gridWidth - 1 && room.doors[Direction.EAST.ordinal()]) {
  292. DungeonRoom eastRoom = grid[x + 1][z];
  293. carveDoorBetween(room, eastRoom, Direction.EAST);
  294. }
  295. if (z < gridHeight - 1 && room.doors[Direction.SOUTH.ordinal()]) {
  296. DungeonRoom southRoom = grid[x][z + 1];
  297. carveDoorBetween(room, southRoom, Direction.SOUTH);
  298. }
  299. }
  300. }
  301. }
  302. private void carveDoorBetween(DungeonRoom from, DungeonRoom to, Direction dir) {
  303. int y = baseY + 1;
  304. Location fromCenter = gridToWorld(from.gridX, from.gridZ);
  305. Location toCenter = gridToWorld(to.gridX, to.gridZ);
  306. int doorX = fromCenter.getBlockX();
  307. int doorZ = fromCenter.getBlockZ();
  308. switch (dir) {
  309. case NORTH -> doorZ -= roomSize / 2;
  310. case EAST -> doorX += roomSize / 2;
  311. case SOUTH -> doorZ += roomSize / 2;
  312. case WEST -> doorX -= roomSize / 2;
  313. }
  314. world.getBlockAt(doorX, y, doorZ).setType(Material.AIR);
  315. world.getBlockAt(doorX, y + 1, doorZ).setType(Material.AIR);
  316. }
  317. /* =========================
  318. CARVE PATHS BETWEEN CONNECTED ROOMS
  319. ========================= */
  320. private void carvePaths() {
  321. for (int x = 0; x < gridWidth; x++) {
  322. for (int z = 0; z < gridHeight; z++) {
  323. DungeonRoom room = grid[x][z];
  324. if (x < gridWidth - 1 && room.doors[Direction.EAST.ordinal()]) {
  325. DungeonRoom eastRoom = grid[x + 1][z];
  326. carvePathBetweenRooms(room, eastRoom);
  327. }
  328. if (z < gridHeight - 1 && room.doors[Direction.SOUTH.ordinal()]) {
  329. DungeonRoom southRoom = grid[x][z + 1];
  330. carvePathBetweenRooms(room, southRoom);
  331. }
  332. }
  333. }
  334. }
  335. private void carvePathBetweenRooms(DungeonRoom from, DungeonRoom to) {
  336. Location start = gridToWorld(from.gridX, from.gridZ);
  337. Location end = gridToWorld(to.gridX, to.gridZ);
  338. int y = baseY;
  339. int x1 = start.getBlockX();
  340. int z1 = start.getBlockZ();
  341. int x2 = end.getBlockX();
  342. int z2 = end.getBlockZ();
  343. for (int x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
  344. placePathBlock(x, y, z1);
  345. placePathBlock(x, y + roomHeight + 10, z1);
  346. }
  347. for (int z = Math.min(z1, z2); z <= Math.max(z1, z2); z++) {
  348. placePathBlock(x2, y, z);
  349. placePathBlock(x2, y + roomHeight + 10, z);
  350. }
  351. }
  352. private void placePathBlock(int x, int y, int z) {
  353. world.getBlockAt(x, y, z).setType(Material.STONE_BRICKS);
  354. world.getBlockAt(x, y + 1, z).setType(Material.AIR);
  355. world.getBlockAt(x, y + 2, z).setType(Material.AIR);
  356. }
  357. /* =========================
  358. HELPERS
  359. ========================= */
  360. private Location gridToWorld(int gridX, int gridZ) {
  361. int worldX = gridX * (roomSize + 2);
  362. int worldZ = gridZ * (roomSize + 2);
  363. return new Location(world, worldX, baseY, worldZ);
  364. }
  365. /* =========================
  366. INNER ROOM CLASS + ROOM TYPES
  367. ========================= */
  368. public enum DungeonRoomType {
  369. START,
  370. FAIRY,
  371. PUZZLE,
  372. MINIBOSS,
  373. TRAP,
  374. BLOOD,
  375. REGULAR
  376. }
  377. public static class DungeonRoom {
  378. public final int gridX, gridZ;
  379. public DungeonRoomType type;
  380. public final boolean[] doors = new boolean[4]; // N, E, S, W
  381. public DungeonRoom(int gridX, int gridZ, DungeonRoomType type) {
  382. this.gridX = gridX;
  383. this.gridZ = gridZ;
  384. this.type = type;
  385. }
  386. }
  387. }