Loads of stuff

This commit is contained in:
Dan Baker 2025-06-23 19:39:55 +01:00
parent f3af522683
commit 66ce3ff503
413 changed files with 14802 additions and 0 deletions

View file

@ -0,0 +1,715 @@
class_name MapGenerationClass
extends Node
# Map settings
@export var map_width: int = 500
@export var map_height: int = 500
# 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():
pass
generate_map()
generate_paths()
generate_water_bodies()
generate_final_map_data()
#print_visual_map_to_console()
#if export_image:
# export_map_as_image()
func generate_final_map_data():
for y in range(map_height):
for x in range(map_width):
MapData.setup_cell_data(y, x, map_data[y][x], is_path_at(x, y), is_water_at(x, y))
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)