Adds basic camp generation and placement logic to the map generation process. It attempts to place the camp in a valid location, avoiding paths and water bodies. It also sets the player's spawn point to the center of the generated camp, including some basic camp props like a tent, campfire, and bed. Additionally, vegetation spawning is now dependent on the `should_spawn_*` methods of the `CellDataResource`, allowing more control over what spawns where.
862 lines
No EOL
30 KiB
GDScript
862 lines
No EOL
30 KiB
GDScript
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
|
|
|
|
# Camp generation settings
|
|
@export var camp_size: int = 2 # Size of the camp (camp_size x camp_size)
|
|
@export var camp_placement_attempts: int = 100 # Max attempts to place the camp
|
|
@export var camp_buffer_zone: int = 1 # Minimum distance from camp to other features
|
|
|
|
# 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
|
|
var camp_data: Array = [] # Separate array to track camp
|
|
|
|
# Camp position storage
|
|
var camp_position: Vector2 = Vector2(-1, -1) # Top-left corner of the camp
|
|
var camp_center: Vector2 = Vector2(-1, -1) # Center of the camp
|
|
|
|
# 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_camp()
|
|
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), is_camp_at(x, y))
|
|
|
|
Log.pr(camp_center)
|
|
Global.spawn_point = Vector3(camp_position.x * 2, 0, camp_position.y * 2) # Set spawn point to camp center
|
|
Log.pr(Global.spawn_point)
|
|
|
|
# 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)
|
|
camp_data.resize(map_height)
|
|
|
|
for y in range(map_height):
|
|
map_data[y] = []
|
|
path_data[y] = []
|
|
water_data[y] = []
|
|
camp_data[y] = []
|
|
map_data[y].resize(map_width)
|
|
path_data[y].resize(map_width)
|
|
water_data[y].resize(map_width)
|
|
camp_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
|
|
camp_data[y][x] = false # Initialize camp data
|
|
|
|
func generate_camp():
|
|
print("Generating camp...")
|
|
|
|
var attempts = 0
|
|
var placed = false
|
|
|
|
while attempts < camp_placement_attempts and not placed:
|
|
# Random position with margin to ensure camp fits within map bounds
|
|
var margin = camp_size + camp_buffer_zone
|
|
var camp_x = randi_range(margin, map_width - margin - camp_size)
|
|
var camp_y = randi_range(margin, map_height - margin - camp_size)
|
|
|
|
# Check if this location is valid for camp placement
|
|
if is_valid_camp_location(camp_x, camp_y):
|
|
place_camp(camp_x, camp_y)
|
|
placed = true
|
|
print("Camp placed at (", camp_x, ",", camp_y, ") with size ", camp_size, "x", camp_size)
|
|
|
|
attempts += 1
|
|
|
|
if not placed:
|
|
print("Could not place camp after ", camp_placement_attempts, " attempts")
|
|
# Fallback: place camp in the center of the map
|
|
var fallback_x = (map_width - camp_size) / 2
|
|
var fallback_y = (map_height - camp_size) / 2
|
|
place_camp(fallback_x, fallback_y)
|
|
print("Camp placed at fallback location (", fallback_x, ",", fallback_y, ")")
|
|
|
|
func is_valid_camp_location(camp_x: int, camp_y: int) -> bool:
|
|
# Check if the camp area and buffer zone are clear
|
|
var check_size = camp_size + (camp_buffer_zone * 2)
|
|
var start_x = camp_x - camp_buffer_zone
|
|
var start_y = camp_y - camp_buffer_zone
|
|
|
|
for y in range(start_y, start_y + check_size):
|
|
for x in range(start_x, start_x + check_size):
|
|
if x >= 0 and x < map_width and y >= 0 and y < map_height:
|
|
# For now, just check if we're within map bounds
|
|
# Later we'll prevent paths and water from being placed here
|
|
continue
|
|
else:
|
|
# Outside map bounds
|
|
return false
|
|
|
|
return true
|
|
|
|
func place_camp(camp_x: int, camp_y: int):
|
|
# Store camp position
|
|
camp_position = Vector2(camp_x, camp_y)
|
|
camp_center = Vector2(camp_x + camp_size / 2.0, camp_y + camp_size / 2.0)
|
|
|
|
Log.pr("Placing camp at position: ", camp_position, " with center: ", camp_center)
|
|
|
|
# Mark camp area in camp_data
|
|
for y in range(camp_y, camp_y + camp_size):
|
|
for x in range(camp_x, camp_x + camp_size):
|
|
if x >= 0 and x < map_width and y >= 0 and y < map_height:
|
|
camp_data[y][x] = true
|
|
|
|
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):
|
|
# Don't place paths in camp area
|
|
if is_camp_at(x, y):
|
|
return
|
|
|
|
# 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:
|
|
# Don't place paths in camp area
|
|
if is_camp_at(nx, ny):
|
|
continue
|
|
|
|
# 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:
|
|
# Don't place paths in camp area
|
|
if not is_camp_at(interp_x, interp_y):
|
|
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) - attempts
|
|
|
|
# Check if this location is valid (no paths or camp 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 or camp 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
|
|
|
|
# Check if this position has camp
|
|
if camp_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] or camp_data[y][x]: # Don't place water on paths or camp
|
|
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, DEAD ENDS, AND CAMP")
|
|
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("Camp Statistics:")
|
|
print("- Camp position: (", camp_position.x, ",", camp_position.y, ")")
|
|
print("- Camp center: (", camp_center.x, ",", camp_center.y, ")")
|
|
print("- Camp size: ", camp_size, "x", camp_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, C = camp):")
|
|
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: camp first, then paths, then water, then terrain
|
|
if camp_data[y][x]:
|
|
row_string += "C"
|
|
elif 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 is_camp_at(x: int, y: int) -> bool:
|
|
if x >= 0 and x < map_width and y >= 0 and y < map_height:
|
|
return camp_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_camp_at(x, y):
|
|
return "camp"
|
|
elif is_path_at(x, y):
|
|
return "path"
|
|
elif is_water_at(x, y):
|
|
return "water"
|
|
else:
|
|
return "terrain"
|
|
|
|
func get_camp_position() -> Vector2:
|
|
return camp_position
|
|
|
|
func get_camp_center() -> Vector2:
|
|
return camp_center
|
|
|
|
func get_camp_size() -> int:
|
|
return camp_size
|
|
|
|
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)
|
|
|
|
# Draw camp as a red dot at the center
|
|
if camp_position.x >= 0 and camp_position.y >= 0:
|
|
var camp_center_pixel_x = int(camp_center.x * tile_size)
|
|
var camp_center_pixel_y = int(camp_center.y * tile_size)
|
|
|
|
# Draw a red dot (3x3 pixels minimum, scaled with tile size)
|
|
var dot_size = 20
|
|
var half_dot = dot_size / 2
|
|
|
|
for dy in range(-half_dot, half_dot + 1):
|
|
for dx in range(-half_dot, half_dot + 1):
|
|
var pixel_x = camp_center_pixel_x + dx
|
|
var pixel_y = camp_center_pixel_y + dy
|
|
|
|
# Make sure we're within image bounds
|
|
if pixel_x >= 0 and pixel_x < image_width and pixel_y >= 0 and pixel_y < image_height:
|
|
# Create a circular dot
|
|
if dx * dx + dy * dy <= half_dot * half_dot:
|
|
image.set_pixel(pixel_x, pixel_y, Color.RED)
|
|
|
|
# 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")
|
|
print("Camp represented as red dot at center: (", camp_center.x, ",", camp_center.y, ")")
|
|
else:
|
|
print("Error saving image: ", error)
|
|
|
|
func get_tile_color(x: int, y: int) -> Color:
|
|
if is_camp_at(x, y):
|
|
return Color(0.4, 0.7, 0.3)
|
|
elif 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) |