class_name MapGenerationClass extends Node # Map settings var map_width: int = Global.map_width var map_height: int = Global.map_height # Path generation settings @export var num_horizontal_paths: int = 3 @export var num_vertical_paths: int = 2 @export var path_wander_strength: float = 0.3 # How much paths can deviate (0-1) @export var path_smoothing_passes: int = 2 # Number of smoothing iterations @export var branch_probability: float = 0.6 # Chance of creating branches (0-1) @export var max_branches_per_path: int = 3 # Maximum branches per main path @export var dead_end_probability: float = 0.3 # Chance for branches to be dead ends (0-1) @export var min_branch_length_ratio: float = 0.2 # Minimum branch length as ratio of main path @export var max_branch_length_ratio: float = 0.4 # Maximum branch length as ratio of main path # Water generation settings @export var num_water_bodies: int = 5 # Number of water bodies to generate @export var min_water_size: int = 15 # Minimum radius of water bodies @export var max_water_size: int = 34 # Maximum radius of water bodies @export var water_placement_attempts: int = 50 # Max attempts to place each water body # Image export settings @export var tile_size: int = 8 # Size of each tile in pixels (square) @export var export_image: bool = true # Whether to export image @export var image_filename: String = "generated_map.png" # Output filename var noise: FastNoiseLite var map: Array = [] var map_data: Array = [] var path_data: Array = [] # Separate array to track paths var water_data: Array = [] # Separate array to track water bodies # New data structures for enhanced path generation var path_segments: Array = [] # Store all path segments for better branching var path_id_counter: int = 0 func _ready(): generate_map() generate_paths() generate_water_bodies() BiomeGenerationClass.generate_environment_maps(map_width, map_height) generate_final_map_data() #if export_image: # export_map_as_image() func generate_final_map_data(): var objects_before = Performance.get_monitor(Performance.OBJECT_COUNT) for y in range(map_height): for x in range(map_width): MapPopulationClass.generate_cell_with_distribution(x, y, map_data[y][x], is_path_at(x, y), is_water_at(x, y)) # Check immediately after await get_tree().process_frame var objects_after = Performance.get_monitor(Performance.OBJECT_COUNT) Log.pr("Objects created in generate_final_map_data: ", objects_after - objects_before) func generate_map(): # Initialize noise noise = FastNoiseLite.new() noise.seed = randi() noise.frequency = 0.01 noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH # Fractal settings for organic paths noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED noise.fractal_octaves = 2 noise.fractal_lacunarity = 4.0 noise.fractal_gain = 0.2 noise.fractal_weighted_strength = 1 noise.fractal_ping_pong_strength = 2.0 # Domain warp for natural winding noise.domain_warp_enabled = true noise.domain_warp_type = FastNoiseLite.DOMAIN_WARP_BASIC_GRID noise.domain_warp_amplitude = 20 noise.domain_warp_frequency = 0.01 # Generate map data map_data.resize(map_height) path_data.resize(map_height) water_data.resize(map_height) for y in range(map_height): map_data[y] = [] path_data[y] = [] water_data[y] = [] map_data[y].resize(map_width) path_data[y].resize(map_width) water_data[y].resize(map_width) for x in range(map_width): # Get noise value (-1 to 1) and normalize to (0 to 1) var noise_value = noise.get_noise_2d(x, y) var normalized_value = (noise_value + 1.0) / 2.0 map_data[y][x] = round(normalized_value * 10.0) / 10.0 path_data[y][x] = false # Initialize path data water_data[y][x] = false # Initialize water data func generate_paths(): print("Generating natural paths with branching...") path_segments.clear() path_id_counter = 0 # Generate main organic paths for i in range(num_horizontal_paths): generate_main_path(i, "horizontal") for i in range(num_vertical_paths): generate_main_path(i, "vertical") # Generate branches for existing paths generate_path_branches() # Smooth and connect paths smooth_paths() func generate_main_path(path_index: int, direction: String): var path_points = [] # Set up start and end points based on direction var start_point: Vector2 var end_point: Vector2 if direction == "horizontal": # Horizontal paths go from left edge to right edge var y_pos = (map_height / (num_horizontal_paths + 1)) * (path_index + 1) y_pos += randi_range(-map_height / 8, map_height / 8) # Add some vertical variation start_point = Vector2(0, clamp(y_pos, 5, map_height - 5)) end_point = Vector2(map_width - 1, clamp(y_pos + randi_range(-map_height / 4, map_height / 4), 5, map_height - 5)) else: # Vertical paths go from top edge to bottom edge var x_pos = (map_width / (num_vertical_paths + 1)) * (path_index + 1) x_pos += randi_range(-map_width / 8, map_width / 8) # Add some horizontal variation start_point = Vector2(clamp(x_pos, 5, map_width - 5), 0) end_point = Vector2(clamp(x_pos + randi_range(-map_width / 4, map_width / 4), 5, map_width - 5), map_height - 1) # Generate control points for smooth curves var control_points = [start_point] # Add several intermediate control points for natural curves var num_control_points = randi_range(3, 6) for i in range(1, num_control_points): var t = float(i) / float(num_control_points) # Interpolate between start and end var base_point = start_point.lerp(end_point, t) # Add random offset for natural wandering var max_offset = min(map_width, map_height) * 0.3 * path_wander_strength var offset = Vector2( randf_range(-max_offset, max_offset), randf_range(-max_offset, max_offset) ) var control_point = base_point + offset control_point.x = clamp(control_point.x, 0, map_width - 1) control_point.y = clamp(control_point.y, 0, map_height - 1) control_points.append(control_point) control_points.append(end_point) # Create main path and store segment info var path_id = path_id_counter path_id_counter += 1 var segment_info = { "id": path_id, "type": "main", "direction": direction, "control_points": control_points, "parent_id": - 1, "branch_points": [] # Points where branches can spawn } path_segments.append(segment_info) create_spline_path(control_points, path_id, true) # true = is_main_path func generate_path_branches(): print("Generating path branches...") # Create branches for each main path for segment in path_segments: if segment.type == "main": generate_branches_for_segment(segment) func generate_branches_for_segment(main_segment: Dictionary): var num_branches = 0 var max_attempts = 5 # Try to create branches up to the maximum allowed while num_branches < max_branches_per_path and max_attempts > 0: max_attempts -= 1 if randf() < branch_probability: create_branch_from_segment(main_segment, num_branches) num_branches += 1 func create_branch_from_segment(parent_segment: Dictionary, branch_index: int): # Pick a random point along the parent path to branch from var branch_start_t = randf_range(0.2, 0.8) # Don't branch too close to ends var branch_start_point = evaluate_catmull_rom_spline(parent_segment.control_points, branch_start_t) # Determine branch direction based on parent direction and some randomness var branch_direction = get_branch_direction(parent_segment.direction, branch_start_point) # Calculate branch length var main_path_length = estimate_path_length(parent_segment.control_points) var branch_length_ratio = randf_range(min_branch_length_ratio, max_branch_length_ratio) var target_branch_length = main_path_length * branch_length_ratio # Determine if this should be a dead end var is_dead_end = randf() < dead_end_probability # Generate branch end point var branch_end_point: Vector2 if is_dead_end: # Dead end - create a point that doesn't reach map edge branch_end_point = create_dead_end_point(branch_start_point, branch_direction, target_branch_length) else: # Through branch - try to reach map edge or connect to another path branch_end_point = create_through_branch_point(branch_start_point, branch_direction, target_branch_length) # Generate branch control points var branch_control_points = generate_branch_control_points(branch_start_point, branch_end_point, branch_direction) # Create the branch var branch_id = path_id_counter path_id_counter += 1 var branch_segment = { "id": branch_id, "type": "branch", "direction": branch_direction, "control_points": branch_control_points, "parent_id": parent_segment.id, "is_dead_end": is_dead_end } path_segments.append(branch_segment) create_spline_path(branch_control_points, branch_id, false) # false = not main path func get_branch_direction(parent_direction: String, branch_point: Vector2) -> String: # Determine branch direction based on parent and position var directions = [] if parent_direction == "horizontal": directions = ["north", "south"] # Add some bias based on position if branch_point.y < map_height * 0.3: directions.append("south") # Bias toward south if in upper area elif branch_point.y > map_height * 0.7: directions.append("north") # Bias toward north if in lower area else: # vertical directions = ["east", "west"] # Add some bias based on position if branch_point.x < map_width * 0.3: directions.append("east") # Bias toward east if in left area elif branch_point.x > map_width * 0.7: directions.append("west") # Bias toward west if in right area return directions[randi() % directions.size()] func create_dead_end_point(start_point: Vector2, direction: String, target_length: float) -> Vector2: # Create a point for a dead end that doesn't reach the map edge var direction_vector = get_direction_vector(direction) var max_distance = target_length * randf_range(0.5, 0.9) # Dead ends are shorter # Add some randomness to the direction var angle_variation = randf_range(-PI / 4, PI / 4) # ±45 degrees direction_vector = direction_vector.rotated(angle_variation) var end_point = start_point + direction_vector * max_distance # Ensure the end point stays within map bounds with some margin var margin = 20 end_point.x = clamp(end_point.x, margin, map_width - margin) end_point.y = clamp(end_point.y, margin, map_height - margin) return end_point func create_through_branch_point(start_point: Vector2, direction: String, target_length: float) -> Vector2: # Create a point that tries to reach toward a map edge var direction_vector = get_direction_vector(direction) # Add some wandering to make it more natural var angle_variation = randf_range(-PI / 6, PI / 6) # ±30 degrees direction_vector = direction_vector.rotated(angle_variation) var end_point = start_point + direction_vector * target_length # Try to reach toward the appropriate map edge match direction: "north": end_point.y = max(5, end_point.y) "south": end_point.y = min(map_height - 5, end_point.y) "east": end_point.x = min(map_width - 5, end_point.x) "west": end_point.x = max(5, end_point.x) # Clamp to map bounds end_point.x = clamp(end_point.x, 5, map_width - 5) end_point.y = clamp(end_point.y, 5, map_height - 5) return end_point func get_direction_vector(direction: String) -> Vector2: match direction: "north": return Vector2(0, -1) "south": return Vector2(0, 1) "east": return Vector2(1, 0) "west": return Vector2(-1, 0) _: return Vector2(0, 1) # Default to south func generate_branch_control_points(start_point: Vector2, end_point: Vector2, direction: String) -> Array: var control_points = [start_point] # Add fewer control points for branches to make them simpler var num_control_points = randi_range(2, 4) for i in range(1, num_control_points): var t = float(i) / float(num_control_points) var base_point = start_point.lerp(end_point, t) # Less wandering for branches var max_offset = min(map_width, map_height) * 0.15 * path_wander_strength var offset = Vector2( randf_range(-max_offset, max_offset), randf_range(-max_offset, max_offset) ) var control_point = base_point + offset control_point.x = clamp(control_point.x, 0, map_width - 1) control_point.y = clamp(control_point.y, 0, map_height - 1) control_points.append(control_point) control_points.append(end_point) return control_points func estimate_path_length(control_points: Array) -> float: # Rough estimate of path length for branch sizing var total_length = 0.0 for i in range(control_points.size() - 1): total_length += control_points[i].distance_to(control_points[i + 1]) return total_length func create_spline_path(control_points: Array, path_id: int, is_main_path: bool = false): # Create a smooth path using Catmull-Rom spline interpolation var path_resolution = max(map_width, map_height) * 2 # High resolution for smooth curves # Reduce resolution for branches to make them less dense if not is_main_path: path_resolution = int(path_resolution * 0.7) for i in range(path_resolution): var t = float(i) / float(path_resolution - 1) var point = evaluate_catmull_rom_spline(control_points, t) # Place path segments along the spline var x = int(round(point.x)) var y = int(round(point.y)) if x >= 0 and x < map_width and y >= 0 and y < map_height: place_organic_path_segment(x, y, path_id, is_main_path) func evaluate_catmull_rom_spline(points: Array, t: float) -> Vector2: var n = points.size() if n < 2: return points[0] if n > 0 else Vector2.ZERO # Handle edge cases if t <= 0.0: return points[0] if t >= 1.0: return points[n - 1] # Find the segment var segment_length = 1.0 / float(n - 1) var segment_index = int(t / segment_length) segment_index = clamp(segment_index, 0, n - 2) # Local t within the segment var local_t = (t - segment_index * segment_length) / segment_length # Get control points (with clamping for edge segments) var p0 = points[max(0, segment_index - 1)] var p1 = points[segment_index] var p2 = points[min(n - 1, segment_index + 1)] var p3 = points[min(n - 1, segment_index + 2)] # Catmull-Rom spline interpolation var t2 = local_t * local_t var t3 = t2 * local_t var result = Vector2.ZERO result += p0 * (-0.5 * t3 + t2 - 0.5 * local_t) result += p1 * (1.5 * t3 - 2.5 * t2 + 1.0) result += p2 * (-1.5 * t3 + 2.0 * t2 + 0.5 * local_t) result += p3 * (0.5 * t3 - 0.5 * t2) return result func place_organic_path_segment(x: int, y: int, path_id: int, is_main_path: bool = false): # Base path placement - always place the center tile path_data[y][x] = true if is_main_path: # Main paths can be wider (2-3 tiles) var width_noise = FastNoiseLite.new() width_noise.seed = randi() + path_id * 5000 width_noise.frequency = 0.1 width_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH # Add organic width variation for main paths only var width_value = width_noise.get_noise_2d(x, y) var base_width = 1 + int((width_value + 1.0) * 1.0) # Width 1-3 tiles for main paths var path_width = max(1, base_width) # Place path tiles in organic pattern around the center for main paths for dy in range(-path_width, path_width + 1): for dx in range(-path_width, path_width + 1): var distance = sqrt(dx * dx + dy * dy) if distance <= path_width: var nx = x + dx var ny = y + dy if nx >= 0 and nx < map_width and ny >= 0 and ny < map_height: # Use noise to create organic edges var edge_noise = width_noise.get_noise_2d(nx, ny) var edge_chance = 1.0 - (distance / float(path_width)) edge_chance += edge_noise * 0.3 if edge_chance > 0.4: path_data[ny][nx] = true # For branches (is_main_path = false), only the center tile is placed above func smooth_paths(): # Apply smoothing passes to make paths more natural for pass_num in range(path_smoothing_passes): var new_path_data = path_data.duplicate(true) for y in range(1, map_height - 1): for x in range(1, map_width - 1): if path_data[y][x]: # Check neighbors and potentially expand path var neighbor_count = 0 for dy in range(-1, 2): for dx in range(-1, 2): if dx == 0 and dy == 0: continue if path_data[y + dy][x + dx]: neighbor_count += 1 # If isolated path point, try to connect it if neighbor_count < 2 and randf() > 0.5: # Find nearest path point and create connection for radius in range(1, 4): var connected = false for dy in range(-radius, radius + 1): for dx in range(-radius, radius + 1): var ny = y + dy var nx = x + dx if ny >= 0 and ny < map_height and nx >= 0 and nx < map_width: if path_data[ny][nx] and (abs(dx) + abs(dy)) <= radius: # Create connection var steps = max(abs(dx), abs(dy)) for step in range(steps + 1): var interp_x = x + int(float(dx * step) / float(steps)) var interp_y = y + int(float(dy * step) / float(steps)) if interp_x >= 0 and interp_x < map_width and interp_y >= 0 and interp_y < map_height: new_path_data[interp_y][interp_x] = true connected = true break if connected: break if connected: break path_data = new_path_data func generate_water_bodies(): print("Generating water bodies...") for i in range(num_water_bodies): generate_single_water_body(i) func generate_single_water_body(body_index: int): var attempts = 0 var placed = false while attempts < water_placement_attempts and not placed: # Random center position var center_x = randi_range(max_water_size, map_width - max_water_size) var center_y = randi_range(max_water_size, map_height - max_water_size) # Random size within configured range var water_radius = randi_range(min_water_size, max_water_size) # Check if this location is valid (no paths in the area) if is_valid_water_location(center_x, center_y, water_radius): place_water_body(center_x, center_y, water_radius, body_index) placed = true print("Water body ", body_index + 1, " placed at (", center_x, ",", center_y, ") with radius ", water_radius) attempts += 1 if not placed: print("Could not place water body ", body_index + 1, " after ", water_placement_attempts, " attempts") func is_valid_water_location(center_x: int, center_y: int, radius: int) -> bool: # Check if any paths would be intersected by this water body # Use a slightly larger radius for safety buffer var safety_buffer = 2 var check_radius = radius + safety_buffer for y in range(center_y - check_radius, center_y + check_radius + 1): for x in range(center_x - check_radius, center_x + check_radius + 1): if x >= 0 and x < map_width and y >= 0 and y < map_height: # Check if this position has a path if path_data[y][x]: var distance = sqrt(pow(x - center_x, 2) + pow(y - center_y, 2)) if distance <= check_radius: return false # Also check if there's already water here (prevent overlap) if water_data[y][x]: var distance = sqrt(pow(x - center_x, 2) + pow(y - center_y, 2)) if distance <= check_radius: return false return true func place_water_body(center_x: int, center_y: int, radius: int, body_index: int): # Simple approach: start with basic circle, then use noise to make edges organic var water_noise = FastNoiseLite.new() water_noise.seed = randi() + body_index * 3000 water_noise.frequency = 0.1 water_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH # Place water in roughly circular area with noise-modified edges for y in range(center_y - radius - 2, center_y + radius + 3): for x in range(center_x - radius - 2, center_x + radius + 3): if x >= 0 and x < map_width and y >= 0 and y < map_height: if path_data[y][x]: # Don't place water on paths continue # Calculate distance from center var distance = sqrt(pow(x - center_x, 2) + pow(y - center_y, 2)) # Use noise to modify the effective radius at this position var noise_value = water_noise.get_noise_2d(x, y) var radius_modifier = noise_value * radius * 0.4 # 40% variation var effective_radius = radius + radius_modifier # Place water if within the noise-modified radius if distance <= effective_radius: water_data[y][x] = true func print_visual_map_to_console(): print("") print("==================================================") print("VISUAL MAP WITH BRANCHING PATHS AND DEAD ENDS") print("==================================================") print("Path Statistics:") print("- Total path segments: ", path_segments.size()) var main_paths = path_segments.filter(func(seg): return seg.type == "main") var branches = path_segments.filter(func(seg): return seg.type == "branch") var dead_ends = branches.filter(func(seg): return seg.get("is_dead_end", false)) print("- Main paths: ", main_paths.size()) print("- Branches: ", branches.size()) print("- Dead ends: ", dead_ends.size()) print("") print_block_map_with_paths_safe() func print_block_map_with_paths_safe(): print("Block character map with paths (X = paths, W = water):") print("") # Print in smaller chunks var chunk_size = 10 for chunk_start in range(0, map_height, chunk_size): var chunk_end = min(chunk_start + chunk_size, map_height) for y in range(chunk_start, chunk_end): var row_string = "" for x in range(map_width): # Check priority: paths first, then water, then terrain if path_data[y][x]: row_string += "X" elif water_data[y][x]: row_string += "W" else: var value = map_data[y][x] var block_char = get_block_character(value) row_string += block_char print(row_string) # Brief pause between chunks if chunk_end < map_height: await get_tree().process_frame func get_block_character(value: float) -> String: # Use Unicode block characters for different densities if value >= 0.8: return "█" # Full block elif value >= 0.6: return "▓" # Dark shade elif value >= 0.4: return "▒" # Medium shade elif value >= 0.2: return "░" # Light shade else: return " " # Empty space # Enhanced utility functions func is_path_at(x: int, y: int) -> bool: if x >= 0 and x < map_width and y >= 0 and y < map_height: return path_data[y][x] return false func is_water_at(x: int, y: int) -> bool: if x >= 0 and x < map_width and y >= 0 and y < map_height: return water_data[y][x] return false func get_terrain_at(x: int, y: int) -> float: if x >= 0 and x < map_width and y >= 0 and y < map_height: return map_data[y][x] return 0.0 func get_tile_type_at(x: int, y: int) -> String: if is_path_at(x, y): return "path" elif is_water_at(x, y): return "water" else: return "terrain" func get_path_segments() -> Array: return path_segments func get_main_paths() -> Array: return path_segments.filter(func(seg): return seg.type == "main") func get_branches() -> Array: return path_segments.filter(func(seg): return seg.type == "branch") func get_dead_ends() -> Array: var branches = get_branches() return branches.filter(func(seg): return seg.get("is_dead_end", false)) func export_map_as_image(): print("Exporting map as image...") # Calculate image dimensions var image_width = map_width * tile_size var image_height = map_height * tile_size # Create image var image = Image.create(image_width, image_height, false, Image.FORMAT_RGB8) # Generate the image pixel by pixel for map_y in range(map_height): for map_x in range(map_width): var color = get_tile_color(map_x, map_y) # Fill the tile area with the color (each tile is tile_size x tile_size pixels) for pixel_y in range(tile_size): for pixel_x in range(tile_size): var image_x = map_x * tile_size + pixel_x var image_y = map_y * tile_size + pixel_y image.set_pixel(image_x, image_y, color) # Save the image - you can choose different locations: var file_path = "user://" + image_filename print("User data directory: ", OS.get_user_data_dir()) print("Saving image to: ", file_path) var error = image.save_png(file_path) if error == OK: print("Map image saved successfully!") print("Image dimensions: ", image_width, "x", image_height, " pixels") print("Map dimensions: ", map_width, "x", map_height, " tiles") print("Tile size: ", tile_size, "x", tile_size, " pixels per tile") else: print("Error saving image: ", error) func get_tile_color(x: int, y: int) -> Color: if is_path_at(x, y): # Brown color for paths return Color(0.6, 0.4, 0.2) # Brown elif is_water_at(x, y): # Blue color for water return Color(0.2, 0.4, 0.8) # Blue else: # Terrain - dark green to light green based on height var terrain_value = get_terrain_at(x, y) # Create gradient from dark green to light green # Dark green for low values, light green for high values var dark_green = Color(0.1, 0.3, 0.1) # Dark green var light_green = Color(0.4, 0.7, 0.3) # Light green # Interpolate between dark and light green based on terrain value return dark_green.lerp(light_green, terrain_value)