4454 lines
132 KiB
GDScript
4454 lines
132 KiB
GDScript
class_name TerrainChunkGenerator
|
|
extends Node3D
|
|
|
|
signal generation_completed(summary: Dictionary)
|
|
|
|
const CONSTRAINT_PROFILE_QUANTIZATION := 0.001
|
|
const CANDIDATE_WEIGHT_SCALE := 10000.0
|
|
const LARGE_GRID_LIGHTWEIGHT_THRESHOLD := 128
|
|
const LARGE_GRID_PROPAGATION_INTERVAL := 8
|
|
const ELEVATED_FEATURE_RADIUS := 2
|
|
const ELEVATED_FEATURE_CLEARANCE_RADIUS := ELEVATED_FEATURE_RADIUS + 1
|
|
const ELEVATED_FEATURE_BOUNDARY_MARGIN := ELEVATED_FEATURE_CLEARANCE_RADIUS + 1
|
|
const SECONDARY_ELEVATED_FEATURE_RADIUS := 1
|
|
const SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS := (
|
|
SECONDARY_ELEVATED_FEATURE_RADIUS + 1
|
|
)
|
|
const LAKE_FEATURE_CLEARANCE := 1
|
|
const RIVER_FEATURE_CLEARANCE := 1
|
|
# Keep packed domain bits below the signed 64-bit sign bit. Catalogs larger
|
|
# than this remain correct through the unpacked solver path.
|
|
const MAX_PACKED_SOLVER_VARIANTS := 62
|
|
const PROJECTED_GRASS_MATERIAL: Material = preload(
|
|
"res://world/materials/generated_terrain_grass.tres"
|
|
)
|
|
const PROJECTED_SAND_MATERIAL: Material = preload(
|
|
"res://world/materials/generated_terrain_sand.tres"
|
|
)
|
|
const PROJECTED_DIRT_MATERIAL: Material = preload(
|
|
"res://world/materials/generated_terrain_dirt.tres"
|
|
)
|
|
|
|
@export var catalog: TerrainChunkCatalog
|
|
@export var grid_size := Vector2i(7, 7)
|
|
@export var generation_seed := 13001
|
|
@export var generate_on_ready := true
|
|
@export var build_collision := true
|
|
@export_range(1, 16, 1) var collision_batch_size := 4
|
|
@export var show_chunk_labels := false
|
|
@export var force_center_chunk_id: StringName = &"chunk_0000"
|
|
@export var required_chunk_ids := PackedStringArray()
|
|
@export_group("Grass-Sand Smoothing")
|
|
@export var grass_sand_smoothing_enabled := false
|
|
@export var smoothing_grass_chunk_id: StringName = &"chunk_0000"
|
|
@export var smoothing_sand_chunk_id: StringName = &"chunk_0001"
|
|
@export var smoothing_diagonal_chunk_id: StringName = &"chunk_0013"
|
|
@export_range(1, 64, 1) var maximum_smoothing_placements := 16
|
|
@export_group("")
|
|
@export_group("Freshwater Lake Feature")
|
|
@export var lake_feature_enabled := false
|
|
@export var lake_edge_chunk_id: StringName = &"chunk_0017"
|
|
@export var lake_edge_variant_chunk_id: StringName = &"chunk_0037"
|
|
@export var lake_corner_chunk_id: StringName = &"chunk_0018"
|
|
@export var lake_fill_chunk_id: StringName = &"chunk_0019"
|
|
@export var lake_diagonal_chunk_id: StringName = &"chunk_0020"
|
|
@export var lake_narrow_edge_chunk_id: StringName = &"chunk_0021"
|
|
@export var lake_regular_to_narrow_chunk_id: StringName = &"chunk_0022"
|
|
@export var lake_narrow_to_regular_chunk_id: StringName = &"chunk_0023"
|
|
@export_range(3, 8, 1) var lake_minimum_length := 3
|
|
@export_range(3, 8, 1) var lake_maximum_length := 5
|
|
@export_range(5, 8, 1) var lake_minimum_size := 5
|
|
@export_range(5, 8, 1) var lake_maximum_size := 6
|
|
@export_group("")
|
|
@export_group("Freshwater River Feature")
|
|
@export var river_feature_enabled := false
|
|
@export var river_edge_chunk_id: StringName = &"chunk_0024"
|
|
@export var river_edge_variant_chunk_id: StringName = &"chunk_0025"
|
|
@export var river_stone_bridge_chunk_id: StringName = &"chunk_0038"
|
|
@export var river_source_right_chunk_id: StringName = &"chunk_0029"
|
|
@export var river_source_left_chunk_id: StringName = &"chunk_0030"
|
|
@export var river_outlet_right_chunk_id: StringName = &"chunk_0031"
|
|
@export var river_outlet_left_chunk_id: StringName = &"chunk_0032"
|
|
@export var river_bend_c_chunk_id: StringName = &"chunk_0033"
|
|
@export var river_bend_b_chunk_id: StringName = &"chunk_0034"
|
|
@export var river_bend_a_chunk_id: StringName = &"chunk_0035"
|
|
@export var river_bend_d_chunk_id: StringName = &"chunk_0036"
|
|
@export_range(5, 8, 1) var river_minimum_length := 5
|
|
@export_range(5, 8, 1) var river_maximum_length := 5
|
|
@export_group("")
|
|
@export_group("Elevated Inland Feature")
|
|
@export var elevated_cliff_feature_enabled := false
|
|
@export var elevated_cliff_base_chunk_id: StringName = &"chunk_0000"
|
|
@export var elevated_cliff_top_chunk_id: StringName = &"chunk_0009"
|
|
@export var elevated_cliff_corner_chunk_id: StringName = &"chunk_0010"
|
|
@export var elevated_cliff_edge_chunk_id: StringName = &"chunk_0011"
|
|
@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012"
|
|
@export var elevated_cliff_third_tier_edge_chunk_id: StringName = &"chunk_0026"
|
|
@export var elevated_cliff_third_tier_corner_chunk_id: StringName = &"chunk_0027"
|
|
@export var elevated_cliff_third_tier_top_chunk_id: StringName = &"chunk_0028"
|
|
@export var elevated_cliff_sea_edge_chunk_id: StringName = &"chunk_0014"
|
|
@export var elevated_cliff_sea_corner_chunk_id: StringName = &"chunk_0015"
|
|
@export var elevated_cliff_sea_transition_right_chunk_id: StringName = &"chunk_9999"
|
|
@export var elevated_cliff_sea_transition_left_chunk_id: StringName = &"chunk_9998"
|
|
@export var elevated_cliff_coast_base_chunk_id: StringName = &"chunk_0005"
|
|
@export var elevated_cliff_beach_base_chunk_id: StringName = &"chunk_0002"
|
|
@export var elevated_cliff_beach_transition_right_chunk_id: StringName = &"chunk_9997"
|
|
@export var elevated_cliff_beach_transition_left_chunk_id: StringName = &"chunk_9996"
|
|
## Generated worlds can require the secondary coastal assembly so every
|
|
## island exposes at least one authored cliff face directly to the ocean.
|
|
@export var elevated_cliff_coastal_feature_required := false
|
|
@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 1.0
|
|
## Third-tier cliffs rise four meters in a single authored piece and have no
|
|
## ramp. They remain less common than the accessible second-tier feature.
|
|
@export_range(0.0, 1.0, 0.05) var elevated_cliff_third_tier_base_chance := 0.3
|
|
## A smaller upper tier is nested into the main cliff assembly rather than
|
|
## occupying or replacing additional terrain-grid cells.
|
|
@export_range(0.0, 1.0, 0.05) var elevated_cliff_third_level_chance := 0.7
|
|
## A second-tier base may carry the taller authored cliff set above it.
|
|
@export_range(0.0, 1.0, 0.05) var elevated_cliff_third_tier_stack_chance := 0.2
|
|
## Stacking the taller set on another taller base is intentionally rare.
|
|
@export_range(0.0, 1.0, 0.01) var elevated_cliff_double_third_tier_chance := 0.05
|
|
@export_range(0.1, 10.0, 0.1) var elevated_cliff_level_height := 2.0
|
|
@export_group("")
|
|
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
|
|
@export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6
|
|
@export_range(0.01, 1.0, 0.01) var long_repeat_weight_multiplier := 0.3
|
|
@export_range(1.0, 10.0, 0.1) var boundary_preference_multiplier := 4.0
|
|
@export_range(0.001, 0.1, 0.001) var edge_match_tolerance := 0.01
|
|
@export_range(1, 100000, 1) var maximum_backtracks := 20000
|
|
|
|
# Every allowed authored rotation remains available for visual placement.
|
|
var _variants: Array[TerrainChunkVariant] = []
|
|
# Rotations with identical terrain edges and connector directions share one
|
|
# solver candidate, then resolve back to an authored rotation after solving.
|
|
var _solver_variants: Array[TerrainChunkVariant] = []
|
|
var _solver_definitions: Array[TerrainChunkDefinition] = []
|
|
var _solver_variants_by_definition: Dictionary[StringName, Array] = {}
|
|
var _definition_variant_masks: Dictionary[StringName, int] = {}
|
|
var _required_neighbor_variant_masks: Dictionary[StringName, int] = {}
|
|
var _equivalent_rotations: Dictionary[String, PackedInt32Array] = {}
|
|
var _solver_variant_indices: Dictionary = {}
|
|
var _edge_compatibility := PackedByteArray()
|
|
var _edge_compatibility_masks := PackedInt64Array()
|
|
var _static_cell_candidate_masks := PackedInt64Array()
|
|
var _neighbor_indices: Array[PackedInt32Array] = []
|
|
var _placements: Array[TerrainChunkVariant] = []
|
|
var _placement_counts: Dictionary[StringName, int] = {}
|
|
var _unfilled_cells := 0
|
|
var _required_stable_ids: Array[StringName] = []
|
|
var _selection_missing_required_count := 0
|
|
var _selection_missing_required_mask := 0
|
|
var _random := RandomNumberGenerator.new()
|
|
var _generated_chunks: Node3D
|
|
var _backtrack_count := 0
|
|
var _elevated_feature_center := Vector2i(-1, -1)
|
|
var _secondary_elevated_feature_center := Vector2i(-1, -1)
|
|
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
|
var _lake_feature_origin := Vector2i(-1, -1)
|
|
var _lake_feature_footprint_size := Vector2i.ZERO
|
|
var _lake_feature_uses_fill := false
|
|
var _lake_feature_uses_diagonal_perimeter := false
|
|
var _lake_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
|
var _river_feature_origin := Vector2i(-1, -1)
|
|
var _river_feature_footprint_size := Vector2i.ZERO
|
|
var _river_feature_flow_direction := Vector2i.ZERO
|
|
var _river_feature_outlet_direction := Vector2i.ZERO
|
|
var _river_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
|
var _river_feature_candidate_order: Array[Dictionary] = []
|
|
var _river_feature_candidate_index := -1
|
|
var _river_feature_placements: Array[Dictionary] = []
|
|
var _river_feature_coordinates: Dictionary[Vector2i, bool] = {}
|
|
var _river_feature_source_coordinates: Array[Vector2i] = []
|
|
var _stacked_elevated_placements: Array[Dictionary] = []
|
|
|
|
|
|
func _ready() -> void:
|
|
if generate_on_ready:
|
|
call_deferred(&"generate")
|
|
|
|
|
|
func generate() -> bool:
|
|
if not _prepare_catalog():
|
|
return false
|
|
var previous_placements: Array[TerrainChunkVariant] = []
|
|
previous_placements.assign(_placements)
|
|
_random.seed = generation_seed
|
|
_backtrack_count = 0
|
|
_reset_placements(grid_size.x * grid_size.y)
|
|
if not _prepare_elevated_feature_region():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
if not _solve_prepared_base_layout():
|
|
_assign_placements(previous_placements)
|
|
push_error(
|
|
"Terrain generation could not solve a %dx%d grid with seed %d."
|
|
% [grid_size.x, grid_size.y, generation_seed]
|
|
)
|
|
return false
|
|
_resolve_equivalent_rotations()
|
|
if not _apply_grass_sand_smoothing():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
if not _apply_lake_feature():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
if not _apply_elevated_feature():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
if not _apply_river_feature():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
if not _configure_stacked_elevated_feature():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
var solution_root := _build_solution_root()
|
|
if solution_root == null:
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
_replace_generated_chunks(solution_root)
|
|
var summary := _build_summary()
|
|
generation_completed.emit(summary)
|
|
return true
|
|
|
|
|
|
func _solve_prepared_base_layout() -> bool:
|
|
if _solve_cell(0):
|
|
return true
|
|
while _select_next_river_feature_region():
|
|
_random.seed = generation_seed
|
|
_backtrack_count = 0
|
|
_reset_placements(grid_size.x * grid_size.y)
|
|
_build_grid_solver_caches()
|
|
if _solve_cell(0):
|
|
return true
|
|
return false
|
|
|
|
|
|
func generate_from_placement_keys(keys: PackedStringArray) -> bool:
|
|
if not _prepare_catalog():
|
|
return false
|
|
if keys.size() != grid_size.x * grid_size.y:
|
|
push_error(
|
|
"Terrain layout contains %d cells; expected %d."
|
|
% [keys.size(), grid_size.x * grid_size.y]
|
|
)
|
|
return false
|
|
var variants_by_key: Dictionary[String, TerrainChunkVariant] = {}
|
|
for variant: TerrainChunkVariant in _variants:
|
|
variants_by_key[variant.stable_key()] = variant
|
|
var resolved: Array[TerrainChunkVariant] = []
|
|
for key: String in keys:
|
|
var variant: TerrainChunkVariant = variants_by_key.get(key)
|
|
if variant == null:
|
|
push_error("Terrain layout references unknown variant %s." % key)
|
|
return false
|
|
resolved.append(variant)
|
|
var validation_error := _resolved_layout_validation_error(resolved)
|
|
if not validation_error.is_empty():
|
|
push_error("Terrain layout is invalid: " + validation_error)
|
|
return false
|
|
|
|
var previous_placements: Array[TerrainChunkVariant] = []
|
|
previous_placements.assign(_placements)
|
|
_assign_placements(resolved)
|
|
_backtrack_count = 0
|
|
if not _configure_stacked_elevated_feature():
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
var solution_root := _build_solution_root()
|
|
if solution_root == null:
|
|
_assign_placements(previous_placements)
|
|
return false
|
|
_replace_generated_chunks(solution_root)
|
|
var summary := _build_summary()
|
|
generation_completed.emit(summary)
|
|
return true
|
|
|
|
|
|
func _resolved_layout_validation_error(
|
|
layout: Array[TerrainChunkVariant],
|
|
) -> String:
|
|
var counts: Dictionary[StringName, int] = {}
|
|
for variant: TerrainChunkVariant in layout:
|
|
if variant == null:
|
|
return "one or more cells are empty."
|
|
var stable_id := variant.definition.stable_id
|
|
counts[stable_id] = counts.get(stable_id, 0) + 1
|
|
var maximum := variant.definition.maximum_placements
|
|
if maximum >= 0 and counts[stable_id] > maximum:
|
|
return "%s exceeds its placement limit." % stable_id
|
|
for required_id_value: String in required_chunk_ids:
|
|
var required_id := StringName(required_id_value)
|
|
if counts.get(required_id, 0) <= 0:
|
|
return "required chunk %s is missing." % required_id
|
|
if force_center_chunk_id != &"":
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
var center_index := center.y * grid_size.x + center.x
|
|
if layout[center_index].definition.stable_id != force_center_chunk_id:
|
|
return "the forced center chunk is missing from the center cell."
|
|
for index: int in layout.size():
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var current := layout[index]
|
|
if not _variant_respects_ocean_boundary(current, coordinate):
|
|
return (
|
|
"cell %d does not exactly match the authored ocean boundary."
|
|
% index
|
|
)
|
|
if coordinate.x + 1 < grid_size.x:
|
|
var east := layout[index + 1]
|
|
if not _edges_are_compatible(
|
|
current,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
east,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
):
|
|
return (
|
|
"cells %d (%s) and %d (%s) have incompatible "
|
|
+ "east/west edges."
|
|
) % [
|
|
index,
|
|
current.stable_key(),
|
|
index + 1,
|
|
east.stable_key(),
|
|
]
|
|
if coordinate.y + 1 < grid_size.y:
|
|
var south := layout[index + grid_size.x]
|
|
if not _edges_are_compatible(
|
|
current,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
south,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
):
|
|
return (
|
|
"cells %d (%s) and %d (%s) have incompatible "
|
|
+ "south/north edges."
|
|
) % [
|
|
index,
|
|
current.stable_key(),
|
|
index + grid_size.x,
|
|
south.stable_key(),
|
|
]
|
|
var neighbor_error := _neighbor_requirement_validation_error(layout)
|
|
if not neighbor_error.is_empty():
|
|
return neighbor_error
|
|
return _walkable_connectivity_validation_error(layout)
|
|
|
|
|
|
func _prepare_catalog() -> bool:
|
|
if catalog == null:
|
|
push_error("TerrainChunkGenerator requires a catalog.")
|
|
return false
|
|
if grid_size.x <= 0 or grid_size.y <= 0:
|
|
push_error("TerrainChunkGenerator grid dimensions must be positive.")
|
|
return false
|
|
var catalog_errors := catalog.validation_errors()
|
|
if not catalog_errors.is_empty():
|
|
push_error("Terrain chunk catalog is invalid:\n" + "\n".join(catalog_errors))
|
|
return false
|
|
if not _validate_generation_requirements():
|
|
return false
|
|
|
|
_variants.clear()
|
|
_solver_variants.clear()
|
|
_solver_definitions.clear()
|
|
_solver_variants_by_definition.clear()
|
|
_definition_variant_masks.clear()
|
|
_required_neighbor_variant_masks.clear()
|
|
_equivalent_rotations.clear()
|
|
_solver_variant_indices.clear()
|
|
for definition: TerrainChunkDefinition in catalog.definitions:
|
|
var analyzed := TerrainChunkAnalyzer.create_variants(
|
|
definition,
|
|
catalog.chunk_size,
|
|
)
|
|
if analyzed.is_empty():
|
|
push_error(
|
|
"Terrain chunk %s produced no usable rotation variants."
|
|
% definition.stable_id
|
|
)
|
|
return false
|
|
for variant: TerrainChunkVariant in analyzed:
|
|
_variants.append(variant)
|
|
if (
|
|
definition.overlay_only
|
|
or not definition.participates_in_base_solver
|
|
):
|
|
continue
|
|
var constraint_key := _constraint_key(variant)
|
|
var rotations: PackedInt32Array = _equivalent_rotations.get(
|
|
constraint_key,
|
|
PackedInt32Array(),
|
|
)
|
|
rotations.append(variant.quarter_turns)
|
|
_equivalent_rotations[constraint_key] = rotations
|
|
if rotations.size() == 1:
|
|
_solver_variants.append(variant)
|
|
if _variants.is_empty():
|
|
push_error("Terrain chunk catalog produced no usable variants.")
|
|
return false
|
|
if _solver_variants.is_empty():
|
|
push_error("Terrain chunk catalog produced no base-terrain solver variants.")
|
|
return false
|
|
for index: int in _solver_variants.size():
|
|
var variant := _solver_variants[index]
|
|
_solver_variant_indices[variant] = index
|
|
var definition_variants: Array = _solver_variants_by_definition.get(
|
|
variant.definition.stable_id,
|
|
[],
|
|
)
|
|
definition_variants.append(variant)
|
|
if definition_variants.size() == 1:
|
|
_solver_definitions.append(variant.definition)
|
|
_solver_variants_by_definition[
|
|
variant.definition.stable_id
|
|
] = definition_variants
|
|
if index < MAX_PACKED_SOLVER_VARIANTS:
|
|
_definition_variant_masks[variant.definition.stable_id] = (
|
|
int(
|
|
_definition_variant_masks.get(
|
|
variant.definition.stable_id,
|
|
0,
|
|
)
|
|
)
|
|
| (1 << index)
|
|
)
|
|
_build_edge_compatibility_cache()
|
|
return true
|
|
|
|
|
|
func _reset_placements(cell_count: int) -> void:
|
|
_placements.clear()
|
|
_placements.resize(cell_count)
|
|
_placement_counts.clear()
|
|
_unfilled_cells = cell_count
|
|
|
|
|
|
func _assign_placements(layout: Array[TerrainChunkVariant]) -> void:
|
|
_placements.assign(layout)
|
|
_rebuild_placement_tracking()
|
|
|
|
|
|
func _rebuild_placement_tracking() -> void:
|
|
_placement_counts.clear()
|
|
_unfilled_cells = 0
|
|
for placement: TerrainChunkVariant in _placements:
|
|
if placement == null:
|
|
_unfilled_cells += 1
|
|
continue
|
|
_adjust_placement_count(placement.definition.stable_id, 1)
|
|
|
|
|
|
func _set_placement(index: int, placement: TerrainChunkVariant) -> void:
|
|
var previous := _placements[index]
|
|
if previous == placement:
|
|
return
|
|
if previous == null:
|
|
_unfilled_cells -= 1
|
|
else:
|
|
_adjust_placement_count(previous.definition.stable_id, -1)
|
|
_placements[index] = placement
|
|
if placement == null:
|
|
_unfilled_cells += 1
|
|
else:
|
|
_adjust_placement_count(placement.definition.stable_id, 1)
|
|
|
|
|
|
func _adjust_placement_count(stable_id: StringName, difference: int) -> void:
|
|
var updated := int(_placement_counts.get(stable_id, 0)) + difference
|
|
if updated <= 0:
|
|
_placement_counts.erase(stable_id)
|
|
return
|
|
_placement_counts[stable_id] = updated
|
|
|
|
|
|
func _constraint_key(variant: TerrainChunkVariant) -> String:
|
|
return "%s|%s" % [
|
|
variant.definition.stable_id,
|
|
variant.constraint_signature(CONSTRAINT_PROFILE_QUANTIZATION),
|
|
]
|
|
|
|
|
|
func _build_edge_compatibility_cache() -> void:
|
|
var variant_count := _solver_variants.size()
|
|
_edge_compatibility.resize(variant_count * 4 * variant_count)
|
|
_edge_compatibility.fill(0)
|
|
_edge_compatibility_masks.resize(
|
|
variant_count * 4 if _packed_solver_domains_are_available() else 0
|
|
)
|
|
_edge_compatibility_masks.fill(0)
|
|
for first_index: int in variant_count:
|
|
var first := _solver_variants[first_index]
|
|
for first_edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var first_edge := first_edge_value as TerrainChunkTopology.Edge
|
|
var second_edge := TerrainChunkTopology.opposite_edge(first_edge)
|
|
for second_index: int in variant_count:
|
|
var second := _solver_variants[second_index]
|
|
var cache_index := _edge_compatibility_index(
|
|
first_index,
|
|
first_edge,
|
|
second_index,
|
|
)
|
|
var compatible := _calculate_edge_compatibility(
|
|
first,
|
|
first_edge,
|
|
second,
|
|
second_edge,
|
|
)
|
|
_edge_compatibility[cache_index] = int(compatible)
|
|
if compatible and _packed_solver_domains_are_available():
|
|
var mask_index := first_index * 4 + int(first_edge)
|
|
_edge_compatibility_masks[mask_index] = (
|
|
_edge_compatibility_masks[mask_index]
|
|
| (1 << second_index)
|
|
)
|
|
|
|
|
|
func _edge_compatibility_index(
|
|
first_index: int,
|
|
first_edge: TerrainChunkTopology.Edge,
|
|
second_index: int,
|
|
) -> int:
|
|
var variant_count := _solver_variants.size()
|
|
return (
|
|
(first_index * 4 + int(first_edge)) * variant_count
|
|
+ second_index
|
|
)
|
|
|
|
|
|
func _validate_generation_requirements() -> bool:
|
|
var seen_required: Dictionary[StringName, bool] = {}
|
|
_required_stable_ids.clear()
|
|
for required_id_value: String in required_chunk_ids:
|
|
var required_id := StringName(required_id_value)
|
|
if seen_required.has(required_id):
|
|
continue
|
|
seen_required[required_id] = true
|
|
_required_stable_ids.append(required_id)
|
|
var definition := catalog.definition_for_id(required_id)
|
|
if definition == null:
|
|
push_error("Required terrain chunk %s is not in the catalog." % required_id)
|
|
return false
|
|
if definition.maximum_placements == 0:
|
|
push_error("Required terrain chunk %s permits no placements." % required_id)
|
|
return false
|
|
if (
|
|
definition.overlay_only
|
|
or not definition.participates_in_base_solver
|
|
):
|
|
push_error(
|
|
"Required terrain chunk %s does not participate in the base solver."
|
|
% required_id
|
|
)
|
|
return false
|
|
if seen_required.size() > grid_size.x * grid_size.y:
|
|
push_error("The terrain grid has fewer cells than required chunk IDs.")
|
|
return false
|
|
if force_center_chunk_id != &"":
|
|
var center_definition := catalog.definition_for_id(force_center_chunk_id)
|
|
if center_definition == null:
|
|
push_error(
|
|
"Forced center terrain chunk %s is not in the catalog."
|
|
% force_center_chunk_id
|
|
)
|
|
return false
|
|
if center_definition.maximum_placements == 0:
|
|
push_error(
|
|
"Forced center terrain chunk %s permits no placements."
|
|
% force_center_chunk_id
|
|
)
|
|
return false
|
|
return true
|
|
|
|
|
|
func _solve_cell(placed_count: int) -> bool:
|
|
if placed_count >= _placements.size():
|
|
return (
|
|
_required_chunks_are_present()
|
|
and _neighbor_requirement_validation_error(_placements).is_empty()
|
|
and _walkable_connectivity_validation_error(_placements).is_empty()
|
|
)
|
|
if _backtrack_count >= maximum_backtracks:
|
|
return false
|
|
_refresh_selection_required_cache()
|
|
if not _requirements_can_still_be_satisfied():
|
|
return false
|
|
var next_cell := _select_next_cell(placed_count)
|
|
var index := int(next_cell.get("index", -1))
|
|
if index < 0:
|
|
return false
|
|
var candidates: Array[TerrainChunkVariant] = []
|
|
candidates.assign(next_cell.get("candidates", []))
|
|
for candidate: TerrainChunkVariant in _weighted_candidate_order(
|
|
candidates,
|
|
index,
|
|
):
|
|
_set_placement(index, candidate)
|
|
if _solve_cell(placed_count + 1):
|
|
return true
|
|
_set_placement(index, null)
|
|
_backtrack_count += 1
|
|
if _backtrack_count >= maximum_backtracks:
|
|
break
|
|
return false
|
|
|
|
|
|
func _refresh_selection_required_cache() -> void:
|
|
_selection_missing_required_count = 0
|
|
_selection_missing_required_mask = 0
|
|
for stable_id: StringName in _required_stable_ids:
|
|
if int(_placement_counts.get(stable_id, 0)) > 0:
|
|
continue
|
|
_selection_missing_required_count += 1
|
|
_selection_missing_required_mask |= int(
|
|
_definition_variant_masks.get(stable_id, 0)
|
|
)
|
|
|
|
|
|
func _select_next_cell(placed_count: int) -> Dictionary:
|
|
if _packed_solver_domains_are_available():
|
|
if (
|
|
_placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD
|
|
and placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0
|
|
):
|
|
return _select_next_large_grid_cell()
|
|
return _select_next_large_grid_propagated_cell()
|
|
if _placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD:
|
|
if placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0:
|
|
return _select_next_large_grid_cell_unpacked()
|
|
# Rebuild and propagate the small domain table on every branch. This catches
|
|
# unsupported connector chains before they turn into a deep recursive dead
|
|
# end, while keeping placement state simple and deterministic.
|
|
var domains: Dictionary = {}
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
var candidates := _compatible_candidates(index)
|
|
if candidates.is_empty():
|
|
return {"index": index, "candidates": candidates}
|
|
domains[index] = candidates
|
|
if not _propagate_domains(domains):
|
|
return {"index": -1, "candidates": []}
|
|
|
|
# Minimum-remaining-values traversal exposes contradictions before a long
|
|
# row-major branch has already filled most of the map.
|
|
var best_index := -1
|
|
var best_candidates: Array[TerrainChunkVariant] = []
|
|
for index_value: Variant in domains:
|
|
var index := int(index_value)
|
|
var candidates: Array[TerrainChunkVariant] = []
|
|
candidates.assign(domains[index])
|
|
if best_index < 0 or candidates.size() < best_candidates.size():
|
|
best_index = index
|
|
best_candidates = candidates
|
|
if candidates.size() == 1:
|
|
break
|
|
return {"index": best_index, "candidates": best_candidates}
|
|
|
|
|
|
func _select_next_large_grid_propagated_cell() -> Dictionary:
|
|
var domains: Dictionary[int, int] = {}
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
var candidate_mask := _compatible_candidate_mask(index)
|
|
if candidate_mask == 0:
|
|
return {"index": index, "candidates": []}
|
|
domains[index] = candidate_mask
|
|
if not _propagate_domain_masks(domains):
|
|
return {"index": -1, "candidates": []}
|
|
|
|
var best_index := -1
|
|
var best_mask := 0
|
|
var best_size := _solver_variants.size() + 1
|
|
for index: int in domains:
|
|
var candidate_mask := domains[index]
|
|
var candidate_count := _mask_bit_count(candidate_mask)
|
|
if best_index < 0 or candidate_count < best_size:
|
|
best_index = index
|
|
best_mask = candidate_mask
|
|
best_size = candidate_count
|
|
if candidate_count == 1:
|
|
break
|
|
return {
|
|
"index": best_index,
|
|
"candidates": _candidates_from_mask(best_mask),
|
|
}
|
|
|
|
|
|
func _candidates_from_mask(candidate_mask: int) -> Array[TerrainChunkVariant]:
|
|
var result: Array[TerrainChunkVariant] = []
|
|
for candidate_index: int in _solver_variants.size():
|
|
if (candidate_mask & (1 << candidate_index)) != 0:
|
|
result.append(_solver_variants[candidate_index])
|
|
return result
|
|
|
|
|
|
func _mask_bit_count(candidate_mask: int) -> int:
|
|
var count := 0
|
|
var remaining := candidate_mask
|
|
while remaining != 0:
|
|
remaining &= remaining - 1
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _propagate_domain_masks(domains: Dictionary[int, int]) -> bool:
|
|
var changed := true
|
|
while changed:
|
|
changed = false
|
|
for index: int in domains.keys():
|
|
var candidates := domains[index]
|
|
var supported := 0
|
|
for candidate_index: int in _solver_variants.size():
|
|
var candidate_bit := 1 << candidate_index
|
|
if (
|
|
(candidates & candidate_bit) != 0
|
|
and _domain_candidate_has_mask_support(
|
|
candidate_index,
|
|
index,
|
|
domains,
|
|
)
|
|
):
|
|
supported |= candidate_bit
|
|
if supported == 0:
|
|
return false
|
|
if supported != candidates:
|
|
domains[index] = supported
|
|
changed = true
|
|
var required_result := _constrain_required_domain_masks(domains)
|
|
if required_result < 0:
|
|
return false
|
|
if required_result > 0:
|
|
changed = true
|
|
if not _placed_neighbor_requirements_have_domain_mask_support(domains):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _domain_candidate_has_mask_support(
|
|
candidate_index: int,
|
|
index: int,
|
|
domains: Dictionary[int, int],
|
|
) -> bool:
|
|
var candidate := _solver_variants[candidate_index]
|
|
var possible_required_neighbors := 0
|
|
var required_neighbor_mask := _required_neighbor_variant_mask(
|
|
candidate.definition
|
|
)
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_index := _neighbor_indices[index][edge_value]
|
|
if neighbor_index < 0:
|
|
continue
|
|
var placed_neighbor := _placements[neighbor_index]
|
|
if placed_neighbor != null:
|
|
if _definition_matches_required_neighbor_tags(
|
|
candidate.definition,
|
|
placed_neighbor.definition,
|
|
):
|
|
possible_required_neighbors += 1
|
|
continue
|
|
var support_mask := (
|
|
int(domains.get(neighbor_index, 0))
|
|
& _edge_compatibility_masks[candidate_index * 4 + int(edge)]
|
|
)
|
|
if (
|
|
candidate.definition.maximum_placements >= 0
|
|
and _definition_placement_count(candidate.definition) + 2
|
|
> candidate.definition.maximum_placements
|
|
):
|
|
support_mask &= ~int(
|
|
_definition_variant_masks.get(
|
|
candidate.definition.stable_id,
|
|
0,
|
|
)
|
|
)
|
|
if support_mask == 0:
|
|
return false
|
|
if (support_mask & required_neighbor_mask) != 0:
|
|
possible_required_neighbors += 1
|
|
return (
|
|
possible_required_neighbors
|
|
>= candidate.definition.minimum_required_neighbors
|
|
)
|
|
|
|
|
|
func _required_neighbor_variant_mask(
|
|
definition: TerrainChunkDefinition,
|
|
) -> int:
|
|
if _required_neighbor_variant_masks.has(definition.stable_id):
|
|
return _required_neighbor_variant_masks[definition.stable_id]
|
|
var result := 0
|
|
for candidate_index: int in _solver_variants.size():
|
|
if _definition_matches_required_neighbor_tags(
|
|
definition,
|
|
_solver_variants[candidate_index].definition,
|
|
):
|
|
result |= 1 << candidate_index
|
|
_required_neighbor_variant_masks[definition.stable_id] = result
|
|
return result
|
|
|
|
|
|
func _constrain_required_domain_masks(
|
|
domains: Dictionary[int, int],
|
|
) -> int:
|
|
var changed := false
|
|
for stable_id: StringName in _missing_required_ids():
|
|
var definition_mask := int(_definition_variant_masks.get(stable_id, 0))
|
|
var supporting_cell := -1
|
|
var supporting_cell_count := 0
|
|
for index: int in domains:
|
|
if (domains[index] & definition_mask) == 0:
|
|
continue
|
|
supporting_cell = index
|
|
supporting_cell_count += 1
|
|
if supporting_cell_count > 1:
|
|
break
|
|
if supporting_cell_count == 0:
|
|
return -1
|
|
if supporting_cell_count != 1:
|
|
continue
|
|
var forced_mask := domains[supporting_cell] & definition_mask
|
|
if forced_mask == 0:
|
|
return -1
|
|
if forced_mask != domains[supporting_cell]:
|
|
domains[supporting_cell] = forced_mask
|
|
changed = true
|
|
return 1 if changed else 0
|
|
|
|
|
|
func _placed_neighbor_requirements_have_domain_mask_support(
|
|
domains: Dictionary[int, int],
|
|
) -> bool:
|
|
for index: int in _placements.size():
|
|
var placement := _placements[index]
|
|
if placement == null or placement.definition.minimum_required_neighbors <= 0:
|
|
continue
|
|
var placement_variant_index := int(
|
|
_solver_variant_indices.get(placement, -1)
|
|
)
|
|
if placement_variant_index < 0:
|
|
return false
|
|
var required_neighbor_mask := _required_neighbor_variant_mask(
|
|
placement.definition
|
|
)
|
|
var possible_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_index := _neighbor_indices[index][edge_value]
|
|
if neighbor_index < 0:
|
|
continue
|
|
var neighbor := _placements[neighbor_index]
|
|
if neighbor != null:
|
|
if _definition_matches_required_neighbor_tags(
|
|
placement.definition,
|
|
neighbor.definition,
|
|
):
|
|
possible_neighbors += 1
|
|
continue
|
|
var compatible_required_mask := (
|
|
int(domains.get(neighbor_index, 0))
|
|
& required_neighbor_mask
|
|
& _edge_compatibility_masks[
|
|
placement_variant_index * 4 + int(edge)
|
|
]
|
|
)
|
|
if compatible_required_mask != 0:
|
|
possible_neighbors += 1
|
|
if possible_neighbors < placement.definition.minimum_required_neighbors:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _select_next_large_grid_cell() -> Dictionary:
|
|
# Full all-cell arc propagation scales cubically as the map grows. Large
|
|
# worlds instead use the same compatibility checks with a frontier-aware MRV
|
|
# pass. Exact placement validation and backtracking remain unchanged.
|
|
var best_index := -1
|
|
var best_candidates: Array[TerrainChunkVariant] = []
|
|
var best_placed_neighbors := -1
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
var candidate_mask := _compatible_candidate_mask(index)
|
|
if candidate_mask == 0:
|
|
return {"index": index, "candidates": []}
|
|
var candidates := _candidates_from_mask(candidate_mask)
|
|
var placed_neighbors := _placed_neighbor_count(index)
|
|
if (
|
|
best_index < 0
|
|
or candidates.size() < best_candidates.size()
|
|
or (
|
|
candidates.size() == best_candidates.size()
|
|
and placed_neighbors > best_placed_neighbors
|
|
)
|
|
):
|
|
best_index = index
|
|
best_candidates = candidates
|
|
best_placed_neighbors = placed_neighbors
|
|
return {"index": best_index, "candidates": best_candidates}
|
|
|
|
|
|
func _select_next_large_grid_cell_unpacked() -> Dictionary:
|
|
var best_index := -1
|
|
var best_candidates: Array[TerrainChunkVariant] = []
|
|
var best_placed_neighbors := -1
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
var candidates := _compatible_candidates_unpacked(index)
|
|
if candidates.is_empty():
|
|
return {"index": index, "candidates": candidates}
|
|
var placed_neighbors := _placed_neighbor_count(index)
|
|
if (
|
|
best_index < 0
|
|
or candidates.size() < best_candidates.size()
|
|
or (
|
|
candidates.size() == best_candidates.size()
|
|
and placed_neighbors > best_placed_neighbors
|
|
)
|
|
):
|
|
best_index = index
|
|
best_candidates = candidates
|
|
best_placed_neighbors = placed_neighbors
|
|
return {"index": best_index, "candidates": best_candidates}
|
|
|
|
|
|
func _placed_neighbor_count(index: int) -> int:
|
|
var count := 0
|
|
for neighbor_index: int in _neighbor_indices[index]:
|
|
if neighbor_index < 0:
|
|
continue
|
|
if _placements[neighbor_index] != null:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _propagate_domains(domains: Dictionary) -> bool:
|
|
var changed := true
|
|
while changed:
|
|
changed = false
|
|
for index_value: Variant in domains.keys():
|
|
var index := int(index_value)
|
|
var candidates: Array[TerrainChunkVariant] = []
|
|
candidates.assign(domains[index])
|
|
var supported: Array[TerrainChunkVariant] = []
|
|
for candidate: TerrainChunkVariant in candidates:
|
|
if _candidate_has_domain_support(candidate, index, domains):
|
|
supported.append(candidate)
|
|
if supported.is_empty():
|
|
return false
|
|
if supported.size() != candidates.size():
|
|
domains[index] = supported
|
|
changed = true
|
|
var required_result := _constrain_required_domains(domains)
|
|
if required_result < 0:
|
|
return false
|
|
if required_result > 0:
|
|
changed = true
|
|
if not _placed_neighbor_requirements_have_domain_support(domains):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _candidate_has_domain_support(
|
|
candidate: TerrainChunkVariant,
|
|
index: int,
|
|
domains: Dictionary,
|
|
) -> bool:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var possible_required_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor_index := (
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
)
|
|
var placed_neighbor := _placements[neighbor_index]
|
|
if placed_neighbor != null:
|
|
if _definition_matches_required_neighbor_tags(
|
|
candidate.definition,
|
|
placed_neighbor.definition,
|
|
):
|
|
possible_required_neighbors += 1
|
|
continue
|
|
var neighbor_candidates: Array[TerrainChunkVariant] = []
|
|
neighbor_candidates.assign(domains.get(neighbor_index, []))
|
|
var has_support := false
|
|
var has_required_support := false
|
|
for neighbor: TerrainChunkVariant in neighbor_candidates:
|
|
if not _definitions_have_joint_capacity(
|
|
candidate.definition,
|
|
neighbor.definition,
|
|
):
|
|
continue
|
|
if _edges_are_compatible(
|
|
candidate,
|
|
edge,
|
|
neighbor,
|
|
TerrainChunkTopology.opposite_edge(edge),
|
|
):
|
|
has_support = true
|
|
if _definition_matches_required_neighbor_tags(
|
|
candidate.definition,
|
|
neighbor.definition,
|
|
):
|
|
has_required_support = true
|
|
if not has_support:
|
|
return false
|
|
if has_required_support:
|
|
possible_required_neighbors += 1
|
|
return (
|
|
possible_required_neighbors
|
|
>= candidate.definition.minimum_required_neighbors
|
|
)
|
|
|
|
|
|
func _placed_neighbor_requirements_have_domain_support(
|
|
domains: Dictionary,
|
|
) -> bool:
|
|
for index: int in _placements.size():
|
|
var placement := _placements[index]
|
|
if placement == null or placement.definition.minimum_required_neighbors <= 0:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var possible_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor_index := (
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
)
|
|
var neighbor := _placements[neighbor_index]
|
|
if neighbor != null:
|
|
if _definition_matches_required_neighbor_tags(
|
|
placement.definition,
|
|
neighbor.definition,
|
|
):
|
|
possible_neighbors += 1
|
|
continue
|
|
var domain_candidates: Array[TerrainChunkVariant] = []
|
|
domain_candidates.assign(domains.get(neighbor_index, []))
|
|
for candidate: TerrainChunkVariant in domain_candidates:
|
|
if (
|
|
_definition_matches_required_neighbor_tags(
|
|
placement.definition,
|
|
candidate.definition,
|
|
)
|
|
and _edges_are_compatible(
|
|
placement,
|
|
edge,
|
|
candidate,
|
|
TerrainChunkTopology.opposite_edge(edge),
|
|
)
|
|
):
|
|
possible_neighbors += 1
|
|
break
|
|
if possible_neighbors < placement.definition.minimum_required_neighbors:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _definition_matches_required_neighbor_tags(
|
|
definition: TerrainChunkDefinition,
|
|
neighbor: TerrainChunkDefinition,
|
|
) -> bool:
|
|
return (
|
|
definition != null
|
|
and neighbor != null
|
|
and TerrainChunkDefinition.has_any_tag(
|
|
neighbor.tags,
|
|
definition.required_neighbor_tags,
|
|
)
|
|
)
|
|
|
|
|
|
func _definitions_have_joint_capacity(
|
|
first: TerrainChunkDefinition,
|
|
second: TerrainChunkDefinition,
|
|
) -> bool:
|
|
if first != second or first.maximum_placements < 0:
|
|
return true
|
|
return (
|
|
_definition_placement_count(first) + 2
|
|
<= first.maximum_placements
|
|
)
|
|
|
|
|
|
## Returns -1 for a contradiction, 0 for no change, or 1 when at least one
|
|
## required definition was forced into its only remaining cell.
|
|
func _constrain_required_domains(domains: Dictionary) -> int:
|
|
var changed := false
|
|
for stable_id: StringName in _missing_required_ids():
|
|
var supporting_cells: Array[int] = []
|
|
for index_value: Variant in domains:
|
|
var index := int(index_value)
|
|
for candidate: TerrainChunkVariant in domains[index]:
|
|
if candidate.definition.stable_id == stable_id:
|
|
supporting_cells.append(index)
|
|
break
|
|
if supporting_cells.is_empty():
|
|
return -1
|
|
if supporting_cells.size() != 1:
|
|
continue
|
|
var forced_index := supporting_cells[0]
|
|
var forced_candidates: Array[TerrainChunkVariant] = []
|
|
for candidate: TerrainChunkVariant in domains[forced_index]:
|
|
if candidate.definition.stable_id == stable_id:
|
|
forced_candidates.append(candidate)
|
|
if forced_candidates.is_empty():
|
|
return -1
|
|
if forced_candidates.size() != domains[forced_index].size():
|
|
domains[forced_index] = forced_candidates
|
|
changed = true
|
|
return 1 if changed else 0
|
|
|
|
|
|
func _compatible_candidates(index: int) -> Array[TerrainChunkVariant]:
|
|
if not _packed_solver_domains_are_available():
|
|
return _compatible_candidates_unpacked(index)
|
|
return _candidates_from_mask(_compatible_candidate_mask(index))
|
|
|
|
|
|
func _compatible_candidates_unpacked(
|
|
index: int,
|
|
) -> Array[TerrainChunkVariant]:
|
|
var result: Array[TerrainChunkVariant] = []
|
|
var missing_required := _missing_required_ids()
|
|
var must_place_missing_required := (
|
|
missing_required.size() >= _unfilled_cells
|
|
)
|
|
for candidate: TerrainChunkVariant in _solver_variants:
|
|
if (
|
|
must_place_missing_required
|
|
and not missing_required.has(candidate.definition.stable_id)
|
|
):
|
|
continue
|
|
if _candidate_can_occupy_cell(candidate, index):
|
|
result.append(candidate)
|
|
return result
|
|
|
|
|
|
func _packed_solver_domains_are_available() -> bool:
|
|
return _solver_variants.size() <= MAX_PACKED_SOLVER_VARIANTS
|
|
|
|
|
|
func _compatible_candidate_mask(index: int) -> int:
|
|
if index < 0 or index >= _static_cell_candidate_masks.size():
|
|
return 0
|
|
var result := int(_static_cell_candidate_masks[index])
|
|
if _selection_missing_required_count >= _unfilled_cells:
|
|
result &= _selection_missing_required_mask
|
|
if result == 0:
|
|
return 0
|
|
result = _mask_matching_placed_neighbors(result, index)
|
|
if result == 0:
|
|
return 0
|
|
|
|
for definition: TerrainChunkDefinition in _solver_definitions:
|
|
var definition_mask := int(
|
|
_definition_variant_masks.get(definition.stable_id, 0)
|
|
)
|
|
if (result & definition_mask) == 0:
|
|
continue
|
|
var definition_variants: Array = _solver_variants_by_definition.get(
|
|
definition.stable_id,
|
|
[],
|
|
)
|
|
if (
|
|
definition_variants.is_empty()
|
|
or not _candidate_dynamic_cell_rules_are_satisfied(
|
|
definition_variants[0] as TerrainChunkVariant,
|
|
index,
|
|
)
|
|
):
|
|
result &= ~definition_mask
|
|
return result
|
|
|
|
|
|
func _mask_matching_placed_neighbors(candidate_mask: int, index: int) -> int:
|
|
var result := candidate_mask
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_index := _neighbor_indices[index][edge_value]
|
|
if neighbor_index < 0:
|
|
continue
|
|
var neighbor := _placements[neighbor_index]
|
|
if neighbor == null:
|
|
continue
|
|
var neighbor_variant_index := int(
|
|
_solver_variant_indices.get(neighbor, -1)
|
|
)
|
|
if neighbor_variant_index < 0:
|
|
return 0
|
|
var neighbor_edge := TerrainChunkTopology.opposite_edge(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
result &= _edge_compatibility_masks[
|
|
neighbor_variant_index * 4 + int(neighbor_edge)
|
|
]
|
|
if result == 0:
|
|
return 0
|
|
return result
|
|
|
|
|
|
func _candidate_dynamic_cell_rules_are_satisfied(
|
|
candidate: TerrainChunkVariant,
|
|
index: int,
|
|
) -> bool:
|
|
if not _definition_has_capacity(candidate.definition):
|
|
return false
|
|
var center_index := (grid_size.y / 2) * grid_size.x + grid_size.x / 2
|
|
if (
|
|
index != center_index
|
|
and _placements[center_index] == null
|
|
and candidate.definition.stable_id == force_center_chunk_id
|
|
and candidate.definition.maximum_placements >= 0
|
|
and _definition_placement_count(candidate.definition)
|
|
>= candidate.definition.maximum_placements - 1
|
|
):
|
|
return false
|
|
return (
|
|
_candidate_neighbor_requirement_can_still_be_met_at_index(
|
|
candidate.definition,
|
|
index,
|
|
)
|
|
and _candidate_preserves_placed_neighbor_requirements_at_index(
|
|
candidate.definition,
|
|
index,
|
|
)
|
|
)
|
|
|
|
|
|
func _candidate_neighbor_requirement_can_still_be_met_at_index(
|
|
definition: TerrainChunkDefinition,
|
|
index: int,
|
|
) -> bool:
|
|
if definition.minimum_required_neighbors <= 0:
|
|
return true
|
|
var possible_neighbors := 0
|
|
for neighbor_index: int in _neighbor_indices[index]:
|
|
if neighbor_index < 0:
|
|
continue
|
|
var neighbor := _placements[neighbor_index]
|
|
if (
|
|
neighbor == null
|
|
or _definition_matches_required_neighbor_tags(
|
|
definition,
|
|
neighbor.definition,
|
|
)
|
|
):
|
|
possible_neighbors += 1
|
|
return possible_neighbors >= definition.minimum_required_neighbors
|
|
|
|
|
|
func _candidate_preserves_placed_neighbor_requirements_at_index(
|
|
candidate_definition: TerrainChunkDefinition,
|
|
candidate_index: int,
|
|
) -> bool:
|
|
for neighbor_index: int in _neighbor_indices[candidate_index]:
|
|
if neighbor_index < 0:
|
|
continue
|
|
var neighbor := _placements[neighbor_index]
|
|
if neighbor == null or neighbor.definition.minimum_required_neighbors <= 0:
|
|
continue
|
|
var possible_neighbors := 0
|
|
for requirement_neighbor_index: int in _neighbor_indices[neighbor_index]:
|
|
if requirement_neighbor_index < 0:
|
|
continue
|
|
if requirement_neighbor_index == candidate_index:
|
|
if _definition_matches_required_neighbor_tags(
|
|
neighbor.definition,
|
|
candidate_definition,
|
|
):
|
|
possible_neighbors += 1
|
|
continue
|
|
var requirement_neighbor := _placements[requirement_neighbor_index]
|
|
if (
|
|
requirement_neighbor == null
|
|
or _definition_matches_required_neighbor_tags(
|
|
neighbor.definition,
|
|
requirement_neighbor.definition,
|
|
)
|
|
):
|
|
possible_neighbors += 1
|
|
if possible_neighbors < neighbor.definition.minimum_required_neighbors:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _candidate_can_occupy_cell(
|
|
candidate: TerrainChunkVariant,
|
|
index: int,
|
|
) -> bool:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
if (
|
|
_cell_requires_reserved_grass(index)
|
|
and candidate.definition.stable_id != elevated_cliff_base_chunk_id
|
|
and not (
|
|
coordinate == Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
and force_center_chunk_id != &""
|
|
and candidate.definition.stable_id == force_center_chunk_id
|
|
and "grass" in candidate.definition.tags
|
|
and "flat" in candidate.definition.tags
|
|
)
|
|
):
|
|
return false
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
if (
|
|
coordinate == center
|
|
and force_center_chunk_id != &""
|
|
and candidate.definition.stable_id != force_center_chunk_id
|
|
):
|
|
return false
|
|
return (
|
|
_variant_respects_ocean_boundary(candidate, coordinate)
|
|
and _candidate_matches_placed_neighbors(candidate, coordinate)
|
|
and _candidate_dynamic_cell_rules_are_satisfied(candidate, index)
|
|
)
|
|
|
|
|
|
func _prepare_elevated_feature_region() -> bool:
|
|
_elevated_feature_center = Vector2i(-1, -1)
|
|
_secondary_elevated_feature_center = Vector2i(-1, -1)
|
|
_elevated_feature_reserved_grass_indices.clear()
|
|
_stacked_elevated_placements.clear()
|
|
if not elevated_cliff_feature_enabled:
|
|
if not _prepare_lake_feature_region():
|
|
return false
|
|
if not _prepare_river_feature_region():
|
|
return false
|
|
_build_grid_solver_caches()
|
|
return true
|
|
for stable_id: StringName in [
|
|
elevated_cliff_base_chunk_id,
|
|
elevated_cliff_top_chunk_id,
|
|
elevated_cliff_corner_chunk_id,
|
|
elevated_cliff_edge_chunk_id,
|
|
elevated_cliff_ramp_chunk_id,
|
|
elevated_cliff_third_tier_edge_chunk_id,
|
|
elevated_cliff_third_tier_corner_chunk_id,
|
|
elevated_cliff_third_tier_top_chunk_id,
|
|
elevated_cliff_sea_edge_chunk_id,
|
|
elevated_cliff_sea_corner_chunk_id,
|
|
elevated_cliff_sea_transition_right_chunk_id,
|
|
elevated_cliff_sea_transition_left_chunk_id,
|
|
elevated_cliff_coast_base_chunk_id,
|
|
elevated_cliff_beach_base_chunk_id,
|
|
elevated_cliff_beach_transition_right_chunk_id,
|
|
elevated_cliff_beach_transition_left_chunk_id,
|
|
]:
|
|
if catalog.definition_for_id(stable_id) == null:
|
|
push_error("Elevated cliff feature references missing chunk %s." % stable_id)
|
|
return false
|
|
if grid_size.x < 11 or grid_size.y < 11:
|
|
push_error(
|
|
"Elevated cliff feature requires at least an 11x11 terrain grid."
|
|
)
|
|
return false
|
|
var forced_center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
var candidates: Array[Vector2i] = []
|
|
for row: int in range(
|
|
ELEVATED_FEATURE_BOUNDARY_MARGIN,
|
|
grid_size.y - ELEVATED_FEATURE_BOUNDARY_MARGIN,
|
|
):
|
|
for column: int in range(
|
|
ELEVATED_FEATURE_BOUNDARY_MARGIN,
|
|
grid_size.x - ELEVATED_FEATURE_BOUNDARY_MARGIN,
|
|
):
|
|
var candidate := Vector2i(column, row)
|
|
if (
|
|
absi(candidate.x - forced_center.x)
|
|
<= ELEVATED_FEATURE_RADIUS
|
|
and absi(candidate.y - forced_center.y)
|
|
<= ELEVATED_FEATURE_RADIUS
|
|
):
|
|
continue
|
|
candidates.append(candidate)
|
|
if candidates.is_empty():
|
|
push_error("Terrain grid has no inland 5x5 cliff feature location.")
|
|
return false
|
|
var feature_random := RandomNumberGenerator.new()
|
|
feature_random.seed = generation_seed ^ 0x2E1E7A7ED
|
|
var candidate_start := feature_random.randi_range(0, candidates.size() - 1)
|
|
var compatible_features_found := false
|
|
for candidate_offset: int in candidates.size():
|
|
_elevated_feature_center = candidates[
|
|
(candidate_start + candidate_offset) % candidates.size()
|
|
]
|
|
_elevated_feature_reserved_grass_indices.clear()
|
|
# Solve the authored 5x5 plateau into a one-cell ring of ordinary
|
|
# level-one flat grass. The lake's own clearance may share that grass,
|
|
# but its actual basin may never replace the plateau or landing ring.
|
|
_reserve_elevated_feature_base(
|
|
_elevated_feature_center,
|
|
ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
|
)
|
|
if not (
|
|
_prepare_lake_feature_region(false)
|
|
and _prepare_river_feature_region(false)
|
|
):
|
|
continue
|
|
var secondary_candidates := _compatible_coastal_elevated_feature_centers()
|
|
if (
|
|
elevated_cliff_coastal_feature_required
|
|
and secondary_candidates.is_empty()
|
|
):
|
|
continue
|
|
if not secondary_candidates.is_empty():
|
|
_secondary_elevated_feature_center = secondary_candidates[
|
|
feature_random.randi_range(0, secondary_candidates.size() - 1)
|
|
]
|
|
compatible_features_found = true
|
|
break
|
|
if not compatible_features_found:
|
|
push_error(
|
|
(
|
|
"Terrain grid has no compatible inland cliff, lake, river, "
|
|
+ "and required coastal cliff locations."
|
|
)
|
|
)
|
|
return false
|
|
if _secondary_elevated_feature_center.x >= 0:
|
|
_reserve_elevated_feature_base(
|
|
_secondary_elevated_feature_center,
|
|
SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
|
)
|
|
_build_grid_solver_caches()
|
|
return true
|
|
|
|
|
|
func _compatible_coastal_elevated_feature_centers() -> Array[Vector2i]:
|
|
var secondary_candidates := _coastal_elevated_feature_centers()
|
|
var combined_clearance := (
|
|
ELEVATED_FEATURE_CLEARANCE_RADIUS
|
|
+ SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS
|
|
)
|
|
secondary_candidates = secondary_candidates.filter(
|
|
func(candidate: Vector2i) -> bool:
|
|
var separation := candidate - _elevated_feature_center
|
|
return (
|
|
(
|
|
absi(separation.x) > combined_clearance
|
|
or absi(separation.y) > combined_clearance
|
|
)
|
|
and not _elevated_feature_overlaps_lake(
|
|
candidate,
|
|
SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
|
)
|
|
and not _elevated_feature_overlaps_river(
|
|
candidate,
|
|
SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
|
)
|
|
)
|
|
)
|
|
return secondary_candidates
|
|
|
|
|
|
func _coastal_elevated_feature_centers() -> Array[Vector2i]:
|
|
return [
|
|
Vector2i(1, 1),
|
|
Vector2i(grid_size.x - 2, 1),
|
|
Vector2i(1, grid_size.y - 2),
|
|
Vector2i(grid_size.x - 2, grid_size.y - 2),
|
|
]
|
|
|
|
|
|
func _elevated_feature_overlaps_lake(
|
|
center: Vector2i,
|
|
clearance_radius: int,
|
|
) -> bool:
|
|
if _lake_feature_origin.x < 0:
|
|
return false
|
|
var lake_end := _lake_feature_origin + _lake_feature_footprint_size
|
|
for row: int in range(
|
|
center.y - clearance_radius,
|
|
center.y + clearance_radius + 1,
|
|
):
|
|
for column: int in range(
|
|
center.x - clearance_radius,
|
|
center.x + clearance_radius + 1,
|
|
):
|
|
if (
|
|
column >= _lake_feature_origin.x
|
|
and column < lake_end.x
|
|
and row >= _lake_feature_origin.y
|
|
and row < lake_end.y
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _elevated_feature_overlaps_river(
|
|
center: Vector2i,
|
|
clearance_radius: int,
|
|
) -> bool:
|
|
if _river_feature_coordinates.is_empty():
|
|
return false
|
|
for row: int in range(
|
|
center.y - clearance_radius,
|
|
center.y + clearance_radius + 1,
|
|
):
|
|
for column: int in range(
|
|
center.x - clearance_radius,
|
|
center.x + clearance_radius + 1,
|
|
):
|
|
var coordinate := Vector2i(column, row)
|
|
if _river_feature_coordinates.has(coordinate):
|
|
return true
|
|
if not _coordinate_is_inside_grid(coordinate):
|
|
continue
|
|
var index := coordinate.y * grid_size.x + coordinate.x
|
|
if _river_feature_reserved_grass_indices.has(index):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _reserve_elevated_feature_base(
|
|
center: Vector2i,
|
|
clearance_radius: int,
|
|
) -> void:
|
|
for row_offset: int in range(-clearance_radius, clearance_radius + 1):
|
|
for column_offset: int in range(
|
|
-clearance_radius,
|
|
clearance_radius + 1,
|
|
):
|
|
var coordinate := center + Vector2i(column_offset, row_offset)
|
|
if (
|
|
not _coordinate_is_inside_grid(coordinate)
|
|
or _coordinate_is_on_boundary(coordinate)
|
|
):
|
|
continue
|
|
_elevated_feature_reserved_grass_indices[
|
|
coordinate.y * grid_size.x + coordinate.x
|
|
] = true
|
|
|
|
|
|
func _prepare_lake_feature_region(report_no_room_error := true) -> bool:
|
|
_lake_feature_origin = Vector2i(-1, -1)
|
|
_lake_feature_footprint_size = Vector2i.ZERO
|
|
_lake_feature_uses_fill = false
|
|
_lake_feature_uses_diagonal_perimeter = false
|
|
_lake_feature_reserved_grass_indices.clear()
|
|
if not lake_feature_enabled:
|
|
return true
|
|
var edge_definition := catalog.definition_for_id(lake_edge_chunk_id)
|
|
var edge_variant_definition := catalog.definition_for_id(
|
|
lake_edge_variant_chunk_id
|
|
)
|
|
var corner_definition := catalog.definition_for_id(lake_corner_chunk_id)
|
|
var fill_definition := catalog.definition_for_id(lake_fill_chunk_id)
|
|
var diagonal_definition := catalog.definition_for_id(lake_diagonal_chunk_id)
|
|
var narrow_definition := catalog.definition_for_id(lake_narrow_edge_chunk_id)
|
|
var regular_to_narrow_definition := catalog.definition_for_id(
|
|
lake_regular_to_narrow_chunk_id
|
|
)
|
|
var narrow_to_regular_definition := catalog.definition_for_id(
|
|
lake_narrow_to_regular_chunk_id
|
|
)
|
|
if (
|
|
edge_definition == null
|
|
or edge_variant_definition == null
|
|
or corner_definition == null
|
|
):
|
|
push_error(
|
|
"Lake feature references missing edge, edge variant, or corner chunks."
|
|
)
|
|
return false
|
|
if (
|
|
edge_definition.participates_in_base_solver
|
|
or edge_variant_definition.participates_in_base_solver
|
|
or corner_definition.participates_in_base_solver
|
|
or (
|
|
fill_definition != null
|
|
and fill_definition.participates_in_base_solver
|
|
)
|
|
):
|
|
push_error("Lake feature chunks must be excluded from the base solver.")
|
|
return false
|
|
if fill_definition != null:
|
|
var diagonal_perimeter_definitions: Array[TerrainChunkDefinition] = [
|
|
diagonal_definition,
|
|
narrow_definition,
|
|
regular_to_narrow_definition,
|
|
narrow_to_regular_definition,
|
|
]
|
|
for definition: TerrainChunkDefinition in diagonal_perimeter_definitions:
|
|
if definition == null:
|
|
push_error(
|
|
"Lake feature references a missing diagonal perimeter chunk."
|
|
)
|
|
return false
|
|
if definition.participates_in_base_solver:
|
|
push_error(
|
|
"Lake diagonal perimeter chunks must be excluded from the base solver."
|
|
)
|
|
return false
|
|
_lake_feature_uses_diagonal_perimeter = true
|
|
var lake_random := RandomNumberGenerator.new()
|
|
lake_random.seed = generation_seed ^ 0x1A4E5EED
|
|
var candidates: Array[Dictionary] = []
|
|
if fill_definition != null:
|
|
var minimum_size := maxi(5, lake_minimum_size)
|
|
var maximum_size := maxi(minimum_size, lake_maximum_size)
|
|
var desired_size := lake_random.randi_range(
|
|
minimum_size,
|
|
maximum_size,
|
|
)
|
|
for size: int in range(desired_size, minimum_size - 1, -1):
|
|
candidates = _lake_feature_candidates(Vector2i(size, size))
|
|
if not candidates.is_empty():
|
|
break
|
|
_lake_feature_uses_fill = true
|
|
else:
|
|
# The two-bank fallback keeps worlds valid while the authored interior
|
|
# lake-bed piece is unavailable. Cataloging lake_fill_chunk_id upgrades
|
|
# this automatically to the large square footprint above.
|
|
var minimum_length := maxi(3, lake_minimum_length)
|
|
var maximum_length := maxi(minimum_length, lake_maximum_length)
|
|
var desired_length := lake_random.randi_range(
|
|
minimum_length,
|
|
maximum_length,
|
|
)
|
|
for length: int in range(
|
|
desired_length,
|
|
minimum_length - 1,
|
|
-1,
|
|
):
|
|
candidates = _lake_feature_candidates(
|
|
Vector2i(2, length),
|
|
)
|
|
candidates.append_array(
|
|
_lake_feature_candidates(Vector2i(length, 2)),
|
|
)
|
|
if not candidates.is_empty():
|
|
break
|
|
if candidates.is_empty():
|
|
if report_no_room_error:
|
|
push_error(
|
|
"Terrain grid has no room for the configured lake feature."
|
|
)
|
|
return false
|
|
var selected: Dictionary = candidates[
|
|
lake_random.randi_range(0, candidates.size() - 1)
|
|
]
|
|
_lake_feature_origin = selected.get("origin", Vector2i(-1, -1))
|
|
_lake_feature_footprint_size = selected.get(
|
|
"size",
|
|
Vector2i.ZERO,
|
|
)
|
|
for row: int in range(
|
|
_lake_feature_origin.y - LAKE_FEATURE_CLEARANCE,
|
|
_lake_feature_origin.y
|
|
+ _lake_feature_footprint_size.y
|
|
+ LAKE_FEATURE_CLEARANCE,
|
|
):
|
|
for column: int in range(
|
|
_lake_feature_origin.x - LAKE_FEATURE_CLEARANCE,
|
|
_lake_feature_origin.x
|
|
+ _lake_feature_footprint_size.x
|
|
+ LAKE_FEATURE_CLEARANCE,
|
|
):
|
|
_lake_feature_reserved_grass_indices[
|
|
row * grid_size.x + column
|
|
] = true
|
|
return true
|
|
|
|
|
|
func _lake_feature_candidates(
|
|
footprint_size: Vector2i,
|
|
) -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
if footprint_size.x < 2 or footprint_size.y < 2:
|
|
return result
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
for row: int in range(
|
|
LAKE_FEATURE_CLEARANCE + 1,
|
|
grid_size.y - footprint_size.y - LAKE_FEATURE_CLEARANCE,
|
|
):
|
|
for column: int in range(
|
|
LAKE_FEATURE_CLEARANCE + 1,
|
|
grid_size.x - footprint_size.x - LAKE_FEATURE_CLEARANCE,
|
|
):
|
|
var origin := Vector2i(column, row)
|
|
var blocked := false
|
|
var spawn_neighbor_overlap_count := 0
|
|
for check_row: int in range(
|
|
origin.y,
|
|
origin.y + footprint_size.y,
|
|
):
|
|
for check_column: int in range(
|
|
origin.x,
|
|
origin.x + footprint_size.x,
|
|
):
|
|
var coordinate := Vector2i(check_column, check_row)
|
|
var index := check_row * grid_size.x + check_column
|
|
var center_distance := (
|
|
absi(coordinate.x - center.x)
|
|
+ absi(coordinate.y - center.y)
|
|
)
|
|
if center_distance == 1:
|
|
spawn_neighbor_overlap_count += 1
|
|
if (
|
|
center_distance == 0
|
|
or spawn_neighbor_overlap_count > 1
|
|
or _elevated_feature_reserved_grass_indices.has(index)
|
|
):
|
|
blocked = true
|
|
break
|
|
if blocked:
|
|
break
|
|
if not blocked:
|
|
result.append({
|
|
"origin": origin,
|
|
"size": footprint_size,
|
|
})
|
|
return result
|
|
|
|
|
|
func _prepare_river_feature_region(report_no_room_error := true) -> bool:
|
|
_river_feature_origin = Vector2i(-1, -1)
|
|
_river_feature_footprint_size = Vector2i.ZERO
|
|
_river_feature_flow_direction = Vector2i.ZERO
|
|
_river_feature_outlet_direction = Vector2i.ZERO
|
|
_river_feature_reserved_grass_indices.clear()
|
|
_river_feature_candidate_order.clear()
|
|
_river_feature_candidate_index = -1
|
|
_river_feature_placements.clear()
|
|
_river_feature_coordinates.clear()
|
|
_river_feature_source_coordinates.clear()
|
|
if not river_feature_enabled:
|
|
return true
|
|
for stable_id: StringName in [
|
|
river_edge_chunk_id,
|
|
river_edge_variant_chunk_id,
|
|
river_stone_bridge_chunk_id,
|
|
river_source_right_chunk_id,
|
|
river_source_left_chunk_id,
|
|
river_outlet_right_chunk_id,
|
|
river_outlet_left_chunk_id,
|
|
river_bend_a_chunk_id,
|
|
river_bend_b_chunk_id,
|
|
river_bend_c_chunk_id,
|
|
river_bend_d_chunk_id,
|
|
]:
|
|
var definition := catalog.definition_for_id(stable_id)
|
|
if definition == null:
|
|
push_error("River feature references missing chunk %s." % stable_id)
|
|
return false
|
|
if definition.participates_in_base_solver:
|
|
push_error("River feature chunks must be excluded from the base solver.")
|
|
return false
|
|
if not elevated_cliff_feature_enabled or _elevated_feature_center.x < 0:
|
|
if report_no_room_error:
|
|
push_error(
|
|
"Terrain river feature requires an elevated cliff source."
|
|
)
|
|
return false
|
|
var river_random := RandomNumberGenerator.new()
|
|
river_random.seed = generation_seed ^ 0x71A3E22D
|
|
var minimum_length := maxi(5, river_minimum_length)
|
|
var maximum_length := maxi(minimum_length, river_maximum_length)
|
|
var desired_length := river_random.randi_range(
|
|
minimum_length,
|
|
maximum_length,
|
|
)
|
|
var candidates: Array[Dictionary] = []
|
|
for length: int in range(desired_length, minimum_length - 1, -1):
|
|
candidates = _river_feature_candidates(length)
|
|
if not candidates.is_empty():
|
|
break
|
|
if candidates.is_empty():
|
|
if report_no_room_error:
|
|
push_error("Terrain grid has no room for the configured river feature.")
|
|
return false
|
|
var candidate_start := river_random.randi_range(0, candidates.size() - 1)
|
|
for candidate_offset: int in candidates.size():
|
|
_river_feature_candidate_order.append(
|
|
candidates[(candidate_start + candidate_offset) % candidates.size()]
|
|
)
|
|
if not _select_river_feature_region(0):
|
|
push_error("Terrain river feature has no compatible reserved region.")
|
|
return false
|
|
return true
|
|
|
|
|
|
func _select_next_river_feature_region() -> bool:
|
|
if not river_feature_enabled:
|
|
return false
|
|
for candidate_index: int in range(
|
|
_river_feature_candidate_index + 1,
|
|
_river_feature_candidate_order.size(),
|
|
):
|
|
if _select_river_feature_region(candidate_index):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _select_river_feature_region(candidate_index: int) -> bool:
|
|
if (
|
|
candidate_index < 0
|
|
or candidate_index >= _river_feature_candidate_order.size()
|
|
):
|
|
return false
|
|
var selected: Dictionary = _river_feature_candidate_order[candidate_index]
|
|
if not _river_feature_candidate_is_valid(selected):
|
|
return false
|
|
var origin: Vector2i = selected.get("origin", Vector2i(-1, -1))
|
|
var footprint_size: Vector2i = selected.get("size", Vector2i.ZERO)
|
|
var flow_direction: Vector2i = selected.get(
|
|
"flow_direction",
|
|
Vector2i.ZERO,
|
|
)
|
|
var outlet_direction: Vector2i = selected.get(
|
|
"outlet_direction",
|
|
Vector2i.ZERO,
|
|
)
|
|
var source_coordinates: Array[Vector2i] = []
|
|
source_coordinates.assign(selected.get("source_coordinates", []))
|
|
var placements: Array[Dictionary] = []
|
|
placements.assign(selected.get("placements", []))
|
|
var coordinates: Array[Vector2i] = []
|
|
coordinates.assign(selected.get("coordinates", []))
|
|
_river_feature_candidate_index = candidate_index
|
|
_river_feature_origin = origin
|
|
_river_feature_footprint_size = footprint_size
|
|
_river_feature_flow_direction = flow_direction
|
|
_river_feature_outlet_direction = outlet_direction
|
|
_river_feature_source_coordinates.assign(source_coordinates)
|
|
_river_feature_placements.assign(placements)
|
|
_river_feature_coordinates.clear()
|
|
for coordinate: Vector2i in coordinates:
|
|
_river_feature_coordinates[coordinate] = true
|
|
_river_feature_reserved_grass_indices.clear()
|
|
for river_coordinate: Vector2i in coordinates:
|
|
for row_offset: int in range(
|
|
-RIVER_FEATURE_CLEARANCE,
|
|
RIVER_FEATURE_CLEARANCE + 1,
|
|
):
|
|
for column_offset: int in range(
|
|
-RIVER_FEATURE_CLEARANCE,
|
|
RIVER_FEATURE_CLEARANCE + 1,
|
|
):
|
|
var coordinate := (
|
|
river_coordinate
|
|
+ Vector2i(column_offset, row_offset)
|
|
)
|
|
if (
|
|
not _coordinate_is_inside_grid(coordinate)
|
|
or _coordinate_is_on_boundary(coordinate)
|
|
):
|
|
continue
|
|
_river_feature_reserved_grass_indices[
|
|
coordinate.y * grid_size.x + coordinate.x
|
|
] = true
|
|
return true
|
|
|
|
|
|
func _river_feature_candidates(
|
|
length: int,
|
|
) -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
if length < 5 or _elevated_feature_center.x < 0:
|
|
return result
|
|
var directions: Array[Vector2i] = [
|
|
Vector2i.RIGHT,
|
|
Vector2i.DOWN,
|
|
Vector2i.LEFT,
|
|
Vector2i.UP,
|
|
]
|
|
for flow_direction: Vector2i in directions:
|
|
for cross_offset: int in [-1, 0]:
|
|
var source_coordinates := _river_source_pair_for_direction(
|
|
flow_direction,
|
|
cross_offset,
|
|
)
|
|
for bend_distance: int in range(2, maxi(3, length - 1)):
|
|
var target_entry: Array[Vector2i] = []
|
|
for coordinate: Vector2i in source_coordinates:
|
|
target_entry.append(
|
|
coordinate + flow_direction * bend_distance
|
|
)
|
|
for bend_turns: int in 4:
|
|
var sides := _river_bend_sides(bend_turns)
|
|
for entry_index: int in 2:
|
|
var entry_side: Dictionary = sides[entry_index]
|
|
if (
|
|
entry_side.get("normal", Vector2i.ZERO)
|
|
!= -flow_direction
|
|
):
|
|
continue
|
|
var exit_side: Dictionary = sides[1 - entry_index]
|
|
var entry_offsets: Array[Vector2i] = []
|
|
entry_offsets.assign(entry_side.get("offsets", []))
|
|
var bend_origin := _river_pair_translation(
|
|
target_entry,
|
|
entry_offsets,
|
|
)
|
|
if bend_origin.x == -1000000:
|
|
continue
|
|
var exit_offsets: Array[Vector2i] = []
|
|
exit_offsets.assign(exit_side.get("offsets", []))
|
|
var exit_coordinates: Array[Vector2i] = []
|
|
for offset: Vector2i in exit_offsets:
|
|
exit_coordinates.append(bend_origin + offset)
|
|
var outlet_direction: Vector2i = exit_side.get(
|
|
"normal",
|
|
Vector2i.ZERO,
|
|
)
|
|
var outlet_distance := _river_distance_to_boundary(
|
|
exit_coordinates,
|
|
outlet_direction,
|
|
)
|
|
if (
|
|
outlet_distance < 2
|
|
or bend_distance + outlet_distance + 2 < length
|
|
):
|
|
continue
|
|
var candidate := _build_river_feature_candidate(
|
|
source_coordinates,
|
|
flow_direction,
|
|
bend_origin,
|
|
bend_turns,
|
|
bend_distance,
|
|
exit_coordinates,
|
|
outlet_direction,
|
|
outlet_distance,
|
|
)
|
|
if _river_feature_candidate_is_valid(candidate):
|
|
result.append(candidate)
|
|
return result
|
|
|
|
|
|
func _river_feature_candidate_is_valid(
|
|
candidate: Dictionary,
|
|
) -> bool:
|
|
if candidate.is_empty():
|
|
return false
|
|
var flow_direction: Vector2i = candidate.get(
|
|
"flow_direction",
|
|
Vector2i.ZERO,
|
|
)
|
|
var outlet_direction: Vector2i = candidate.get(
|
|
"outlet_direction",
|
|
Vector2i.ZERO,
|
|
)
|
|
if (
|
|
flow_direction == Vector2i.ZERO
|
|
or outlet_direction == Vector2i.ZERO
|
|
or (
|
|
flow_direction.x * outlet_direction.x
|
|
+ flow_direction.y * outlet_direction.y
|
|
) != 0
|
|
):
|
|
return false
|
|
var coordinates: Array[Vector2i] = []
|
|
coordinates.assign(candidate.get("coordinates", []))
|
|
var source_coordinates: Array[Vector2i] = []
|
|
source_coordinates.assign(candidate.get("source_coordinates", []))
|
|
var outlet_coordinates: Array[Vector2i] = []
|
|
outlet_coordinates.assign(candidate.get("outlet_coordinates", []))
|
|
if (
|
|
coordinates.is_empty()
|
|
or source_coordinates.size() != 2
|
|
or outlet_coordinates.size() != 2
|
|
):
|
|
return false
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
var source_lookup: Dictionary[Vector2i, bool] = {}
|
|
var source_exit_lookup: Dictionary[Vector2i, bool] = {}
|
|
var outlet_lookup: Dictionary[Vector2i, bool] = {}
|
|
var outlet_approach_lookup: Dictionary[Vector2i, bool] = {}
|
|
for coordinate: Vector2i in source_coordinates:
|
|
source_lookup[coordinate] = true
|
|
source_exit_lookup[coordinate + flow_direction] = true
|
|
for coordinate: Vector2i in outlet_coordinates:
|
|
outlet_lookup[coordinate] = true
|
|
outlet_approach_lookup[coordinate - outlet_direction] = true
|
|
var coordinate_lookup: Dictionary[Vector2i, bool] = {}
|
|
for coordinate: Vector2i in coordinates:
|
|
if not _coordinate_is_inside_grid(coordinate):
|
|
return false
|
|
if coordinate_lookup.has(coordinate):
|
|
return false
|
|
var boundary_distance := _distance_from_map_boundary(coordinate)
|
|
if (
|
|
boundary_distance == 0
|
|
and not outlet_lookup.has(coordinate)
|
|
):
|
|
return false
|
|
if (
|
|
boundary_distance == 1
|
|
and not outlet_approach_lookup.has(coordinate)
|
|
):
|
|
return false
|
|
coordinate_lookup[coordinate] = true
|
|
var index := coordinate.y * grid_size.x + coordinate.x
|
|
var center_distance := (
|
|
absi(coordinate.x - center.x)
|
|
+ absi(coordinate.y - center.y)
|
|
)
|
|
if (
|
|
center_distance <= 1
|
|
or _lake_feature_reserved_grass_indices.has(index)
|
|
or (
|
|
_elevated_feature_reserved_grass_indices.has(index)
|
|
and not source_lookup.has(coordinate)
|
|
and not source_exit_lookup.has(coordinate)
|
|
)
|
|
):
|
|
return false
|
|
for coordinate: Vector2i in source_lookup:
|
|
var index := coordinate.y * grid_size.x + coordinate.x
|
|
if not _elevated_feature_reserved_grass_indices.has(index):
|
|
return false
|
|
for coordinate: Vector2i in outlet_coordinates:
|
|
if (
|
|
not _coordinate_is_on_boundary(coordinate)
|
|
or _coordinate_is_inside_grid(coordinate + outlet_direction)
|
|
):
|
|
return false
|
|
var ordered_outlet := _river_sorted_pair(
|
|
outlet_coordinates,
|
|
outlet_direction,
|
|
)
|
|
var tangent := (
|
|
Vector2i.DOWN
|
|
if outlet_direction.x != 0
|
|
else Vector2i.RIGHT
|
|
)
|
|
for beach_coordinate: Vector2i in [
|
|
ordered_outlet[0] - tangent,
|
|
ordered_outlet[1] + tangent,
|
|
]:
|
|
if (
|
|
not _coordinate_is_inside_grid(beach_coordinate)
|
|
or not _coordinate_is_on_boundary(beach_coordinate)
|
|
or coordinate_lookup.has(beach_coordinate)
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _river_source_pair_for_direction(
|
|
flow_direction: Vector2i,
|
|
cross_offset: int,
|
|
) -> Array[Vector2i]:
|
|
match flow_direction:
|
|
Vector2i.RIGHT:
|
|
return [
|
|
Vector2i(
|
|
_elevated_feature_center.x + ELEVATED_FEATURE_RADIUS,
|
|
_elevated_feature_center.y + cross_offset,
|
|
),
|
|
Vector2i(
|
|
_elevated_feature_center.x + ELEVATED_FEATURE_RADIUS,
|
|
_elevated_feature_center.y + cross_offset + 1,
|
|
),
|
|
]
|
|
Vector2i.LEFT:
|
|
return [
|
|
Vector2i(
|
|
_elevated_feature_center.x - ELEVATED_FEATURE_RADIUS,
|
|
_elevated_feature_center.y + cross_offset,
|
|
),
|
|
Vector2i(
|
|
_elevated_feature_center.x - ELEVATED_FEATURE_RADIUS,
|
|
_elevated_feature_center.y + cross_offset + 1,
|
|
),
|
|
]
|
|
Vector2i.DOWN:
|
|
return [
|
|
Vector2i(
|
|
_elevated_feature_center.x + cross_offset,
|
|
_elevated_feature_center.y + ELEVATED_FEATURE_RADIUS,
|
|
),
|
|
Vector2i(
|
|
_elevated_feature_center.x + cross_offset + 1,
|
|
_elevated_feature_center.y + ELEVATED_FEATURE_RADIUS,
|
|
),
|
|
]
|
|
Vector2i.UP:
|
|
return [
|
|
Vector2i(
|
|
_elevated_feature_center.x + cross_offset,
|
|
_elevated_feature_center.y - ELEVATED_FEATURE_RADIUS,
|
|
),
|
|
Vector2i(
|
|
_elevated_feature_center.x + cross_offset + 1,
|
|
_elevated_feature_center.y - ELEVATED_FEATURE_RADIUS,
|
|
),
|
|
]
|
|
return []
|
|
|
|
|
|
func _river_bend_sides(quarter_turns: int) -> Array[Dictionary]:
|
|
var west_offsets: Array[Vector2i] = [Vector2i(0, 0), Vector2i(0, 1)]
|
|
var south_offsets: Array[Vector2i] = [Vector2i(0, 1), Vector2i(1, 1)]
|
|
return [
|
|
{
|
|
"normal": _rotate_grid_direction(Vector2i.LEFT, quarter_turns),
|
|
"offsets": _rotate_river_bend_offsets(
|
|
west_offsets,
|
|
quarter_turns,
|
|
),
|
|
},
|
|
{
|
|
"normal": _rotate_grid_direction(Vector2i.DOWN, quarter_turns),
|
|
"offsets": _rotate_river_bend_offsets(
|
|
south_offsets,
|
|
quarter_turns,
|
|
),
|
|
},
|
|
]
|
|
|
|
|
|
func _rotate_river_bend_offsets(
|
|
offsets: Array[Vector2i],
|
|
quarter_turns: int,
|
|
) -> Array[Vector2i]:
|
|
var result: Array[Vector2i] = []
|
|
for offset: Vector2i in offsets:
|
|
var rotated := offset
|
|
for _turn: int in posmod(quarter_turns, 4):
|
|
rotated = Vector2i(rotated.y, 1 - rotated.x)
|
|
result.append(rotated)
|
|
return result
|
|
|
|
|
|
func _rotate_grid_direction(
|
|
direction: Vector2i,
|
|
quarter_turns: int,
|
|
) -> Vector2i:
|
|
var result := direction
|
|
for _turn: int in posmod(quarter_turns, 4):
|
|
result = Vector2i(result.y, -result.x)
|
|
return result
|
|
|
|
|
|
func _river_pair_translation(
|
|
target_coordinates: Array[Vector2i],
|
|
local_offsets: Array[Vector2i],
|
|
) -> Vector2i:
|
|
if target_coordinates.size() != 2 or local_offsets.size() != 2:
|
|
return Vector2i(-1000000, -1000000)
|
|
for reversed_offsets: bool in [false, true]:
|
|
var first_offset := local_offsets[1 if reversed_offsets else 0]
|
|
var second_offset := local_offsets[0 if reversed_offsets else 1]
|
|
var origin := target_coordinates[0] - first_offset
|
|
if origin + second_offset == target_coordinates[1]:
|
|
return origin
|
|
return Vector2i(-1000000, -1000000)
|
|
|
|
|
|
func _river_distance_to_boundary(
|
|
pair: Array[Vector2i],
|
|
direction: Vector2i,
|
|
) -> int:
|
|
if pair.size() != 2:
|
|
return -1
|
|
match direction:
|
|
Vector2i.RIGHT:
|
|
return grid_size.x - 1 - pair[0].x
|
|
Vector2i.LEFT:
|
|
return pair[0].x
|
|
Vector2i.DOWN:
|
|
return grid_size.y - 1 - pair[0].y
|
|
Vector2i.UP:
|
|
return pair[0].y
|
|
return -1
|
|
|
|
|
|
func _build_river_feature_candidate(
|
|
source_coordinates: Array[Vector2i],
|
|
flow_direction: Vector2i,
|
|
bend_origin: Vector2i,
|
|
bend_turns: int,
|
|
bend_distance: int,
|
|
exit_coordinates: Array[Vector2i],
|
|
outlet_direction: Vector2i,
|
|
outlet_distance: int,
|
|
) -> Dictionary:
|
|
var placements: Array[Dictionary] = []
|
|
placements.append_array(
|
|
_river_endpoint_pair_specs(
|
|
source_coordinates,
|
|
flow_direction,
|
|
true,
|
|
)
|
|
)
|
|
var phase := posmod(
|
|
generation_seed
|
|
+ source_coordinates[0].x
|
|
+ source_coordinates[0].y
|
|
+ bend_origin.x
|
|
+ bend_origin.y,
|
|
2,
|
|
)
|
|
var straight_station_count := bend_distance + outlet_distance - 2
|
|
var bridge_station_index := posmod(
|
|
generation_seed
|
|
+ source_coordinates[0].x * 73856093
|
|
+ source_coordinates[0].y * 19349663
|
|
+ bend_origin.x * 83492791
|
|
+ bend_origin.y * 2971215073
|
|
+ bend_turns * 433494437,
|
|
straight_station_count,
|
|
)
|
|
var station_index := 0
|
|
for step: int in range(1, bend_distance):
|
|
var pair: Array[Vector2i] = []
|
|
for coordinate: Vector2i in source_coordinates:
|
|
pair.append(coordinate + flow_direction * step)
|
|
placements.append_array(
|
|
_river_edge_pair_specs(
|
|
pair,
|
|
flow_direction,
|
|
phase + station_index,
|
|
station_index == bridge_station_index,
|
|
)
|
|
)
|
|
station_index += 1
|
|
placements.append_array(
|
|
_river_bend_placement_specs(bend_origin, bend_turns)
|
|
)
|
|
for step: int in range(1, outlet_distance):
|
|
var pair: Array[Vector2i] = []
|
|
for coordinate: Vector2i in exit_coordinates:
|
|
pair.append(coordinate + outlet_direction * step)
|
|
placements.append_array(
|
|
_river_edge_pair_specs(
|
|
pair,
|
|
outlet_direction,
|
|
phase + station_index,
|
|
station_index == bridge_station_index,
|
|
)
|
|
)
|
|
station_index += 1
|
|
var outlet_coordinates: Array[Vector2i] = []
|
|
for coordinate: Vector2i in exit_coordinates:
|
|
outlet_coordinates.append(
|
|
coordinate + outlet_direction * outlet_distance
|
|
)
|
|
placements.append_array(
|
|
_river_endpoint_pair_specs(
|
|
outlet_coordinates,
|
|
outlet_direction,
|
|
false,
|
|
)
|
|
)
|
|
var coordinates: Array[Vector2i] = []
|
|
var minimum := Vector2i(1000000, 1000000)
|
|
var maximum := Vector2i(-1000000, -1000000)
|
|
for spec: Dictionary in placements:
|
|
var coordinate := spec.get("coordinate", Vector2i(-1, -1)) as Vector2i
|
|
coordinates.append(coordinate)
|
|
minimum.x = mini(minimum.x, coordinate.x)
|
|
minimum.y = mini(minimum.y, coordinate.y)
|
|
maximum.x = maxi(maximum.x, coordinate.x)
|
|
maximum.y = maxi(maximum.y, coordinate.y)
|
|
return {
|
|
"origin": minimum,
|
|
"size": maximum - minimum + Vector2i.ONE,
|
|
"flow_direction": flow_direction,
|
|
"outlet_direction": outlet_direction,
|
|
"source_coordinates": source_coordinates.duplicate(),
|
|
"outlet_coordinates": outlet_coordinates,
|
|
"coordinates": coordinates,
|
|
"placements": placements,
|
|
"bend_origin": bend_origin,
|
|
"bend_turns": bend_turns,
|
|
}
|
|
|
|
|
|
func _river_endpoint_pair_specs(
|
|
pair: Array[Vector2i],
|
|
flow_direction: Vector2i,
|
|
is_source: bool,
|
|
) -> Array[Dictionary]:
|
|
var ordered := _river_sorted_pair(pair, flow_direction)
|
|
if ordered.size() != 2:
|
|
return []
|
|
var result: Array[Dictionary] = []
|
|
match flow_direction:
|
|
Vector2i.RIGHT:
|
|
result.assign([
|
|
{
|
|
"coordinate": ordered[0],
|
|
"id": (
|
|
river_source_right_chunk_id
|
|
if is_source
|
|
else river_outlet_right_chunk_id
|
|
),
|
|
"turns": 0,
|
|
},
|
|
{
|
|
"coordinate": ordered[1],
|
|
"id": (
|
|
river_source_left_chunk_id
|
|
if is_source
|
|
else river_outlet_left_chunk_id
|
|
),
|
|
"turns": 0,
|
|
},
|
|
])
|
|
Vector2i.LEFT:
|
|
result.assign([
|
|
{
|
|
"coordinate": ordered[0],
|
|
"id": (
|
|
river_source_left_chunk_id
|
|
if is_source
|
|
else river_outlet_left_chunk_id
|
|
),
|
|
"turns": 2,
|
|
},
|
|
{
|
|
"coordinate": ordered[1],
|
|
"id": (
|
|
river_source_right_chunk_id
|
|
if is_source
|
|
else river_outlet_right_chunk_id
|
|
),
|
|
"turns": 2,
|
|
},
|
|
])
|
|
Vector2i.DOWN:
|
|
result.assign([
|
|
{
|
|
"coordinate": ordered[0],
|
|
"id": (
|
|
river_source_left_chunk_id
|
|
if is_source
|
|
else river_outlet_left_chunk_id
|
|
),
|
|
"turns": 3,
|
|
},
|
|
{
|
|
"coordinate": ordered[1],
|
|
"id": (
|
|
river_source_right_chunk_id
|
|
if is_source
|
|
else river_outlet_right_chunk_id
|
|
),
|
|
"turns": 3,
|
|
},
|
|
])
|
|
Vector2i.UP:
|
|
result.assign([
|
|
{
|
|
"coordinate": ordered[0],
|
|
"id": (
|
|
river_source_right_chunk_id
|
|
if is_source
|
|
else river_outlet_right_chunk_id
|
|
),
|
|
"turns": 1,
|
|
},
|
|
{
|
|
"coordinate": ordered[1],
|
|
"id": (
|
|
river_source_left_chunk_id
|
|
if is_source
|
|
else river_outlet_left_chunk_id
|
|
),
|
|
"turns": 1,
|
|
},
|
|
])
|
|
return result
|
|
|
|
|
|
func _river_edge_pair_specs(
|
|
pair: Array[Vector2i],
|
|
flow_direction: Vector2i,
|
|
phase: int,
|
|
use_stone_bridge := false,
|
|
) -> Array[Dictionary]:
|
|
var ordered := _river_sorted_pair(pair, flow_direction)
|
|
if ordered.size() != 2:
|
|
return []
|
|
var edge_ids: Array[StringName] = [
|
|
river_edge_chunk_id,
|
|
river_edge_variant_chunk_id,
|
|
]
|
|
if use_stone_bridge:
|
|
edge_ids.assign([
|
|
river_stone_bridge_chunk_id,
|
|
river_stone_bridge_chunk_id,
|
|
])
|
|
var first_index := posmod(phase, edge_ids.size())
|
|
var first_turns := 0 if flow_direction.x != 0 else 1
|
|
var second_turns := 2 if flow_direction.x != 0 else 3
|
|
return [
|
|
{
|
|
"coordinate": ordered[0],
|
|
"id": edge_ids[first_index],
|
|
"turns": first_turns,
|
|
},
|
|
{
|
|
"coordinate": ordered[1],
|
|
"id": edge_ids[posmod(first_index + 1, edge_ids.size())],
|
|
"turns": second_turns,
|
|
},
|
|
]
|
|
|
|
|
|
func _river_bend_placement_specs(
|
|
bend_origin: Vector2i,
|
|
quarter_turns: int,
|
|
) -> Array[Dictionary]:
|
|
var canonical: Array[Dictionary] = [
|
|
{
|
|
"offset": Vector2i(0, 0),
|
|
"id": river_bend_a_chunk_id,
|
|
"base_turns": 0,
|
|
},
|
|
{
|
|
"offset": Vector2i(1, 0),
|
|
"id": river_bend_b_chunk_id,
|
|
"base_turns": 0,
|
|
},
|
|
{
|
|
"offset": Vector2i(0, 1),
|
|
"id": river_bend_c_chunk_id,
|
|
"base_turns": 2,
|
|
},
|
|
{
|
|
"offset": Vector2i(1, 1),
|
|
"id": river_bend_d_chunk_id,
|
|
"base_turns": 0,
|
|
},
|
|
]
|
|
var result: Array[Dictionary] = []
|
|
for canonical_spec: Dictionary in canonical:
|
|
var offsets: Array[Vector2i] = [
|
|
canonical_spec["offset"] as Vector2i,
|
|
]
|
|
var rotated_offsets := _rotate_river_bend_offsets(
|
|
offsets,
|
|
quarter_turns,
|
|
)
|
|
result.append({
|
|
"coordinate": bend_origin + rotated_offsets[0],
|
|
"id": canonical_spec["id"],
|
|
"turns": posmod(
|
|
int(canonical_spec["base_turns"]) + quarter_turns,
|
|
4,
|
|
),
|
|
})
|
|
return result
|
|
|
|
|
|
func _river_sorted_pair(
|
|
pair: Array[Vector2i],
|
|
flow_direction: Vector2i,
|
|
) -> Array[Vector2i]:
|
|
var result: Array[Vector2i] = []
|
|
result.assign(pair)
|
|
if result.size() != 2:
|
|
return result
|
|
var swap := false
|
|
if flow_direction.x != 0:
|
|
swap = (
|
|
result[0].y > result[1].y
|
|
or (
|
|
result[0].y == result[1].y
|
|
and result[0].x > result[1].x
|
|
)
|
|
)
|
|
else:
|
|
swap = (
|
|
result[0].x > result[1].x
|
|
or (
|
|
result[0].x == result[1].x
|
|
and result[0].y > result[1].y
|
|
)
|
|
)
|
|
if swap:
|
|
var first := result[0]
|
|
result[0] = result[1]
|
|
result[1] = first
|
|
return result
|
|
|
|
|
|
func _river_source_coordinates(
|
|
origin: Vector2i,
|
|
footprint_size: Vector2i,
|
|
flow_direction: Vector2i,
|
|
) -> Array[Vector2i]:
|
|
if (
|
|
origin == _river_feature_origin
|
|
and footprint_size == _river_feature_footprint_size
|
|
and flow_direction == _river_feature_flow_direction
|
|
and not _river_feature_source_coordinates.is_empty()
|
|
):
|
|
var authored_result: Array[Vector2i] = []
|
|
authored_result.assign(_river_feature_source_coordinates)
|
|
return authored_result
|
|
var northwest := origin
|
|
var northeast := Vector2i(
|
|
origin.x + footprint_size.x - 1,
|
|
origin.y,
|
|
)
|
|
var southwest := Vector2i(
|
|
origin.x,
|
|
origin.y + footprint_size.y - 1,
|
|
)
|
|
var southeast := origin + footprint_size - Vector2i.ONE
|
|
match flow_direction:
|
|
Vector2i.RIGHT:
|
|
return [northwest, southwest]
|
|
Vector2i.LEFT:
|
|
return [northeast, southeast]
|
|
Vector2i.DOWN:
|
|
return [northwest, northeast]
|
|
Vector2i.UP:
|
|
return [southwest, southeast]
|
|
return []
|
|
|
|
|
|
func _cell_requires_reserved_grass(index: int) -> bool:
|
|
return (
|
|
_elevated_feature_reserved_grass_indices.has(index)
|
|
or _lake_feature_reserved_grass_indices.has(index)
|
|
or _river_feature_reserved_grass_indices.has(index)
|
|
)
|
|
|
|
|
|
func _build_grid_solver_caches() -> void:
|
|
_neighbor_indices.clear()
|
|
_neighbor_indices.resize(_placements.size())
|
|
_static_cell_candidate_masks.resize(_placements.size())
|
|
_static_cell_candidate_masks.fill(0)
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
for index: int in _placements.size():
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var neighbors := PackedInt32Array([-1, -1, -1, -1])
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if _coordinate_is_inside_grid(neighbor_coordinate):
|
|
neighbors[edge_value] = (
|
|
neighbor_coordinate.y * grid_size.x
|
|
+ neighbor_coordinate.x
|
|
)
|
|
_neighbor_indices[index] = neighbors
|
|
|
|
if not _packed_solver_domains_are_available():
|
|
continue
|
|
var candidate_mask := 0
|
|
for candidate_index: int in _solver_variants.size():
|
|
var candidate := _solver_variants[candidate_index]
|
|
if (
|
|
_cell_requires_reserved_grass(index)
|
|
and candidate.definition.stable_id
|
|
!= elevated_cliff_base_chunk_id
|
|
and not (
|
|
coordinate == center
|
|
and force_center_chunk_id != &""
|
|
and candidate.definition.stable_id
|
|
== force_center_chunk_id
|
|
and "grass" in candidate.definition.tags
|
|
and "flat" in candidate.definition.tags
|
|
)
|
|
):
|
|
continue
|
|
if (
|
|
coordinate == center
|
|
and force_center_chunk_id != &""
|
|
and candidate.definition.stable_id != force_center_chunk_id
|
|
):
|
|
continue
|
|
if not _variant_respects_ocean_boundary(candidate, coordinate):
|
|
continue
|
|
candidate_mask |= 1 << candidate_index
|
|
_static_cell_candidate_masks[index] = candidate_mask
|
|
|
|
|
|
func _apply_grass_sand_smoothing() -> bool:
|
|
if not grass_sand_smoothing_enabled:
|
|
return true
|
|
var diagonal_definition := catalog.definition_for_id(
|
|
smoothing_diagonal_chunk_id
|
|
)
|
|
if diagonal_definition == null:
|
|
push_error(
|
|
"Grass-sand smoothing references missing chunk %s."
|
|
% smoothing_diagonal_chunk_id
|
|
)
|
|
return false
|
|
if diagonal_definition.participates_in_base_solver:
|
|
push_error(
|
|
"Grass-sand smoothing chunk %s must be excluded from the base solver."
|
|
% smoothing_diagonal_chunk_id
|
|
)
|
|
return false
|
|
|
|
var candidates: Array[Dictionary] = []
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
for index: int in _placements.size():
|
|
var placement := _placements[index]
|
|
if placement == null or placement.definition.stable_id not in [
|
|
smoothing_grass_chunk_id,
|
|
smoothing_sand_chunk_id,
|
|
]:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
if (
|
|
_coordinate_is_on_boundary(coordinate)
|
|
or _cell_requires_reserved_grass(index)
|
|
or (
|
|
absi(coordinate.x - center.x)
|
|
+ absi(coordinate.y - center.y)
|
|
<= 1
|
|
)
|
|
):
|
|
continue
|
|
for quarter_turns: int in 4:
|
|
var diagonal := _authored_variant(
|
|
diagonal_definition,
|
|
quarter_turns,
|
|
)
|
|
if (
|
|
diagonal != null
|
|
and _variant_respects_ocean_boundary(
|
|
diagonal,
|
|
coordinate,
|
|
)
|
|
and _candidate_matches_placed_neighbors(diagonal, coordinate)
|
|
):
|
|
candidates.append({"index": index, "variant": diagonal})
|
|
break
|
|
|
|
var smoothing_random := RandomNumberGenerator.new()
|
|
smoothing_random.seed = generation_seed ^ 0xD1A60A1
|
|
for candidate_index: int in range(candidates.size() - 1, 0, -1):
|
|
var swap_index := smoothing_random.randi_range(0, candidate_index)
|
|
var temporary := candidates[candidate_index]
|
|
candidates[candidate_index] = candidates[swap_index]
|
|
candidates[swap_index] = temporary
|
|
|
|
var previous_placements: Array[TerrainChunkVariant] = []
|
|
previous_placements.assign(_placements)
|
|
var replacement_count := 0
|
|
for candidate: Dictionary in candidates:
|
|
if replacement_count >= maximum_smoothing_placements:
|
|
break
|
|
var index := int(candidate["index"])
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var diagonal := candidate["variant"] as TerrainChunkVariant
|
|
if (
|
|
diagonal == null
|
|
or not _candidate_matches_placed_neighbors(diagonal, coordinate)
|
|
or not _replacement_preserves_required_chunk(_placements[index])
|
|
):
|
|
continue
|
|
_set_placement(index, diagonal)
|
|
replacement_count += 1
|
|
|
|
var validation_error := _resolved_layout_validation_error(_placements)
|
|
if not validation_error.is_empty():
|
|
_assign_placements(previous_placements)
|
|
push_error(
|
|
"Grass-sand smoothing produced an invalid layout: "
|
|
+ validation_error
|
|
)
|
|
return false
|
|
return true
|
|
|
|
|
|
func _replacement_preserves_required_chunk(
|
|
original: TerrainChunkVariant,
|
|
) -> bool:
|
|
if original == null:
|
|
return false
|
|
var stable_id := original.definition.stable_id
|
|
if String(stable_id) not in required_chunk_ids:
|
|
return true
|
|
return _definition_placement_count(original.definition) > 1
|
|
|
|
|
|
func _apply_lake_feature() -> bool:
|
|
if not lake_feature_enabled:
|
|
return true
|
|
if (
|
|
_lake_feature_origin.x < 0
|
|
or _lake_feature_footprint_size.x < 2
|
|
or _lake_feature_footprint_size.y < 2
|
|
):
|
|
push_error("Lake feature has no reserved terrain region.")
|
|
return false
|
|
for spec: Dictionary in _lake_feature_placement_specs():
|
|
var definition := catalog.definition_for_id(spec["id"] as StringName)
|
|
var variant := _authored_variant(definition, int(spec["turns"]))
|
|
if variant == null:
|
|
push_error(
|
|
"Lake feature cannot resolve %s rotation %d."
|
|
% [spec["id"], spec["turns"]]
|
|
)
|
|
return false
|
|
var coordinate := spec["coordinate"] as Vector2i
|
|
_set_placement(
|
|
coordinate.y * grid_size.x + coordinate.x,
|
|
variant,
|
|
)
|
|
var validation_error := _resolved_layout_validation_error(_placements)
|
|
if not validation_error.is_empty():
|
|
push_error("Lake feature is invalid: " + validation_error)
|
|
return false
|
|
return true
|
|
|
|
|
|
func _lake_feature_placement_specs() -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
var northwest := _lake_feature_origin
|
|
var northeast := Vector2i(
|
|
_lake_feature_origin.x + _lake_feature_footprint_size.x - 1,
|
|
_lake_feature_origin.y,
|
|
)
|
|
var southwest := Vector2i(
|
|
_lake_feature_origin.x,
|
|
_lake_feature_origin.y + _lake_feature_footprint_size.y - 1,
|
|
)
|
|
var southeast := (
|
|
_lake_feature_origin
|
|
+ _lake_feature_footprint_size
|
|
- Vector2i.ONE
|
|
)
|
|
if _lake_feature_uses_diagonal_perimeter:
|
|
return _diagonal_lake_feature_placement_specs(
|
|
northwest,
|
|
northeast,
|
|
southwest,
|
|
southeast,
|
|
)
|
|
result.append({"coordinate": northwest, "id": lake_corner_chunk_id, "turns": 0})
|
|
result.append({"coordinate": northeast, "id": lake_corner_chunk_id, "turns": 3})
|
|
result.append({"coordinate": southwest, "id": lake_corner_chunk_id, "turns": 1})
|
|
result.append({"coordinate": southeast, "id": lake_corner_chunk_id, "turns": 2})
|
|
for row: int in range(northwest.y + 1, southwest.y):
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(northwest.x, row),
|
|
lake_edge_chunk_id,
|
|
0,
|
|
)
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(northeast.x, row),
|
|
lake_edge_chunk_id,
|
|
2,
|
|
)
|
|
for column: int in range(northwest.x + 1, northeast.x):
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(column, northwest.y),
|
|
lake_edge_chunk_id,
|
|
3,
|
|
)
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(column, southwest.y),
|
|
lake_edge_chunk_id,
|
|
1,
|
|
)
|
|
if _lake_feature_uses_fill:
|
|
for row: int in range(northwest.y + 1, southwest.y):
|
|
for column: int in range(northwest.x + 1, northeast.x):
|
|
result.append({
|
|
"coordinate": Vector2i(column, row),
|
|
"id": lake_fill_chunk_id,
|
|
"turns": 0,
|
|
})
|
|
return result
|
|
|
|
|
|
func _diagonal_lake_feature_placement_specs(
|
|
northwest: Vector2i,
|
|
northeast: Vector2i,
|
|
southwest: Vector2i,
|
|
southeast: Vector2i,
|
|
) -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = [
|
|
{"coordinate": northwest, "id": lake_diagonal_chunk_id, "turns": 0},
|
|
{"coordinate": northeast, "id": lake_diagonal_chunk_id, "turns": 3},
|
|
{"coordinate": southwest, "id": lake_diagonal_chunk_id, "turns": 1},
|
|
{"coordinate": southeast, "id": lake_diagonal_chunk_id, "turns": 2},
|
|
]
|
|
var top_side := _lake_top_side_specs(
|
|
_lake_feature_footprint_size.x - 2
|
|
)
|
|
for offset: int in top_side.size():
|
|
var top_spec: Dictionary = top_side[offset]
|
|
var reverse_spec: Dictionary = top_side[top_side.size() - 1 - offset]
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(northwest.x + 1 + offset, northwest.y),
|
|
top_spec["id"] as StringName,
|
|
int(top_spec["turns"]),
|
|
)
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(southwest.x + 1 + offset, southwest.y),
|
|
reverse_spec["id"] as StringName,
|
|
posmod(int(reverse_spec["turns"]) + 2, 4),
|
|
)
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(northwest.x, northwest.y + 1 + offset),
|
|
reverse_spec["id"] as StringName,
|
|
posmod(int(reverse_spec["turns"]) + 1, 4),
|
|
)
|
|
_append_lake_perimeter_spec(
|
|
result,
|
|
Vector2i(northeast.x, northeast.y + 1 + offset),
|
|
top_spec["id"] as StringName,
|
|
posmod(int(top_spec["turns"]) + 3, 4),
|
|
)
|
|
if _lake_feature_uses_fill:
|
|
for row: int in range(northwest.y + 1, southwest.y):
|
|
for column: int in range(northwest.x + 1, northeast.x):
|
|
result.append({
|
|
"coordinate": Vector2i(column, row),
|
|
"id": lake_fill_chunk_id,
|
|
"turns": 0,
|
|
})
|
|
return result
|
|
|
|
|
|
func _append_lake_perimeter_spec(
|
|
result: Array[Dictionary],
|
|
coordinate: Vector2i,
|
|
stable_id: StringName,
|
|
quarter_turns: int,
|
|
) -> void:
|
|
var selected_id := stable_id
|
|
if stable_id == lake_edge_chunk_id and lake_edge_variant_chunk_id != &"":
|
|
if posmod(int(generation_seed) + result.size(), 2) == 1:
|
|
selected_id = lake_edge_variant_chunk_id
|
|
result.append({
|
|
"coordinate": coordinate,
|
|
"id": selected_id,
|
|
"turns": quarter_turns,
|
|
})
|
|
|
|
|
|
func _lake_top_side_specs(interior_count: int) -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
if interior_count < 3:
|
|
return result
|
|
var uses_narrow_run := interior_count >= 4
|
|
if uses_narrow_run:
|
|
result.append({"id": lake_narrow_edge_chunk_id, "turns": 0})
|
|
result.append({"id": lake_narrow_to_regular_chunk_id, "turns": 0})
|
|
var regular_count := interior_count - (4 if uses_narrow_run else 2)
|
|
for _index: int in regular_count:
|
|
result.append({"id": lake_edge_chunk_id, "turns": 3})
|
|
result.append({"id": lake_regular_to_narrow_chunk_id, "turns": 0})
|
|
if uses_narrow_run:
|
|
result.append({"id": lake_narrow_edge_chunk_id, "turns": 0})
|
|
return result
|
|
|
|
|
|
func _apply_river_feature() -> bool:
|
|
if not river_feature_enabled:
|
|
return true
|
|
if (
|
|
_river_feature_origin.x < 0
|
|
or _river_feature_placements.is_empty()
|
|
or _river_feature_flow_direction == Vector2i.ZERO
|
|
or _river_feature_outlet_direction == Vector2i.ZERO
|
|
or (
|
|
_river_feature_flow_direction.x * _river_feature_outlet_direction.x
|
|
+ _river_feature_flow_direction.y * _river_feature_outlet_direction.y
|
|
) != 0
|
|
):
|
|
push_error("River feature has no reserved terrain region.")
|
|
return false
|
|
for spec: Dictionary in _river_feature_placement_specs():
|
|
var definition := catalog.definition_for_id(spec["id"] as StringName)
|
|
var variant := _authored_variant(definition, int(spec["turns"]))
|
|
if variant == null:
|
|
push_error(
|
|
"River feature cannot resolve %s rotation %d."
|
|
% [spec["id"], spec["turns"]]
|
|
)
|
|
return false
|
|
var coordinate := spec["coordinate"] as Vector2i
|
|
_set_placement(
|
|
coordinate.y * grid_size.x + coordinate.x,
|
|
variant,
|
|
)
|
|
var validation_error := _resolved_layout_validation_error(_placements)
|
|
if not validation_error.is_empty():
|
|
push_error("River feature is invalid: " + validation_error)
|
|
return false
|
|
return true
|
|
|
|
|
|
func _river_feature_placement_specs() -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
result.assign(_river_feature_placements)
|
|
return result
|
|
|
|
|
|
func _apply_elevated_feature() -> bool:
|
|
if not elevated_cliff_feature_enabled:
|
|
return true
|
|
if _elevated_feature_center.x < 0 or _elevated_feature_center.y < 0:
|
|
push_error("Elevated cliff feature has no reserved base region.")
|
|
return false
|
|
if (
|
|
elevated_cliff_coastal_feature_required
|
|
and _secondary_elevated_feature_center.x < 0
|
|
):
|
|
push_error("Elevated cliff feature is missing its required coastal assembly.")
|
|
return false
|
|
var feature_random := RandomNumberGenerator.new()
|
|
feature_random.seed = generation_seed ^ 0x51A7C11FF
|
|
var tier_random := RandomNumberGenerator.new()
|
|
tier_random.seed = generation_seed ^ 0x4C1FF713D
|
|
# The authored river source is a third-tier cliff pair. A generated river
|
|
# therefore promotes its inland source feature to that matching elevation;
|
|
# rivers may never begin from an unrelated flat cap.
|
|
var use_third_tier := (
|
|
river_feature_enabled
|
|
or tier_random.randf() < elevated_cliff_third_tier_base_chance
|
|
)
|
|
var placements := _elevated_feature_placement_specs(
|
|
_elevated_feature_center,
|
|
ELEVATED_FEATURE_RADIUS,
|
|
feature_random,
|
|
use_third_tier,
|
|
)
|
|
var grass_coast_count := int(
|
|
_placement_counts.get(elevated_cliff_coast_base_chunk_id, 0)
|
|
)
|
|
var beach_coast_count := int(
|
|
_placement_counts.get(elevated_cliff_beach_base_chunk_id, 0)
|
|
)
|
|
if _secondary_elevated_feature_center.x >= 0:
|
|
if grass_coast_count <= 0 and beach_coast_count <= 0:
|
|
push_error("Coastal cliff feature has no compatible coastline base.")
|
|
return false
|
|
placements.append_array(
|
|
_coastal_elevated_feature_placement_specs(
|
|
_secondary_elevated_feature_center,
|
|
feature_random,
|
|
beach_coast_count > grass_coast_count,
|
|
)
|
|
)
|
|
for spec: Dictionary in placements:
|
|
var definition := catalog.definition_for_id(spec["id"] as StringName)
|
|
var variant := _authored_variant(definition, int(spec["turns"]))
|
|
if variant == null:
|
|
push_error(
|
|
"Elevated cliff feature cannot resolve %s rotation %d."
|
|
% [spec["id"], spec["turns"]]
|
|
)
|
|
return false
|
|
var coordinate := spec["coordinate"] as Vector2i
|
|
_set_placement(
|
|
coordinate.y * grid_size.x + coordinate.x,
|
|
variant,
|
|
)
|
|
var validation_error := _resolved_layout_validation_error(_placements)
|
|
if not validation_error.is_empty():
|
|
push_error("Elevated cliff feature is invalid: " + validation_error)
|
|
return false
|
|
return true
|
|
|
|
|
|
func _elevated_feature_placement_specs(
|
|
center: Vector2i,
|
|
radius: int,
|
|
feature_random: RandomNumberGenerator,
|
|
use_third_tier: bool = false,
|
|
) -> Array[Dictionary]:
|
|
var top_chunk_id := (
|
|
elevated_cliff_third_tier_top_chunk_id
|
|
if use_third_tier
|
|
else elevated_cliff_top_chunk_id
|
|
)
|
|
var corner_chunk_id := (
|
|
elevated_cliff_third_tier_corner_chunk_id
|
|
if use_third_tier
|
|
else elevated_cliff_corner_chunk_id
|
|
)
|
|
var edge_chunk_id := (
|
|
elevated_cliff_third_tier_edge_chunk_id
|
|
if use_third_tier
|
|
else elevated_cliff_edge_chunk_id
|
|
)
|
|
var edge_specs: Array[Dictionary] = [
|
|
{
|
|
"edge": TerrainChunkTopology.Edge.NORTH,
|
|
"offset": Vector2i(0, -radius),
|
|
"turns": 1,
|
|
},
|
|
{
|
|
"edge": TerrainChunkTopology.Edge.EAST,
|
|
"offset": Vector2i(radius, 0),
|
|
"turns": 0,
|
|
},
|
|
{
|
|
"edge": TerrainChunkTopology.Edge.SOUTH,
|
|
"offset": Vector2i(0, radius),
|
|
"turns": 3,
|
|
},
|
|
{
|
|
"edge": TerrainChunkTopology.Edge.WEST,
|
|
"offset": Vector2i(-radius, 0),
|
|
"turns": 2,
|
|
},
|
|
]
|
|
var ramp_edge: int = -1
|
|
if (
|
|
not use_third_tier
|
|
and feature_random.randf() < elevated_cliff_ramp_chance
|
|
):
|
|
var usable_edges: Array[int] = []
|
|
for edge_index: int in edge_specs.size():
|
|
var edge_spec := edge_specs[edge_index]
|
|
var landing_coordinate := (
|
|
center
|
|
+ (edge_spec["offset"] as Vector2i)
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_spec["edge"] as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if (
|
|
_coordinate_is_inside_grid(landing_coordinate)
|
|
and not _coordinate_is_on_boundary(landing_coordinate)
|
|
):
|
|
usable_edges.append(edge_index)
|
|
if not usable_edges.is_empty():
|
|
ramp_edge = usable_edges[
|
|
feature_random.randi_range(0, usable_edges.size() - 1)
|
|
]
|
|
var placements: Array[Dictionary] = []
|
|
for row_offset: int in range(-radius, radius + 1):
|
|
for column_offset: int in range(-radius, radius + 1):
|
|
var offset := Vector2i(column_offset, row_offset)
|
|
var on_horizontal_edge := absi(column_offset) == radius
|
|
var on_vertical_edge := absi(row_offset) == radius
|
|
if on_horizontal_edge and on_vertical_edge:
|
|
placements.append({
|
|
"coordinate": center + offset,
|
|
"id": corner_chunk_id,
|
|
"turns": _elevated_corner_turns(offset),
|
|
})
|
|
continue
|
|
if on_horizontal_edge or on_vertical_edge:
|
|
var edge_index := _elevated_edge_index(offset, radius)
|
|
placements.append({
|
|
"coordinate": center + offset,
|
|
"id": (
|
|
elevated_cliff_ramp_chunk_id
|
|
if ramp_edge >= 0
|
|
and offset == edge_specs[ramp_edge]["offset"]
|
|
else edge_chunk_id
|
|
),
|
|
"turns": edge_specs[edge_index]["turns"],
|
|
})
|
|
continue
|
|
placements.append({
|
|
"coordinate": center + offset,
|
|
"id": top_chunk_id,
|
|
"turns": 0,
|
|
})
|
|
return placements
|
|
|
|
|
|
func _coastal_elevated_feature_placement_specs(
|
|
center: Vector2i,
|
|
feature_random: RandomNumberGenerator,
|
|
use_beach_base: bool,
|
|
) -> Array[Dictionary]:
|
|
var placements := _elevated_feature_placement_specs(
|
|
center,
|
|
SECONDARY_ELEVATED_FEATURE_RADIUS,
|
|
feature_random,
|
|
false,
|
|
)
|
|
for spec: Dictionary in placements:
|
|
var coordinate := spec["coordinate"] as Vector2i
|
|
var outside_edge_count := _outside_edge_count(coordinate)
|
|
if outside_edge_count == 2:
|
|
spec["id"] = elevated_cliff_sea_corner_chunk_id
|
|
elif (
|
|
outside_edge_count == 1
|
|
and spec["id"] == elevated_cliff_corner_chunk_id
|
|
):
|
|
spec["id"] = _coastal_transition_id(
|
|
coordinate,
|
|
int(spec["turns"]),
|
|
use_beach_base,
|
|
)
|
|
elif outside_edge_count == 1 and (
|
|
spec["id"] == elevated_cliff_edge_chunk_id
|
|
or spec["id"] == elevated_cliff_ramp_chunk_id
|
|
):
|
|
spec["id"] = elevated_cliff_sea_edge_chunk_id
|
|
return placements
|
|
|
|
|
|
func _coastal_transition_id(
|
|
coordinate: Vector2i,
|
|
quarter_turns: int,
|
|
use_beach_base: bool = false,
|
|
) -> StringName:
|
|
var transition_ids: Array[StringName] = []
|
|
if use_beach_base:
|
|
transition_ids.assign([
|
|
elevated_cliff_beach_transition_right_chunk_id,
|
|
elevated_cliff_beach_transition_left_chunk_id,
|
|
])
|
|
else:
|
|
transition_ids.assign([
|
|
elevated_cliff_sea_transition_right_chunk_id,
|
|
elevated_cliff_sea_transition_left_chunk_id,
|
|
])
|
|
for stable_id: StringName in transition_ids:
|
|
var definition := catalog.definition_for_id(stable_id)
|
|
var variant := _authored_variant(definition, quarter_turns)
|
|
if (
|
|
variant != null
|
|
and _variant_respects_ocean_boundary(variant, coordinate)
|
|
):
|
|
return stable_id
|
|
push_error(
|
|
"Coastal cliff feature has no transition for %s rotation %d."
|
|
% [coordinate, quarter_turns]
|
|
)
|
|
return &""
|
|
|
|
|
|
func _outside_edge_count(coordinate: Vector2i) -> int:
|
|
var result := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor):
|
|
result += 1
|
|
return result
|
|
|
|
|
|
func _elevated_edge_index(offset: Vector2i, radius: int) -> int:
|
|
if offset.y == -radius:
|
|
return 0
|
|
if offset.x == radius:
|
|
return 1
|
|
if offset.y == radius:
|
|
return 2
|
|
return 3
|
|
|
|
|
|
func _elevated_corner_turns(offset: Vector2i) -> int:
|
|
if offset.x < 0 and offset.y < 0:
|
|
return 2
|
|
if offset.x > 0 and offset.y < 0:
|
|
return 1
|
|
if offset.x < 0 and offset.y > 0:
|
|
return 3
|
|
return 0
|
|
|
|
|
|
func _configure_stacked_elevated_feature() -> bool:
|
|
_stacked_elevated_placements.clear()
|
|
if (
|
|
not elevated_cliff_feature_enabled
|
|
or elevated_cliff_third_level_chance <= 0.0
|
|
):
|
|
return true
|
|
var top_coordinate := _primary_elevated_top_coordinate()
|
|
if top_coordinate.x < 0:
|
|
return true
|
|
var top_index := top_coordinate.y * grid_size.x + top_coordinate.x
|
|
if (
|
|
top_index < 0
|
|
or top_index >= _placements.size()
|
|
or _placements[top_index] == null
|
|
):
|
|
push_error("Stacked cliff feature lost its central supporting top.")
|
|
return false
|
|
var supporting_top_id := _placements[top_index].definition.stable_id
|
|
if supporting_top_id not in [
|
|
elevated_cliff_top_chunk_id,
|
|
elevated_cliff_third_tier_top_chunk_id,
|
|
]:
|
|
push_error("Stacked cliff feature lost its central supporting top.")
|
|
return false
|
|
var stack_random := RandomNumberGenerator.new()
|
|
stack_random.seed = generation_seed ^ 0x7312DC11F
|
|
if stack_random.randf() >= elevated_cliff_third_level_chance:
|
|
return true
|
|
var supporting_third_tier := (
|
|
supporting_top_id == elevated_cliff_third_tier_top_chunk_id
|
|
)
|
|
var use_third_tier_stack := (
|
|
stack_random.randf() < (
|
|
elevated_cliff_double_third_tier_chance
|
|
if supporting_third_tier
|
|
else elevated_cliff_third_tier_stack_chance
|
|
)
|
|
)
|
|
var stacked_corner_chunk_id := (
|
|
elevated_cliff_third_tier_corner_chunk_id
|
|
if use_third_tier_stack
|
|
else elevated_cliff_corner_chunk_id
|
|
)
|
|
var vertical_offset := (
|
|
elevated_cliff_level_height * 2.0
|
|
if supporting_third_tier
|
|
else elevated_cliff_level_height
|
|
)
|
|
var corner_definition := catalog.definition_for_id(
|
|
stacked_corner_chunk_id
|
|
)
|
|
if corner_definition == null:
|
|
push_error(
|
|
"Stacked cliff feature references missing corner chunk %s."
|
|
% stacked_corner_chunk_id
|
|
)
|
|
return false
|
|
# Four nearly half-cell-offset corners form a closed 2x2 ring. A slight
|
|
# inward overlap keeps the authored curved feet fully seated on the lower
|
|
# plateau instead of exposing a hairline gap at its outermost vertices.
|
|
var specs: Array[Dictionary] = [
|
|
{"offset": Vector2(-0.45, -0.45), "turns": 2},
|
|
{"offset": Vector2(0.45, -0.45), "turns": 1},
|
|
{"offset": Vector2(-0.45, 0.45), "turns": 3},
|
|
{"offset": Vector2(0.45, 0.45), "turns": 0},
|
|
]
|
|
for spec: Dictionary in specs:
|
|
var variant := _authored_variant(
|
|
corner_definition,
|
|
int(spec["turns"]),
|
|
)
|
|
if variant == null:
|
|
push_error(
|
|
"Stacked cliff feature cannot resolve %s rotation %d."
|
|
% [stacked_corner_chunk_id, spec["turns"]]
|
|
)
|
|
return false
|
|
_stacked_elevated_placements.append({
|
|
"support_coordinate": top_coordinate,
|
|
"offset": spec["offset"],
|
|
"vertical_offset": vertical_offset,
|
|
"variant": variant,
|
|
})
|
|
return true
|
|
|
|
|
|
func _primary_elevated_top_coordinate() -> Vector2i:
|
|
if _coordinate_has_primary_elevated_top(_elevated_feature_center):
|
|
return _elevated_feature_center
|
|
for index: int in _placements.size():
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
if not _coordinate_has_primary_elevated_top(coordinate):
|
|
continue
|
|
var top_id := _placements[index].definition.stable_id
|
|
var surrounded_by_matching_tops := true
|
|
for row_offset: int in range(-1, 2):
|
|
for column_offset: int in range(-1, 2):
|
|
var neighbor := coordinate + Vector2i(column_offset, row_offset)
|
|
if not _coordinate_is_inside_grid(neighbor):
|
|
surrounded_by_matching_tops = false
|
|
break
|
|
var neighbor_index := neighbor.y * grid_size.x + neighbor.x
|
|
var neighbor_variant := _placements[neighbor_index]
|
|
if (
|
|
neighbor_variant == null
|
|
or neighbor_variant.definition.stable_id != top_id
|
|
):
|
|
surrounded_by_matching_tops = false
|
|
break
|
|
if not surrounded_by_matching_tops:
|
|
break
|
|
if surrounded_by_matching_tops:
|
|
_elevated_feature_center = coordinate
|
|
return coordinate
|
|
return Vector2i(-1, -1)
|
|
|
|
|
|
func _coordinate_has_primary_elevated_top(coordinate: Vector2i) -> bool:
|
|
if not _coordinate_is_inside_grid(coordinate):
|
|
return false
|
|
var index := coordinate.y * grid_size.x + coordinate.x
|
|
if index < 0 or index >= _placements.size() or _placements[index] == null:
|
|
return false
|
|
return _placements[index].definition.stable_id in [
|
|
elevated_cliff_top_chunk_id,
|
|
elevated_cliff_third_tier_top_chunk_id,
|
|
]
|
|
|
|
|
|
func _variant_respects_ocean_boundary(
|
|
variant: TerrainChunkVariant,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
var maximum_boundary_distance := (
|
|
variant.definition.maximum_boundary_distance
|
|
)
|
|
if (
|
|
maximum_boundary_distance >= 0
|
|
and _distance_from_map_boundary(coordinate)
|
|
> maximum_boundary_distance
|
|
):
|
|
return false
|
|
var ocean_edges := variant.rotated_edge_mask(
|
|
variant.definition.ocean_facing_edges
|
|
)
|
|
if (
|
|
variant.definition.must_be_interior
|
|
and _coordinate_is_on_boundary(coordinate)
|
|
):
|
|
return false
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var ocean_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
var edge_faces_ocean := (ocean_edges & (1 << edge_value)) != 0
|
|
if _coordinate_is_inside_grid(ocean_coordinate):
|
|
if edge_faces_ocean:
|
|
return false
|
|
continue
|
|
if edge_faces_ocean:
|
|
continue
|
|
return false
|
|
return true
|
|
|
|
|
|
func _candidate_matches_placed_neighbors(
|
|
candidate: TerrainChunkVariant,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor_index := (
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
)
|
|
var neighbor := _placements[neighbor_index]
|
|
if neighbor == null:
|
|
continue
|
|
if not _edges_are_compatible(
|
|
candidate,
|
|
edge,
|
|
neighbor,
|
|
TerrainChunkTopology.opposite_edge(edge),
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _coordinate_is_inside_grid(coordinate: Vector2i) -> bool:
|
|
return (
|
|
coordinate.x >= 0
|
|
and coordinate.y >= 0
|
|
and coordinate.x < grid_size.x
|
|
and coordinate.y < grid_size.y
|
|
)
|
|
|
|
|
|
func _definition_has_capacity(definition: TerrainChunkDefinition) -> bool:
|
|
if definition.maximum_placements < 0:
|
|
return true
|
|
return _definition_placement_count(definition) < definition.maximum_placements
|
|
|
|
|
|
func _definition_placement_count(definition: TerrainChunkDefinition) -> int:
|
|
if definition == null:
|
|
return 0
|
|
return int(_placement_counts.get(definition.stable_id, 0))
|
|
|
|
|
|
func _edges_are_compatible(
|
|
first: TerrainChunkVariant,
|
|
first_edge: TerrainChunkTopology.Edge,
|
|
second: TerrainChunkVariant,
|
|
second_edge: TerrainChunkTopology.Edge,
|
|
) -> bool:
|
|
if (
|
|
second_edge == TerrainChunkTopology.opposite_edge(first_edge)
|
|
and _solver_variant_indices.has(first)
|
|
and _solver_variant_indices.has(second)
|
|
):
|
|
var cache_index := _edge_compatibility_index(
|
|
int(_solver_variant_indices[first]),
|
|
first_edge,
|
|
int(_solver_variant_indices[second]),
|
|
)
|
|
return _edge_compatibility[cache_index] != 0
|
|
return _calculate_edge_compatibility(
|
|
first,
|
|
first_edge,
|
|
second,
|
|
second_edge,
|
|
)
|
|
|
|
|
|
func _calculate_edge_compatibility(
|
|
first: TerrainChunkVariant,
|
|
first_edge: TerrainChunkTopology.Edge,
|
|
second: TerrainChunkVariant,
|
|
second_edge: TerrainChunkTopology.Edge,
|
|
) -> bool:
|
|
var first_inlet := _variant_edge_has_connector(
|
|
first,
|
|
first.definition.water_inlet_edges,
|
|
first_edge,
|
|
)
|
|
var first_outlet := _variant_edge_has_connector(
|
|
first,
|
|
first.definition.water_outlet_edges,
|
|
first_edge,
|
|
)
|
|
var second_inlet := _variant_edge_has_connector(
|
|
second,
|
|
second.definition.water_inlet_edges,
|
|
second_edge,
|
|
)
|
|
var second_outlet := _variant_edge_has_connector(
|
|
second,
|
|
second.definition.water_outlet_edges,
|
|
second_edge,
|
|
)
|
|
var first_has_water := first_inlet or first_outlet
|
|
var second_has_water := second_inlet or second_outlet
|
|
var profile_tolerance := maxf(
|
|
edge_match_tolerance,
|
|
maxf(
|
|
first.definition.edge_profile_tolerance_override,
|
|
second.definition.edge_profile_tolerance_override,
|
|
),
|
|
)
|
|
var profiles_match := first.profile(first_edge).matches(
|
|
second.profile(second_edge),
|
|
profile_tolerance,
|
|
)
|
|
if not profiles_match and (
|
|
_variant_edge_has_connector(
|
|
first,
|
|
first.definition.buried_cliff_seam_edges,
|
|
first_edge,
|
|
)
|
|
or _variant_edge_has_connector(
|
|
second,
|
|
second.definition.buried_cliff_seam_edges,
|
|
second_edge,
|
|
)
|
|
):
|
|
profiles_match = first.profile(first_edge).matches_above_height(
|
|
second.profile(second_edge),
|
|
0.0,
|
|
profile_tolerance,
|
|
)
|
|
if not profiles_match and first_has_water and second_has_water:
|
|
profiles_match = first.profile(first_edge).matches_at_or_above_height(
|
|
second.profile(second_edge),
|
|
0.0,
|
|
profile_tolerance,
|
|
)
|
|
if (
|
|
not profiles_match
|
|
and first_has_water
|
|
and second_has_water
|
|
and (
|
|
_variant_edge_has_connector(
|
|
first,
|
|
first.definition.continuous_water_seam_edges,
|
|
first_edge,
|
|
)
|
|
or _variant_edge_has_connector(
|
|
second,
|
|
second.definition.continuous_water_seam_edges,
|
|
second_edge,
|
|
)
|
|
)
|
|
):
|
|
profiles_match = true
|
|
if not profiles_match:
|
|
return false
|
|
if first_has_water or second_has_water:
|
|
return (
|
|
(first_outlet and second_inlet)
|
|
or (first_inlet and second_outlet)
|
|
)
|
|
return (
|
|
_edge_surfaces_are_compatible(
|
|
first,
|
|
first_edge,
|
|
second,
|
|
second_edge,
|
|
)
|
|
and first.allows_non_water_neighbor_on_edge(second, first_edge)
|
|
and second.allows_non_water_neighbor_on_edge(first, second_edge)
|
|
)
|
|
|
|
|
|
func _edge_surfaces_are_compatible(
|
|
first: TerrainChunkVariant,
|
|
first_edge: TerrainChunkTopology.Edge,
|
|
second: TerrainChunkVariant,
|
|
second_edge: TerrainChunkTopology.Edge,
|
|
) -> bool:
|
|
var first_tags := first.surface_tags(first_edge)
|
|
var second_tags := second.surface_tags(second_edge)
|
|
if first_tags.is_empty() or second_tags.is_empty():
|
|
return true
|
|
return TerrainChunkDefinition.has_any_tag(first_tags, second_tags)
|
|
|
|
|
|
func _variant_edge_has_connector(
|
|
variant: TerrainChunkVariant,
|
|
source_mask: int,
|
|
edge: TerrainChunkTopology.Edge,
|
|
) -> bool:
|
|
return (variant.rotated_edge_mask(source_mask) & (1 << int(edge))) != 0
|
|
|
|
|
|
func _required_chunks_are_present() -> bool:
|
|
return _missing_required_ids().is_empty()
|
|
|
|
|
|
func _requirements_can_still_be_satisfied() -> bool:
|
|
var missing_required := _missing_required_ids()
|
|
if missing_required.size() > _unfilled_cell_count():
|
|
return false
|
|
for stable_id: StringName in missing_required:
|
|
var definition := catalog.definition_for_id(stable_id)
|
|
if definition == null or not _definition_has_capacity(definition):
|
|
return false
|
|
if not _definition_has_available_cell(definition):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _definition_has_available_cell(
|
|
definition: TerrainChunkDefinition,
|
|
) -> bool:
|
|
var definition_variants: Array = _solver_variants_by_definition.get(
|
|
definition.stable_id,
|
|
[],
|
|
)
|
|
if (
|
|
_packed_solver_domains_are_available()
|
|
and not definition_variants.is_empty()
|
|
):
|
|
var definition_mask := int(
|
|
_definition_variant_masks.get(definition.stable_id, 0)
|
|
)
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
var candidate_mask := (
|
|
int(_static_cell_candidate_masks[index]) & definition_mask
|
|
)
|
|
if candidate_mask == 0:
|
|
continue
|
|
candidate_mask = _mask_matching_placed_neighbors(
|
|
candidate_mask,
|
|
index,
|
|
)
|
|
if (
|
|
candidate_mask != 0
|
|
and _candidate_dynamic_cell_rules_are_satisfied(
|
|
definition_variants[0] as TerrainChunkVariant,
|
|
index,
|
|
)
|
|
):
|
|
return true
|
|
return false
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
for candidate: TerrainChunkVariant in definition_variants:
|
|
if _candidate_can_occupy_cell(candidate, index):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _unfilled_cell_count() -> int:
|
|
return _unfilled_cells
|
|
|
|
|
|
func _missing_required_ids() -> Array[StringName]:
|
|
var result: Array[StringName] = []
|
|
for required_id: StringName in _required_stable_ids:
|
|
if (
|
|
int(_placement_counts.get(required_id, 0)) <= 0
|
|
):
|
|
result.append(required_id)
|
|
return result
|
|
|
|
|
|
func _weighted_candidate_order(
|
|
candidates: Array[TerrainChunkVariant],
|
|
index: int,
|
|
) -> Array[TerrainChunkVariant]:
|
|
var candidate_counts: Dictionary[StringName, int] = {}
|
|
for candidate: TerrainChunkVariant in candidates:
|
|
var stable_id := candidate.definition.stable_id
|
|
candidate_counts[stable_id] = candidate_counts.get(stable_id, 0) + 1
|
|
var remaining: Array[TerrainChunkVariant] = []
|
|
var weight_units: Array[int] = []
|
|
for candidate: TerrainChunkVariant in candidates:
|
|
var stable_id := candidate.definition.stable_id
|
|
var rotation_count := maxi(candidate_counts.get(stable_id, 1), 1)
|
|
var weight := (
|
|
maxf(candidate.definition.selection_weight, 0.01)
|
|
/ float(rotation_count)
|
|
)
|
|
if _required_chunk_is_missing(candidate.definition.stable_id):
|
|
weight *= required_chunk_weight_multiplier
|
|
var preferred_neighbors := _preferred_neighbor_count(
|
|
index,
|
|
candidate.definition,
|
|
)
|
|
for _preferred_index: int in preferred_neighbors:
|
|
weight *= preferred_neighbor_weight_multiplier
|
|
for _repeat_index: int in _long_definition_run_count(
|
|
index,
|
|
candidate.definition,
|
|
):
|
|
weight *= long_repeat_weight_multiplier
|
|
if (
|
|
candidate.definition.prefers_map_boundary
|
|
and _coordinate_is_on_boundary(
|
|
Vector2i(index % grid_size.x, index / grid_size.x)
|
|
)
|
|
):
|
|
weight *= boundary_preference_multiplier
|
|
remaining.append(candidate)
|
|
weight_units.append(
|
|
maxi(roundi(weight * CANDIDATE_WEIGHT_SCALE), 1)
|
|
)
|
|
var result: Array[TerrainChunkVariant] = []
|
|
while not remaining.is_empty():
|
|
var total_units := 0
|
|
for units: int in weight_units:
|
|
total_units += units
|
|
var ticket := _random.randi_range(1, total_units)
|
|
var accumulated := 0
|
|
var selected_index := 0
|
|
for candidate_index: int in remaining.size():
|
|
accumulated += weight_units[candidate_index]
|
|
if ticket <= accumulated:
|
|
selected_index = candidate_index
|
|
break
|
|
result.append(remaining[selected_index])
|
|
remaining.remove_at(selected_index)
|
|
weight_units.remove_at(selected_index)
|
|
return result
|
|
|
|
|
|
func _adjacent_definition_repeat_count(
|
|
index: int,
|
|
definition: TerrainChunkDefinition,
|
|
) -> int:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var count := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if neighbor != null and neighbor.definition == definition:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _preferred_neighbor_count(
|
|
index: int,
|
|
definition: TerrainChunkDefinition,
|
|
) -> int:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var count := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if neighbor != null and definition.prefers_neighbor(neighbor.definition):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _long_definition_run_count(
|
|
index: int,
|
|
definition: TerrainChunkDefinition,
|
|
) -> int:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var count := 0
|
|
for axis: Vector2i in [Vector2i.RIGHT, Vector2i.DOWN]:
|
|
if (
|
|
_definition_at(coordinate - axis) == definition
|
|
and _definition_at(coordinate + axis) == definition
|
|
):
|
|
count += 1
|
|
for direction: int in [-1, 1]:
|
|
if (
|
|
_definition_at(coordinate + axis * direction) == definition
|
|
and _definition_at(coordinate + axis * direction * 2)
|
|
== definition
|
|
):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _definition_at(coordinate: Vector2i) -> TerrainChunkDefinition:
|
|
if not _coordinate_is_inside_grid(coordinate):
|
|
return null
|
|
var placement := _placements[coordinate.y * grid_size.x + coordinate.x]
|
|
return placement.definition if placement != null else null
|
|
|
|
|
|
func _coordinate_is_on_boundary(coordinate: Vector2i) -> bool:
|
|
return (
|
|
coordinate.x == 0
|
|
or coordinate.y == 0
|
|
or coordinate.x == grid_size.x - 1
|
|
or coordinate.y == grid_size.y - 1
|
|
)
|
|
|
|
|
|
func _distance_from_map_boundary(coordinate: Vector2i) -> int:
|
|
return mini(
|
|
mini(coordinate.x, coordinate.y),
|
|
mini(
|
|
grid_size.x - 1 - coordinate.x,
|
|
grid_size.y - 1 - coordinate.y,
|
|
),
|
|
)
|
|
|
|
|
|
func _neighbor_requirement_validation_error(
|
|
layout: Array[TerrainChunkVariant],
|
|
) -> String:
|
|
for index: int in layout.size():
|
|
var placement := layout[index]
|
|
if placement == null or placement.definition.minimum_required_neighbors <= 0:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var matching_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := layout[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if (
|
|
neighbor != null
|
|
and _definition_matches_required_neighbor_tags(
|
|
placement.definition,
|
|
neighbor.definition,
|
|
)
|
|
):
|
|
matching_neighbors += 1
|
|
if matching_neighbors < placement.definition.minimum_required_neighbors:
|
|
return (
|
|
"%s at cell %d has %d required neighbors; expected at least %d."
|
|
% [
|
|
placement.definition.stable_id,
|
|
index,
|
|
matching_neighbors,
|
|
placement.definition.minimum_required_neighbors,
|
|
]
|
|
)
|
|
return ""
|
|
|
|
|
|
func _walkable_connectivity_validation_error(
|
|
layout: Array[TerrainChunkVariant],
|
|
) -> String:
|
|
var walkable_indices: Array[int] = []
|
|
for index: int in layout.size():
|
|
var placement := layout[index]
|
|
if placement != null and "walkable" in placement.definition.tags:
|
|
walkable_indices.append(index)
|
|
if walkable_indices.is_empty():
|
|
return ""
|
|
var start_index := walkable_indices[0]
|
|
if force_center_chunk_id != &"":
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
var center_index := center.y * grid_size.x + center.x
|
|
if (
|
|
layout[center_index] != null
|
|
and "walkable" in layout[center_index].definition.tags
|
|
):
|
|
start_index = center_index
|
|
var visited: Dictionary[int, bool] = {start_index: true}
|
|
var pending: Array[int] = [start_index]
|
|
var pending_cursor := 0
|
|
while pending_cursor < pending.size():
|
|
var index := pending[pending_cursor]
|
|
pending_cursor += 1
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var current := layout[index]
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
var neighbor_coordinate := (
|
|
coordinate + TerrainChunkTopology.grid_offset(edge)
|
|
)
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor_index := (
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
)
|
|
if visited.has(neighbor_index):
|
|
continue
|
|
var neighbor := layout[neighbor_index]
|
|
if neighbor == null or "walkable" not in neighbor.definition.tags:
|
|
continue
|
|
if not _edges_are_compatible(
|
|
current,
|
|
edge,
|
|
neighbor,
|
|
TerrainChunkTopology.opposite_edge(edge),
|
|
):
|
|
continue
|
|
visited[neighbor_index] = true
|
|
pending.append(neighbor_index)
|
|
if visited.size() != walkable_indices.size():
|
|
return (
|
|
"walkable terrain is split into disconnected regions "
|
|
+ "(%d of %d cells reachable from spawn)."
|
|
% [visited.size(), walkable_indices.size()]
|
|
)
|
|
return ""
|
|
|
|
|
|
func _required_chunk_is_missing(stable_id: StringName) -> bool:
|
|
if not required_chunk_ids.has(String(stable_id)):
|
|
return false
|
|
return int(_placement_counts.get(stable_id, 0)) <= 0
|
|
|
|
|
|
func _resolve_equivalent_rotations() -> void:
|
|
for index: int in _placements.size():
|
|
var placement := _placements[index]
|
|
if placement == null:
|
|
continue
|
|
var rotations: PackedInt32Array = _equivalent_rotations.get(
|
|
_constraint_key(placement),
|
|
PackedInt32Array([placement.quarter_turns]),
|
|
)
|
|
if rotations.size() <= 1:
|
|
continue
|
|
var selected_turns := _least_repeated_equivalent_rotation(
|
|
index,
|
|
placement.definition,
|
|
rotations,
|
|
)
|
|
var resolved := _authored_variant(
|
|
placement.definition,
|
|
selected_turns,
|
|
)
|
|
if resolved != null:
|
|
_placements[index] = resolved
|
|
|
|
|
|
func _least_repeated_equivalent_rotation(
|
|
index: int,
|
|
definition: TerrainChunkDefinition,
|
|
rotations: PackedInt32Array,
|
|
) -> int:
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var best_score := 3
|
|
var best_rotations := PackedInt32Array()
|
|
for turns: int in rotations:
|
|
var score := 0
|
|
for neighbor_coordinate: Vector2i in [
|
|
coordinate + Vector2i.LEFT,
|
|
coordinate + Vector2i.UP,
|
|
]:
|
|
if not _coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if (
|
|
neighbor != null
|
|
and neighbor.definition == definition
|
|
and neighbor.quarter_turns == turns
|
|
):
|
|
score += 1
|
|
if score < best_score:
|
|
best_score = score
|
|
best_rotations = PackedInt32Array([turns])
|
|
elif score == best_score:
|
|
best_rotations.append(turns)
|
|
return best_rotations[_random.randi_range(0, best_rotations.size() - 1)]
|
|
|
|
|
|
func _authored_variant(
|
|
definition: TerrainChunkDefinition,
|
|
quarter_turns: int,
|
|
) -> TerrainChunkVariant:
|
|
for variant: TerrainChunkVariant in _variants:
|
|
if (
|
|
variant.definition == definition
|
|
and variant.quarter_turns == quarter_turns
|
|
):
|
|
return variant
|
|
return null
|
|
|
|
|
|
func _build_solution_root() -> Node3D:
|
|
var solution_root := Node3D.new()
|
|
solution_root.name = "GeneratedChunks"
|
|
var half_grid := Vector2(
|
|
float(grid_size.x - 1) * 0.5,
|
|
float(grid_size.y - 1) * 0.5,
|
|
)
|
|
for index: int in _placements.size():
|
|
var variant := _placements[index]
|
|
if variant == null:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var chunk_root := _instantiate_chunk_root(variant.definition)
|
|
if chunk_root == null:
|
|
push_error("%s does not instantiate as Node3D." % variant.stable_key())
|
|
solution_root.free()
|
|
return null
|
|
chunk_root.name = "%s_%d_%d" % [
|
|
variant.stable_key().replace("@", "r"),
|
|
coordinate.x,
|
|
coordinate.y,
|
|
]
|
|
chunk_root.position = Vector3(
|
|
(float(coordinate.x) - half_grid.x) * catalog.chunk_size,
|
|
0.0,
|
|
(float(coordinate.y) - half_grid.y) * catalog.chunk_size,
|
|
)
|
|
chunk_root.rotation.y = variant.rotation_radians()
|
|
chunk_root.set_meta(&"terrain_chunk_coordinate", coordinate)
|
|
chunk_root.set_meta(
|
|
&"terrain_chunk_id",
|
|
variant.definition.stable_id,
|
|
)
|
|
solution_root.add_child(chunk_root)
|
|
if build_collision:
|
|
_add_collision(chunk_root, variant.definition)
|
|
if show_chunk_labels:
|
|
_add_chunk_label(chunk_root, variant)
|
|
if not _add_stacked_elevated_chunks(solution_root):
|
|
solution_root.free()
|
|
return null
|
|
if build_collision and collision_batch_size > 1:
|
|
_batch_generated_terrain_collision(solution_root)
|
|
return solution_root
|
|
|
|
|
|
func _batch_generated_terrain_collision(solution_root: Node3D) -> void:
|
|
var shapes_by_batch: Dictionary[Vector2i, Array] = {}
|
|
var host_by_batch: Dictionary[Vector2i, Node3D] = {}
|
|
var bodies_to_remove: Array[StaticBody3D] = []
|
|
for chunk_node: Node in solution_root.get_children():
|
|
var chunk_root := chunk_node as Node3D
|
|
if chunk_root == null:
|
|
continue
|
|
var coordinate: Vector2i = chunk_root.get_meta(
|
|
&"terrain_chunk_coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
var batch_coordinate := Vector2i(
|
|
floori(float(coordinate.x) / float(collision_batch_size)),
|
|
floori(float(coordinate.y) / float(collision_batch_size)),
|
|
)
|
|
if not host_by_batch.has(batch_coordinate):
|
|
host_by_batch[batch_coordinate] = chunk_root
|
|
var batch_shapes: Array = shapes_by_batch.get(
|
|
batch_coordinate,
|
|
[],
|
|
)
|
|
for value: Node in chunk_root.find_children(
|
|
"TerrainShape",
|
|
"CollisionShape3D",
|
|
true,
|
|
false,
|
|
):
|
|
var collision_shape := value as CollisionShape3D
|
|
if collision_shape == null:
|
|
continue
|
|
var collision_body := collision_shape.get_parent() as StaticBody3D
|
|
if collision_shape.shape == null or collision_body == null:
|
|
continue
|
|
var shape_to_solution := _transform_to_ancestor(
|
|
collision_shape,
|
|
solution_root,
|
|
)
|
|
batch_shapes.append({
|
|
"shape": collision_shape.shape,
|
|
"transform": shape_to_solution,
|
|
})
|
|
if collision_body not in bodies_to_remove:
|
|
bodies_to_remove.append(collision_body)
|
|
shapes_by_batch[batch_coordinate] = batch_shapes
|
|
while not bodies_to_remove.is_empty():
|
|
var collision_body: StaticBody3D = bodies_to_remove.pop_back()
|
|
collision_body.free()
|
|
for batch_coordinate: Vector2i in shapes_by_batch:
|
|
var batch_shapes: Array = shapes_by_batch[batch_coordinate]
|
|
if batch_shapes.is_empty():
|
|
continue
|
|
var host_root: Node3D = host_by_batch.get(batch_coordinate)
|
|
if host_root == null:
|
|
continue
|
|
var body := StaticBody3D.new()
|
|
body.name = "TerrainCollisionBatch_%d_%d" % [
|
|
batch_coordinate.x,
|
|
batch_coordinate.y,
|
|
]
|
|
body.collision_layer = 1
|
|
body.collision_mask = 0
|
|
body.set_meta(&"terrain_collision_batch", batch_coordinate)
|
|
host_root.add_child(body)
|
|
var solution_to_body: Transform3D = host_root.transform.affine_inverse()
|
|
for shape_index: int in batch_shapes.size():
|
|
var record: Dictionary = batch_shapes[shape_index]
|
|
var collision := CollisionShape3D.new()
|
|
collision.name = "TerrainShape_%d" % shape_index
|
|
collision.shape = record.get("shape") as Shape3D
|
|
collision.transform = (
|
|
solution_to_body
|
|
* (record.get("transform", Transform3D.IDENTITY) as Transform3D)
|
|
)
|
|
body.add_child(collision)
|
|
|
|
|
|
static func _transform_to_ancestor(
|
|
node: Node3D,
|
|
ancestor: Node3D,
|
|
) -> Transform3D:
|
|
var result := Transform3D.IDENTITY
|
|
var current: Node3D = node
|
|
while current != ancestor:
|
|
result = current.transform * result
|
|
current = current.get_parent() as Node3D
|
|
if current == null:
|
|
return Transform3D.IDENTITY
|
|
return result
|
|
|
|
|
|
func _add_stacked_elevated_chunks(solution_root: Node3D) -> bool:
|
|
for index: int in _stacked_elevated_placements.size():
|
|
var record := _stacked_elevated_placements[index]
|
|
var variant := record.get("variant") as TerrainChunkVariant
|
|
var support_coordinate: Vector2i = record.get(
|
|
"support_coordinate",
|
|
Vector2i(-1, -1),
|
|
)
|
|
var support_root := _find_chunk_root_for_coordinate(
|
|
solution_root,
|
|
support_coordinate,
|
|
)
|
|
if variant == null or support_root == null:
|
|
push_error("Stacked cliff feature lost its supporting terrain cell.")
|
|
return false
|
|
var stacked_root := variant.definition.packed_scene.instantiate() as Node3D
|
|
if stacked_root == null:
|
|
push_error(
|
|
"%s does not instantiate as a stacked Node3D."
|
|
% variant.stable_key()
|
|
)
|
|
return false
|
|
stacked_root.name = "Stacked_%s_%d" % [
|
|
variant.stable_key().replace("@", "r"),
|
|
index,
|
|
]
|
|
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
|
var vertical_offset := float(record.get(
|
|
"vertical_offset",
|
|
elevated_cliff_level_height,
|
|
))
|
|
stacked_root.position = Vector3(
|
|
offset.x * catalog.chunk_size,
|
|
vertical_offset,
|
|
offset.y * catalog.chunk_size,
|
|
)
|
|
stacked_root.rotation.y = variant.rotation_radians()
|
|
stacked_root.set_meta(&"terrain_stacked_elevation", true)
|
|
stacked_root.set_meta(&"terrain_stacked_elevation_height", vertical_offset)
|
|
stacked_root.set_meta(
|
|
&"terrain_chunk_id",
|
|
variant.definition.stable_id,
|
|
)
|
|
stacked_root.set_meta(
|
|
&"terrain_chunk_coordinate",
|
|
support_coordinate,
|
|
)
|
|
_apply_projected_surface_materials(stacked_root)
|
|
support_root.add_child(stacked_root)
|
|
if build_collision:
|
|
_add_collision(stacked_root, variant.definition)
|
|
if show_chunk_labels:
|
|
_add_chunk_label(stacked_root, variant)
|
|
return true
|
|
|
|
|
|
func _find_chunk_root_for_coordinate(
|
|
solution_root: Node3D,
|
|
coordinate: Vector2i,
|
|
) -> Node3D:
|
|
for child: Node in solution_root.get_children():
|
|
if child.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)) == (
|
|
coordinate
|
|
):
|
|
return child as Node3D
|
|
return null
|
|
|
|
|
|
func _instantiate_chunk_root(
|
|
definition: TerrainChunkDefinition,
|
|
) -> Node3D:
|
|
var terrain_visual := definition.packed_scene.instantiate() as Node3D
|
|
if terrain_visual == null:
|
|
return null
|
|
_apply_projected_surface_materials(terrain_visual)
|
|
if definition.base_layer_scene == null:
|
|
return terrain_visual
|
|
var base_layer_visual := definition.base_layer_scene.instantiate() as Node3D
|
|
if base_layer_visual == null:
|
|
terrain_visual.free()
|
|
return null
|
|
_apply_projected_surface_materials(base_layer_visual)
|
|
var layered_root := Node3D.new()
|
|
layered_root.name = "LayeredTerrainChunk"
|
|
base_layer_visual.name = "TerrainBaseLayer"
|
|
base_layer_visual.rotation.y = (
|
|
float(definition.base_layer_quarter_turns) * PI * 0.5
|
|
)
|
|
terrain_visual.name = "TerrainOverlay"
|
|
layered_root.add_child(base_layer_visual)
|
|
layered_root.add_child(terrain_visual)
|
|
return layered_root
|
|
|
|
|
|
func _apply_projected_surface_materials(root: Node) -> void:
|
|
var mesh_instance := root as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.mesh != null:
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
var source_material := mesh_instance.get_active_material(surface_index)
|
|
var projected_material := _projected_material_for(source_material)
|
|
if projected_material != null:
|
|
mesh_instance.set_surface_override_material(
|
|
surface_index,
|
|
projected_material,
|
|
)
|
|
for child: Node in root.get_children():
|
|
_apply_projected_surface_materials(child)
|
|
|
|
|
|
static func _projected_material_for(source_material: Material) -> Material:
|
|
if source_material == null:
|
|
return null
|
|
var material_name := source_material.resource_name
|
|
if material_name == "grass_lite" or material_name.begins_with(
|
|
"grass_lite."
|
|
):
|
|
return PROJECTED_GRASS_MATERIAL
|
|
if material_name == "sand" or material_name.begins_with("sand."):
|
|
return PROJECTED_SAND_MATERIAL
|
|
if (
|
|
material_name == "dirt"
|
|
or material_name.begins_with("dirt.")
|
|
or material_name == "water_reference"
|
|
or material_name.begins_with("water_reference.")
|
|
):
|
|
return PROJECTED_DIRT_MATERIAL
|
|
return null
|
|
|
|
|
|
func _replace_generated_chunks(solution_root: Node3D) -> void:
|
|
_clear_generated_chunks()
|
|
_generated_chunks = solution_root
|
|
add_child(_generated_chunks)
|
|
|
|
|
|
func _add_collision(
|
|
chunk_root: Node3D,
|
|
definition: TerrainChunkDefinition,
|
|
) -> void:
|
|
var primary_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
chunk_root,
|
|
definition.primary_mesh_name,
|
|
)
|
|
_add_collision_to_mesh(
|
|
primary_mesh,
|
|
definition.stable_id,
|
|
"TerrainCollision",
|
|
)
|
|
if definition.base_layer_scene == null:
|
|
return
|
|
var base_layer_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
chunk_root,
|
|
definition.base_layer_mesh_name,
|
|
)
|
|
_add_collision_to_mesh(
|
|
base_layer_mesh,
|
|
definition.stable_id,
|
|
"TerrainBaseLayerCollision",
|
|
)
|
|
|
|
|
|
func _add_collision_to_mesh(
|
|
primary_mesh: MeshInstance3D,
|
|
stable_id: StringName,
|
|
collision_name: String,
|
|
) -> void:
|
|
if primary_mesh == null or primary_mesh.mesh == null:
|
|
return
|
|
var terrain_shape := primary_mesh.mesh.create_trimesh_shape()
|
|
if terrain_shape == null or terrain_shape.get_faces().is_empty():
|
|
push_warning("%s produced no terrain collision." % stable_id)
|
|
return
|
|
var concave_shape := terrain_shape as ConcavePolygonShape3D
|
|
if concave_shape != null:
|
|
# Authored terrain can expose cliff walls and transitional surfaces with
|
|
# mixed winding. Treat both sides as solid so a visible walkable surface
|
|
# can never become an invisible collision gap.
|
|
concave_shape.backface_collision = true
|
|
var collision_body := StaticBody3D.new()
|
|
collision_body.name = collision_name
|
|
collision_body.collision_layer = 1
|
|
collision_body.collision_mask = 0
|
|
primary_mesh.add_child(collision_body)
|
|
var collision_shape := CollisionShape3D.new()
|
|
collision_shape.name = "TerrainShape"
|
|
collision_shape.shape = terrain_shape
|
|
collision_body.add_child(collision_shape)
|
|
|
|
|
|
func _add_chunk_label(
|
|
chunk_root: Node3D,
|
|
variant: TerrainChunkVariant,
|
|
) -> void:
|
|
var label := Label3D.new()
|
|
label.name = "ChunkLabel"
|
|
label.text = "%s r%d" % [
|
|
variant.definition.stable_id,
|
|
variant.quarter_turns,
|
|
]
|
|
label.position = Vector3(0.0, 1.25, 0.0)
|
|
label.font_size = 24
|
|
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
|
label.no_depth_test = true
|
|
chunk_root.add_child(label)
|
|
|
|
|
|
func _clear_generated_chunks() -> void:
|
|
if is_instance_valid(_generated_chunks):
|
|
remove_child(_generated_chunks)
|
|
_generated_chunks.free()
|
|
_generated_chunks = null
|
|
|
|
|
|
func _build_summary() -> Dictionary:
|
|
var counts: Dictionary[StringName, int] = {}
|
|
var variant_counts: Dictionary[StringName, int] = {}
|
|
for variant: TerrainChunkVariant in _variants:
|
|
var variant_id := variant.definition.stable_id
|
|
variant_counts[variant_id] = variant_counts.get(variant_id, 0) + 1
|
|
for variant: TerrainChunkVariant in _placements:
|
|
if variant == null:
|
|
continue
|
|
var stable_id := variant.definition.stable_id
|
|
counts[stable_id] = counts.get(stable_id, 0) + 1
|
|
return {
|
|
"seed": generation_seed,
|
|
"grid_size": grid_size,
|
|
"chunk_count": _placements.size(),
|
|
"variant_count": _variants.size(),
|
|
"solver_variant_count": _solver_variants.size(),
|
|
"compatible_edge_pairs": _compatible_edge_pair_count(),
|
|
"backtracks": _backtrack_count,
|
|
"adjacent_repeat_edges": _count_adjacent_repeat_edges(),
|
|
"counts": counts,
|
|
"variant_counts": variant_counts,
|
|
"placements": placement_keys(),
|
|
"stacked_elevated_placements": stacked_elevated_placement_keys(),
|
|
"layout_fingerprint": placement_fingerprint(),
|
|
}
|
|
|
|
|
|
func _compatible_edge_pair_count() -> int:
|
|
var count := 0
|
|
for compatible: int in _edge_compatibility:
|
|
if compatible != 0:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _count_adjacent_repeat_edges() -> int:
|
|
var count := 0
|
|
for index: int in _placements.size():
|
|
var current := _placements[index]
|
|
if current == null:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
if coordinate.x > 0:
|
|
var west := _placements[index - 1]
|
|
if west != null and west.definition == current.definition:
|
|
count += 1
|
|
if coordinate.y > 0:
|
|
var north := _placements[index - grid_size.x]
|
|
if north != null and north.definition == current.definition:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func placement_keys() -> PackedStringArray:
|
|
var result := PackedStringArray()
|
|
for variant: TerrainChunkVariant in _placements:
|
|
result.append(variant.stable_key() if variant != null else "empty")
|
|
return result
|
|
|
|
|
|
func placement_fingerprint() -> String:
|
|
var fingerprint_input := PackedStringArray([
|
|
"grid:%dx%d" % [grid_size.x, grid_size.y],
|
|
"chunk_size:%.6f" % (catalog.chunk_size if catalog != null else 0.0),
|
|
])
|
|
fingerprint_input.append_array(placement_keys())
|
|
fingerprint_input.append_array(stacked_elevated_placement_keys())
|
|
return "\n".join(fingerprint_input).sha256_text()
|
|
|
|
|
|
func stacked_elevated_placement_keys() -> PackedStringArray:
|
|
var result := PackedStringArray()
|
|
for record: Dictionary in _stacked_elevated_placements:
|
|
var variant := record.get("variant") as TerrainChunkVariant
|
|
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
|
var vertical_offset := float(record.get(
|
|
"vertical_offset",
|
|
elevated_cliff_level_height,
|
|
))
|
|
if variant == null:
|
|
continue
|
|
result.append(
|
|
"%s@%.2f,%.2f,+%.2fm"
|
|
% [
|
|
variant.stable_key(),
|
|
offset.x,
|
|
offset.y,
|
|
vertical_offset,
|
|
]
|
|
)
|
|
return result
|
|
|
|
|
|
func placement_records() -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
for index: int in _placements.size():
|
|
var variant: TerrainChunkVariant = _placements[index]
|
|
if variant == null:
|
|
continue
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
result.append({
|
|
"coordinate": coordinate,
|
|
"position": chunk_position(coordinate),
|
|
"rotation_quarters": variant.quarter_turns,
|
|
"stable_id": variant.definition.stable_id,
|
|
"tags": variant.definition.tags,
|
|
"ocean_facing_edges": variant.rotated_edge_mask(
|
|
variant.definition.ocean_facing_edges
|
|
),
|
|
"water_surface_size": variant.definition.water_surface_size,
|
|
"water_surface_offset": variant.definition.water_surface_offset,
|
|
"water_surface_polygon": variant.definition.water_surface_polygon,
|
|
})
|
|
return result
|
|
|
|
|
|
func chunk_position(coordinate: Vector2i) -> Vector3:
|
|
if catalog == null:
|
|
return Vector3.ZERO
|
|
var half_grid := Vector2(
|
|
float(grid_size.x - 1) * 0.5,
|
|
float(grid_size.y - 1) * 0.5,
|
|
)
|
|
return Vector3(
|
|
(float(coordinate.x) - half_grid.x) * catalog.chunk_size,
|
|
0.0,
|
|
(float(coordinate.y) - half_grid.y) * catalog.chunk_size,
|
|
)
|
|
|
|
|
|
func get_generated_chunks_root() -> Node3D:
|
|
return _generated_chunks
|
|
|
|
|
|
func get_primary_terrain_meshes() -> Array[MeshInstance3D]:
|
|
var result: Array[MeshInstance3D] = []
|
|
if not is_instance_valid(_generated_chunks):
|
|
return result
|
|
var chunk_count := mini(
|
|
_generated_chunks.get_child_count(),
|
|
_placements.size(),
|
|
)
|
|
for index: int in chunk_count:
|
|
var placement := _placements[index]
|
|
var chunk_root := _generated_chunks.get_child(index)
|
|
if placement == null or chunk_root == null:
|
|
continue
|
|
var primary_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
chunk_root,
|
|
placement.definition.primary_mesh_name,
|
|
)
|
|
if primary_mesh != null and primary_mesh.mesh != null:
|
|
result.append(primary_mesh)
|
|
for child: Node in chunk_root.get_children():
|
|
if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
|
|
continue
|
|
var definition := catalog.definition_for_id(
|
|
StringName(child.get_meta(&"terrain_chunk_id", &""))
|
|
)
|
|
if definition == null:
|
|
continue
|
|
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
child,
|
|
definition.primary_mesh_name,
|
|
)
|
|
if stacked_mesh != null and stacked_mesh.mesh != null:
|
|
result.append(stacked_mesh)
|
|
return result
|