forked from woofmeow/straywild
1452 lines
43 KiB
GDScript
1452 lines
43 KiB
GDScript
class_name TerrainChunkGenerator
|
|
extends Node3D
|
|
|
|
signal generation_completed(summary: Dictionary)
|
|
|
|
const CONSTRAINT_PROFILE_QUANTIZATION := 0.001
|
|
const CANDIDATE_WEIGHT_SCALE := 10000.0
|
|
|
|
@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 var show_chunk_labels := false
|
|
@export var force_center_chunk_id: StringName = &"chunk_0000"
|
|
@export var required_chunk_ids := PackedStringArray()
|
|
@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 _equivalent_rotations: Dictionary[String, PackedInt32Array] = {}
|
|
var _solver_variant_indices: Dictionary = {}
|
|
var _edge_compatibility := PackedByteArray()
|
|
var _placements: Array[TerrainChunkVariant] = []
|
|
var _random := RandomNumberGenerator.new()
|
|
var _generated_chunks: Node3D
|
|
var _backtrack_count := 0
|
|
|
|
|
|
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
|
|
_placements.clear()
|
|
_placements.resize(grid_size.x * grid_size.y)
|
|
if not _solve_cell(0):
|
|
_placements.assign(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()
|
|
var solution_root := _build_solution_root()
|
|
if solution_root == null:
|
|
_placements.assign(previous_placements)
|
|
return false
|
|
_replace_generated_chunks(solution_root)
|
|
var summary := _build_summary()
|
|
generation_completed.emit(summary)
|
|
return true
|
|
|
|
|
|
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)
|
|
_placements.assign(resolved)
|
|
_backtrack_count = 0
|
|
var solution_root := _build_solution_root()
|
|
if solution_root == null:
|
|
_placements.assign(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 has an ocean-facing edge directed into the terrain grid."
|
|
% 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 and %d have incompatible east/west edges." % [
|
|
index,
|
|
index + 1,
|
|
]
|
|
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 and %d have incompatible south/north edges." % [
|
|
index,
|
|
index + grid_size.x,
|
|
]
|
|
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()
|
|
_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)
|
|
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
|
|
for index: int in _solver_variants.size():
|
|
_solver_variant_indices[_solver_variants[index]] = index
|
|
_build_edge_compatibility_cache()
|
|
return true
|
|
|
|
|
|
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)
|
|
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,
|
|
)
|
|
_edge_compatibility[cache_index] = int(
|
|
_calculate_edge_compatibility(
|
|
first,
|
|
first_edge,
|
|
second,
|
|
second_edge,
|
|
)
|
|
)
|
|
|
|
|
|
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] = {}
|
|
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
|
|
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 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
|
|
if not _requirements_can_still_be_satisfied():
|
|
return false
|
|
var next_cell := _select_next_cell()
|
|
var index := int(next_cell.get("index", -1))
|
|
if index < 0:
|
|
return false
|
|
var candidates := next_cell.get(
|
|
"candidates",
|
|
[],
|
|
) as Array[TerrainChunkVariant]
|
|
for candidate: TerrainChunkVariant in _weighted_candidate_order(
|
|
candidates,
|
|
index,
|
|
):
|
|
_placements[index] = candidate
|
|
if _solve_cell(placed_count + 1):
|
|
return true
|
|
_placements[index] = null
|
|
_backtrack_count += 1
|
|
if _backtrack_count >= maximum_backtracks:
|
|
break
|
|
return false
|
|
|
|
|
|
func _select_next_cell() -> Dictionary:
|
|
# 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 _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]:
|
|
var result: Array[TerrainChunkVariant] = []
|
|
var missing_required := _missing_required_ids()
|
|
var remaining_cells := _unfilled_cell_count()
|
|
var must_place_missing_required := missing_required.size() >= remaining_cells
|
|
for candidate: TerrainChunkVariant in _solver_variants:
|
|
if (
|
|
must_place_missing_required
|
|
and not missing_required.has(candidate.definition.stable_id)
|
|
):
|
|
continue
|
|
if not _candidate_can_occupy_cell(candidate, index):
|
|
continue
|
|
result.append(candidate)
|
|
return result
|
|
|
|
|
|
func _candidate_can_occupy_cell(
|
|
candidate: TerrainChunkVariant,
|
|
index: int,
|
|
) -> bool:
|
|
if not _definition_has_capacity(candidate.definition):
|
|
return false
|
|
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
|
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
|
var center_index := center.y * grid_size.x + center.x
|
|
if (
|
|
coordinate == center
|
|
and force_center_chunk_id != &""
|
|
and candidate.definition.stable_id != force_center_chunk_id
|
|
):
|
|
return false
|
|
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 (
|
|
_variant_respects_ocean_boundary(candidate, coordinate)
|
|
and _candidate_matches_placed_neighbors(candidate, coordinate)
|
|
and _candidate_neighbor_requirement_can_still_be_met(
|
|
candidate.definition,
|
|
coordinate,
|
|
)
|
|
and _candidate_preserves_placed_neighbor_requirements(
|
|
candidate.definition,
|
|
coordinate,
|
|
)
|
|
)
|
|
|
|
|
|
func _variant_respects_ocean_boundary(
|
|
variant: TerrainChunkVariant,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
var ocean_edges := variant.rotated_edge_mask(
|
|
variant.definition.ocean_facing_edges
|
|
)
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
if (ocean_edges & (1 << edge_value)) == 0:
|
|
continue
|
|
var ocean_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if _coordinate_is_inside_grid(ocean_coordinate):
|
|
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 _candidate_neighbor_requirement_can_still_be_met(
|
|
definition: TerrainChunkDefinition,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
if definition.minimum_required_neighbors <= 0:
|
|
return true
|
|
var possible_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 := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
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(
|
|
definition: TerrainChunkDefinition,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
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 := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if neighbor == null or neighbor.definition.minimum_required_neighbors <= 0:
|
|
continue
|
|
if not _placed_requirement_can_still_be_met_with_candidate(
|
|
neighbor.definition,
|
|
neighbor_coordinate,
|
|
definition,
|
|
coordinate,
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _placed_requirement_can_still_be_met_with_candidate(
|
|
definition: TerrainChunkDefinition,
|
|
coordinate: Vector2i,
|
|
candidate_definition: TerrainChunkDefinition,
|
|
candidate_coordinate: Vector2i,
|
|
) -> bool:
|
|
var possible_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
|
|
if neighbor_coordinate == candidate_coordinate:
|
|
if _definition_matches_required_neighbor_tags(
|
|
definition,
|
|
candidate_definition,
|
|
):
|
|
possible_neighbors += 1
|
|
continue
|
|
var neighbor := _placements[
|
|
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
|
|
]
|
|
if (
|
|
neighbor == null
|
|
or _definition_matches_required_neighbor_tags(
|
|
definition,
|
|
neighbor.definition,
|
|
)
|
|
):
|
|
possible_neighbors += 1
|
|
return possible_neighbors >= definition.minimum_required_neighbors
|
|
|
|
|
|
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:
|
|
var count := 0
|
|
for placement: TerrainChunkVariant in _placements:
|
|
if placement != null and placement.definition == definition:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
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:
|
|
if not first.profile(first_edge).matches(
|
|
second.profile(second_edge),
|
|
edge_match_tolerance,
|
|
):
|
|
return false
|
|
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
|
|
if first_has_water or second_has_water:
|
|
return (
|
|
(first_outlet and second_inlet)
|
|
or (first_inlet and second_outlet)
|
|
)
|
|
return (
|
|
first.allows_non_water_neighbor_on_edge(second, first_edge)
|
|
and second.allows_non_water_neighbor_on_edge(first, second_edge)
|
|
)
|
|
|
|
|
|
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:
|
|
for index: int in _placements.size():
|
|
if _placements[index] != null:
|
|
continue
|
|
for candidate: TerrainChunkVariant in _solver_variants:
|
|
if (
|
|
candidate.definition == definition
|
|
and _candidate_can_occupy_cell(candidate, index)
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _unfilled_cell_count() -> int:
|
|
var count := 0
|
|
for placement: TerrainChunkVariant in _placements:
|
|
if placement == null:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _missing_required_ids() -> Array[StringName]:
|
|
var found: Dictionary[StringName, bool] = {}
|
|
for placement: TerrainChunkVariant in _placements:
|
|
if placement != null:
|
|
found[placement.definition.stable_id] = true
|
|
var result: Array[StringName] = []
|
|
for required_id_value: String in required_chunk_ids:
|
|
var required_id := StringName(required_id_value)
|
|
if not found.has(required_id) and not result.has(required_id):
|
|
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 _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
|
|
for placement: TerrainChunkVariant in _placements:
|
|
if (
|
|
placement != null
|
|
and placement.definition.stable_id == stable_id
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
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 := variant.definition.packed_scene.instantiate() as Node3D
|
|
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)
|
|
return solution_root
|
|
|
|
|
|
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,
|
|
)
|
|
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." % definition.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 = "TerrainCollision"
|
|
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(),
|
|
"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())
|
|
return "\n".join(fingerprint_input).sha256_text()
|
|
|
|
|
|
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,
|
|
})
|
|
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)
|
|
return result
|