Adds basic camp generation and placement

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.
This commit is contained in:
Dan Baker 2025-06-29 14:05:48 +01:00
parent 3959333534
commit a1efaf6294
8 changed files with 402 additions and 125 deletions

View file

@ -5,13 +5,134 @@ extends Resource
@export var x: int = 0
@export var z: int = 0
@export var vegetation_density: float = 0.5
@export var ground_compaction: float = 0.0
@export var water: float = 0
@export var moisture_level: float = 0.6
# Core cell type properties - these will trigger automatic updates
@export var camp: bool = false: set = set_camp
@export var path: bool = false: set = set_trail
@export var water: bool = false: set = set_water
# Dependent properties that get automatically managed
@export var vegetation_density: float = 0.5: set = set_vegetation_density
@export var ground_compaction: float = 0.0: set = set_ground_compaction
@export var moisture_level: float = 0.6
@export var trees: Array = []
# Internal flag to prevent infinite recursion during initialization
var _initializing: bool = false
func _init():
_initializing = true
# Set default values
vegetation_density = 0.5
ground_compaction = 0.0
camp = false
path = false
water = false
_initializing = false
func set_camp(value: bool):
camp = value
if not _initializing:
_update_dependent_properties()
## Using a different name due to Resource using the function set_path in Godot
func set_trail(value: bool):
path = value
if not _initializing:
_update_dependent_properties()
func set_water(value: bool):
water = value
if not _initializing:
_update_dependent_properties()
func set_vegetation_density(value: float):
# Only allow manual setting if not a special cell type
if _initializing or not (camp or path or water):
vegetation_density = value
else:
vegetation_density = 0.0
func set_ground_compaction(value: float):
# Camp always has maximum compaction, others can be set manually
if camp:
ground_compaction = 1.0
elif not _initializing:
ground_compaction = value
func _update_dependent_properties():
# Update vegetation density - always 0 for camp, path, or water
if camp or path or water:
vegetation_density = 0.0
# Update ground compaction - camp always has maximum compaction
if camp:
ground_compaction = 1.0
# Utility function to set cell type and ensure only one is active
func set_cell_type(cell_type: String):
_initializing = true
# Clear all cell types first
camp = false
path = false
water = false
# Set the specified type
match cell_type.to_lower():
"camp":
camp = true
"path":
path = true
"water":
water = true
"terrain", "normal", "":
pass # Leave all false for normal terrain
_:
push_warning("Unknown cell type: " + cell_type)
_initializing = false
_update_dependent_properties()
# Utility function to get the primary cell type
func get_cell_type() -> String:
if camp:
return "camp"
elif path:
return "path"
elif water:
return "water"
else:
return "terrain"
# Check if this is a special cell type (not normal terrain)
func is_special_cell() -> bool:
return camp or path or water
func add_trees(tree: TreeDataResource, qty: int) -> void:
for i in qty:
trees.append(tree)
for i in qty:
trees.append(tree)
# Override vegetation density and ground compaction for special cases
func force_set_vegetation_density(value: float):
# Allows bypassing the automatic management if absolutely needed
vegetation_density = value
func force_set_ground_compaction(value: float):
# Allows bypassing the automatic management if absolutely needed
ground_compaction = value
func should_spawn_trees() -> bool:
# Only spawn trees if this is not a special cell type
return not (camp or path or water) and vegetation_density > 0.0
func should_spawn_grass() -> bool:
# Grass can spawn in any terrain cell, but not in special cells
return not (camp or path or water) and vegetation_density > 0.0
func should_spawn_bushes() -> bool:
# Bushes can spawn in any terrain cell, but not in special cells
return not (camp or path or water) and vegetation_density > 0.1
func should_spawn_flowers() -> bool:
# Flowers can spawn in any terrain cell, but not in special cells
return not (camp or path or water) and vegetation_density > 0.1

View file

@ -14,41 +14,15 @@ var map_data: Array = Global.map_data
## Density 0.1 to 0.6 - add bushes, varying quantity TBD
## Density 0.1 to 0.4 - add flowers, varying quantity TBD
# Weighted random selection based on tree chances
static func select_weighted_tree(tree_preferences: Dictionary):
if tree_preferences.is_empty():
return null
# Calculate total weight
var total_weight = 0.0
for tree_name in tree_preferences.keys():
total_weight += tree_preferences[tree_name]["chance"]
if total_weight <= 0.0:
return null
# Generate random number between 0 and total_weight
var random_value = randf() * total_weight
# Find which tree this random value corresponds to
var cumulative_weight = 0.0
for tree_name in tree_preferences.keys():
cumulative_weight += tree_preferences[tree_name]["chance"]
if random_value <= cumulative_weight:
return tree_preferences[tree_name]["resource"]
# Fallback (shouldn't happen, but just in case)
var first_key = tree_preferences.keys()[0]
return tree_preferences[first_key]["resource"]
# Pre-calculate tree distribution for the cell
static func generate_cell_with_distribution(x: int, z: int, density: float, path: bool = false, water: bool = false):
static func generate_cell_with_distribution(x: int, z: int, density: float, path: bool = false, water: bool = false, camp: bool = false):
var cell_data = CellDataResource.new()
cell_data.x = x
cell_data.z = z
cell_data.vegetation_density = density
cell_data.camp = camp
if not (path or water):
if not (path or water or camp):
if density >= 0.6:
var tree_preferences = BiomeData.calculate_tree_probabilities(x, z)
var tree_distribution = calculate_tree_distribution(tree_preferences, density)

View file

@ -16,6 +16,11 @@ var map_height: int = Global.map_height
@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
@ -32,6 +37,11 @@ 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
@ -39,22 +49,27 @@ 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()
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))
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)
@ -86,14 +101,17 @@ func generate_map():
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)
@ -101,7 +119,66 @@ func generate_map():
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...")
@ -416,6 +493,10 @@ func evaluate_catmull_rom_spline(points: Array, t: float) -> Vector2:
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
@ -440,6 +521,10 @@ func place_organic_path_segment(x: int, y: int, path_id: int, is_main_path: bool
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))
@ -483,7 +568,9 @@ func smooth_paths():
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
# 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:
@ -509,9 +596,9 @@ func generate_single_water_body(body_index: int):
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)
var water_radius = randi_range(min_water_size, max_water_size) - attempts
# Check if this location is valid (no paths in the area)
# 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
@ -523,7 +610,7 @@ func generate_single_water_body(body_index: int):
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
# 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
@ -537,6 +624,12 @@ func is_valid_water_location(center_x: int, center_y: int, radius: int) -> bool:
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))
@ -556,7 +649,7 @@ func place_water_body(center_x: int, center_y: int, radius: int, body_index: int
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
if path_data[y][x] or camp_data[y][x]: # Don't place water on paths or camp
continue
# Calculate distance from center
@ -574,7 +667,7 @@ func place_water_body(center_x: int, center_y: int, radius: int, body_index: int
func print_visual_map_to_console():
print("")
print("==================================================")
print("VISUAL MAP WITH BRANCHING PATHS AND DEAD ENDS")
print("VISUAL MAP WITH BRANCHING PATHS, DEAD ENDS, AND CAMP")
print("==================================================")
print("Path Statistics:")
print("- Total path segments: ", path_segments.size())
@ -585,11 +678,16 @@ func print_visual_map_to_console():
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):")
print("Block character map with paths (X = paths, W = water, C = camp):")
print("")
# Print in smaller chunks
@ -600,8 +698,10 @@ func print_block_map_with_paths_safe():
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]:
# 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"
@ -639,19 +739,35 @@ func is_water_at(x: int, y: int) -> bool:
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_path_at(x, y):
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
@ -687,6 +803,26 @@ func export_map_as_image():
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
@ -700,11 +836,14 @@ func export_map_as_image():
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_path_at(x, y):
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):
@ -720,4 +859,4 @@ func get_tile_color(x: int, y: int) -> Color:
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)
return dark_green.lerp(light_green, terrain_value)