class_name DungeonGenerator ## Pure dungeon layout generator. Produces a grid dictionary describing rooms, ## their door bitmasks, and room types. No scene-tree or Node3D dependencies. # Side: W=0, S=1, E=2, N=3 (bit = 1 << side) const SIDE_DX := [-1, 0, 1, 0] const SIDE_DZ := [0, 1, 0, -1] const TPL_CORRIDOR := 0b1010 const TPL_T_JUNCT := 0b1110 const TPL_CROSS := 0b1111 const TPL_CORNER := 0b1100 const TPL_DEADEND := 0b1000 const TEMPLATES := [TPL_CORRIDOR, TPL_CORRIDOR, TPL_CORRIDOR, TPL_T_JUNCT, TPL_CROSS, TPL_CORNER, TPL_CORNER, TPL_DEADEND] const DEFAULT_TARGET_ROOMS := 8 func generate(target_rooms: int = DEFAULT_TARGET_ROOMS) -> Dictionary: var grid: Dictionary = {} _generate(grid, target_rooms) _designate_exit(grid) _designate_treasure(grid) return grid func _generate(grid: Dictionary, target_rooms: int) -> void: # Place entrance at (0,0) with doors on E + S for initial branching grid["0,0"] = {"type": "entrance", "doors": 0b0110} var frontier: Array = [] for side in 4: if (grid["0,0"]["doors"] >> side) & 1: frontier.append([SIDE_DX[side], SIDE_DZ[side]]) var attempts := 0 while grid.size() < target_rooms and attempts < 200: attempts += 1 if frontier.is_empty(): break var idx := randi() % frontier.size() var cell = frontier[idx] frontier.remove_at(idx) var gx := cell[0] as int var gz := cell[1] as int var key = "%d,%d" % [gx, gz] if key in grid: continue var required = _get_required_doors(grid, gx, gz) if required == 0: continue var compatible = _find_compatible(required) if compatible.is_empty(): continue var pick = compatible[randi() % compatible.size()] grid[key] = {"type": "normal", "doors": pick} for side in 4: if (pick >> side) & 1: frontier.append([gx + SIDE_DX[side], gz + SIDE_DZ[side]]) func _get_required_doors(grid: Dictionary, gx: int, gz: int) -> int: var req := 0 var nkey = "%d,%d" % [gx, gz - 1] if nkey in grid and (grid[nkey]["doors"] >> 1) & 1: req |= (1 << 3) var ekey = "%d,%d" % [gx + 1, gz] if ekey in grid and (grid[ekey]["doors"] >> 0) & 1: req |= (1 << 2) var skey = "%d,%d" % [gx, gz + 1] if skey in grid and (grid[skey]["doors"] >> 3) & 1: req |= (1 << 1) var wkey = "%d,%d" % [gx - 1, gz] if wkey in grid and (grid[wkey]["doors"] >> 2) & 1: req |= (1 << 0) return req func _rotate_mask(mask: int, times: int) -> int: var result := 0 for i in 4: if (mask >> i) & 1: result |= (1 << ((i + times) % 4)) return result func _find_compatible(required: int) -> Array: var results: Array = [] for tpl in TEMPLATES: for rot in 4: var rotated = _rotate_mask(tpl, rot) if (rotated & required) == required: results.append(rotated) return results func _valid_connections(grid: Dictionary, gx: int, gz: int, doors: int) -> int: var count := 0 for side in 4: if not ((doors >> side) & 1): continue var nkey = "%d,%d" % [gx + SIDE_DX[side], gz + SIDE_DZ[side]] if nkey in grid: var opp = (side + 2) % 4 if (grid[nkey]["doors"] >> opp) & 1: count += 1 return count func _designate_exit(grid: Dictionary) -> void: var dead_ends: Array = [] for key in grid: if grid[key]["type"] == "entrance": continue var parts = key.split(",") var gx = int(parts[0]) var gz = int(parts[1]) if _valid_connections(grid, gx, gz, grid[key]["doors"]) == 1: dead_ends.append([key, gx * gx + gz * gz]) if not dead_ends.is_empty(): dead_ends.sort_custom(func(a, b): return a[1] > b[1]) grid[dead_ends[0][0]]["type"] = "exit" return # Fallback: farthest non-entrance room var farthest_key := "" var farthest_dist := 0 for key in grid: if grid[key]["type"] == "entrance": continue var parts = key.split(",") var dist = int(parts[0]) * int(parts[0]) + int(parts[1]) * int(parts[1]) if dist > farthest_dist: farthest_dist = dist farthest_key = key if not farthest_key.is_empty(): grid[farthest_key]["type"] = "exit" func _designate_treasure(grid: Dictionary) -> void: var candidates: Array = [] for key in grid: if grid[key]["type"] in ["entrance", "exit"]: continue var parts = key.split(",") var gx = int(parts[0]) var gz = int(parts[1]) if _valid_connections(grid, gx, gz, grid[key]["doors"]) == 1: candidates.append(key) var count = min(candidates.size(), 2) for i in count: if candidates.is_empty(): break var idx = randi() % candidates.size() grid[candidates[idx]]["type"] = "treasure" candidates.remove_at(idx)