Expand generated terrain and coastal elevations
This commit is contained in:
parent
6d0e996583
commit
339812763d
28 changed files with 906 additions and 171 deletions
|
|
@ -13,7 +13,7 @@ const GeneratedRiverPool: FishPool = preload(
|
|||
"res://fish/pools/generated_river_pool.tres"
|
||||
)
|
||||
const FIRST_SEED := 13001
|
||||
const SECOND_SEED := 13004
|
||||
const SECOND_SEED := 13002
|
||||
const EXPECTED_PROP_IDS: Array[StringName] = [
|
||||
&"prop_bridge",
|
||||
&"prop_dock",
|
||||
|
|
@ -110,23 +110,47 @@ func _validate_generated_region(
|
|||
assert(placements[center_index].begins_with("chunk_spawn@"))
|
||||
assert(_placement_count(placements, "chunk_spawn") == 1)
|
||||
assert(_placement_count(placements, "chunk_0001") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0002") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0003") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0016") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0004") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0005") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0006") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0007") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0008") >= 1)
|
||||
var beach_count := _placement_count(placements, "chunk_0002")
|
||||
var grass_coast_count := _placement_count(placements, "chunk_0005")
|
||||
var grass_corner_count := _placement_count(placements, "chunk_0007")
|
||||
var beach_corner_count := _placement_count(placements, "chunk_0008")
|
||||
assert((beach_count > 0) != (grass_coast_count > 0))
|
||||
if beach_count > 0:
|
||||
assert(beach_count == 48 and grass_coast_count == 0)
|
||||
assert(beach_corner_count == 4 and grass_corner_count == 0)
|
||||
assert(_placement_count(placements, "chunk_0009") == 9)
|
||||
assert(_placement_count(placements, "chunk_0010") == 4)
|
||||
assert(
|
||||
_placement_count(placements, "chunk_0011")
|
||||
+ _placement_count(placements, "chunk_0012")
|
||||
== 12
|
||||
)
|
||||
assert(_placement_count(placements, "chunk_0012") == 1)
|
||||
for stable_id: String in [
|
||||
"chunk_0014",
|
||||
"chunk_0015",
|
||||
"chunk_0017",
|
||||
"chunk_0018",
|
||||
]:
|
||||
assert(_placement_count(placements, stable_id) == 0)
|
||||
else:
|
||||
assert(grass_coast_count == 44 and beach_count == 0)
|
||||
assert(grass_corner_count == 3 and beach_corner_count == 0)
|
||||
assert(_placement_count(placements, "chunk_0009") == 10)
|
||||
assert(_placement_count(placements, "chunk_0010") == 5)
|
||||
assert(
|
||||
_placement_count(placements, "chunk_0011")
|
||||
+ _placement_count(placements, "chunk_0012")
|
||||
== 14
|
||||
)
|
||||
assert(_placement_count(placements, "chunk_0012") == 2)
|
||||
assert(_placement_count(placements, "chunk_0014") == 2)
|
||||
assert(_placement_count(placements, "chunk_0015") == 1)
|
||||
assert(_placement_count(placements, "chunk_0017") == 1)
|
||||
assert(_placement_count(placements, "chunk_0018") == 1)
|
||||
assert(_placement_count(placements, "chunk_0013") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0009") == 1)
|
||||
assert(_placement_count(placements, "chunk_0010") == 4)
|
||||
assert(
|
||||
_placement_count(placements, "chunk_0011")
|
||||
+ _placement_count(placements, "chunk_0012")
|
||||
== 4
|
||||
)
|
||||
assert(_placement_count(placements, "chunk_0012") <= 1)
|
||||
assert(
|
||||
generator.get_generated_chunks_root().get_child_count()
|
||||
== expected_chunk_count
|
||||
|
|
@ -164,6 +188,7 @@ func _validate_generated_region(
|
|||
_validate_ocean_facing_record(record, generator.grid_size)
|
||||
if "fresh_water" in tags:
|
||||
fresh_placement_count += 1
|
||||
_validate_complete_coastline(generator)
|
||||
assert(fresh_root.get_child_count() == fresh_placement_count)
|
||||
for child: Node in fresh_root.get_children():
|
||||
var fresh := child as WaterBodyAuthoring
|
||||
|
|
@ -191,6 +216,7 @@ func _validate_generated_region(
|
|||
assert(decorations != null and decorations.get_child_count() > 0)
|
||||
assert(anchors != null)
|
||||
assert(anchors.get_spawn_positions().size() > 0)
|
||||
_validate_tree_gatherable_anchors(region, decorations, anchors)
|
||||
var palm_clusters: Dictionary[int, Array] = {}
|
||||
var tree_scales: Array[float] = []
|
||||
var tree_yaws: Dictionary[float, bool] = {}
|
||||
|
|
@ -295,7 +321,7 @@ func _validate_generated_region(
|
|||
)
|
||||
assert(not palm_clusters.is_empty())
|
||||
for members: Array in palm_clusters.values():
|
||||
assert(members.size() >= 2 and members.size() <= 3)
|
||||
assert(members.size() >= 1 and members.size() <= 3)
|
||||
for first_index: int in members.size():
|
||||
for second_index: int in range(first_index + 1, members.size()):
|
||||
var first := members[first_index] as Node3D
|
||||
|
|
@ -314,14 +340,14 @@ func _validate_generated_region(
|
|||
region,
|
||||
&"biome_forest",
|
||||
&"grass_tree",
|
||||
) >= 2
|
||||
) >= 4
|
||||
)
|
||||
assert(
|
||||
_decoration_biome_group_count(
|
||||
region,
|
||||
&"biome_pine_forest",
|
||||
&"grass_tree",
|
||||
) >= 2
|
||||
) >= 4
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -329,7 +355,15 @@ func _validate_elevated_cliff_feature(
|
|||
generator: TerrainChunkGenerator,
|
||||
) -> void:
|
||||
var elevated_coordinates: Dictionary[Vector2i, StringName] = {}
|
||||
var top_coordinate := Vector2i(-1, -1)
|
||||
var top_coordinates: Array[Vector2i] = []
|
||||
var ramp_records: Array[Dictionary] = []
|
||||
var coastal_ids: Array[StringName] = [
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
]
|
||||
var has_coastal_feature := false
|
||||
for record: Dictionary in generator.placement_records():
|
||||
var stable_id: StringName = record.get("stable_id", &"")
|
||||
if stable_id not in [
|
||||
|
|
@ -337,23 +371,100 @@ func _validate_elevated_cliff_feature(
|
|||
&"chunk_0010",
|
||||
&"chunk_0011",
|
||||
&"chunk_0012",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
]:
|
||||
continue
|
||||
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||
assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1)
|
||||
assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1)
|
||||
if stable_id in coastal_ids:
|
||||
has_coastal_feature = true
|
||||
assert(
|
||||
coordinate.x in [0, generator.grid_size.x - 1]
|
||||
or coordinate.y in [0, generator.grid_size.y - 1]
|
||||
)
|
||||
else:
|
||||
assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1)
|
||||
assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1)
|
||||
elevated_coordinates[coordinate] = stable_id
|
||||
if stable_id == &"chunk_0009":
|
||||
top_coordinate = coordinate
|
||||
assert(elevated_coordinates.size() == 9)
|
||||
assert(top_coordinate != Vector2i(-1, -1))
|
||||
for row_offset: int in range(-1, 2):
|
||||
for column_offset: int in range(-1, 2):
|
||||
top_coordinates.append(coordinate)
|
||||
elif stable_id == &"chunk_0012":
|
||||
ramp_records.append(record)
|
||||
assert(elevated_coordinates.size() == (34 if has_coastal_feature else 25))
|
||||
assert(top_coordinates.size() == (10 if has_coastal_feature else 9))
|
||||
assert(ramp_records.size() == (2 if has_coastal_feature else 1))
|
||||
var feature_center := Vector2i(-1, -1)
|
||||
for candidate: Vector2i in top_coordinates:
|
||||
var neighboring_top_count := 0
|
||||
for row_offset: int in range(-1, 2):
|
||||
for column_offset: int in range(-1, 2):
|
||||
neighboring_top_count += int(
|
||||
elevated_coordinates.get(
|
||||
candidate + Vector2i(column_offset, row_offset),
|
||||
&"",
|
||||
) == &"chunk_0009"
|
||||
)
|
||||
if neighboring_top_count == 9:
|
||||
feature_center = candidate
|
||||
break
|
||||
assert(feature_center != Vector2i(-1, -1))
|
||||
for row_offset: int in range(-2, 3):
|
||||
for column_offset: int in range(-2, 3):
|
||||
assert(
|
||||
elevated_coordinates.has(
|
||||
top_coordinate + Vector2i(column_offset, row_offset)
|
||||
feature_center + Vector2i(column_offset, row_offset)
|
||||
)
|
||||
)
|
||||
if has_coastal_feature:
|
||||
var secondary_top := Vector2i(-1, -1)
|
||||
for coordinate: Vector2i in top_coordinates:
|
||||
if (
|
||||
absi(coordinate.x - feature_center.x) > 1
|
||||
or absi(coordinate.y - feature_center.y) > 1
|
||||
):
|
||||
secondary_top = coordinate
|
||||
break
|
||||
assert(secondary_top != Vector2i(-1, -1))
|
||||
for row_offset: int in range(-1, 2):
|
||||
for column_offset: int in range(-1, 2):
|
||||
assert(
|
||||
elevated_coordinates.has(
|
||||
secondary_top + Vector2i(column_offset, row_offset)
|
||||
)
|
||||
)
|
||||
var found_deep_top_landing := false
|
||||
for ramp_record: Dictionary in ramp_records:
|
||||
var ramp_coordinate: Vector2i = ramp_record.get(
|
||||
"coordinate", Vector2i(-1, -1)
|
||||
)
|
||||
var ramp_turns := int(ramp_record.get("rotation_quarters", 0))
|
||||
var high_edge := TerrainChunkTopology.rotated_edge(
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
ramp_turns,
|
||||
)
|
||||
var high_offset := TerrainChunkTopology.grid_offset(high_edge)
|
||||
assert(
|
||||
elevated_coordinates.get(ramp_coordinate + high_offset, &"")
|
||||
== &"chunk_0009"
|
||||
)
|
||||
found_deep_top_landing = (
|
||||
found_deep_top_landing
|
||||
or elevated_coordinates.get(
|
||||
ramp_coordinate + high_offset * 2,
|
||||
&"",
|
||||
) == &"chunk_0009"
|
||||
)
|
||||
var low_coordinate := ramp_coordinate - high_offset
|
||||
var low_index := (
|
||||
low_coordinate.y * generator.grid_size.x + low_coordinate.x
|
||||
)
|
||||
assert(
|
||||
generator.placement_keys()[low_index].begins_with("chunk_0000@")
|
||||
or generator.placement_keys()[low_index].begins_with("chunk_spawn@")
|
||||
)
|
||||
assert(found_deep_top_landing)
|
||||
|
||||
var generated := generator.get_generated_chunks_root()
|
||||
assert(generated != null)
|
||||
|
|
@ -367,25 +478,33 @@ func _validate_elevated_cliff_feature(
|
|||
&"chunk_0010",
|
||||
&"chunk_0011",
|
||||
&"chunk_0012",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
]:
|
||||
continue
|
||||
if stable_id in [&"chunk_0014", &"chunk_0015"]:
|
||||
assert(chunk_root.get_node_or_null("TerrainBaseLayer") == null)
|
||||
continue
|
||||
layered_count += 1
|
||||
var base_layer := chunk_root.get_node_or_null("TerrainBaseLayer")
|
||||
var overlay := chunk_root.get_node_or_null("TerrainOverlay")
|
||||
assert(base_layer != null and overlay != null)
|
||||
var is_transition := stable_id in [&"chunk_0017", &"chunk_0018"]
|
||||
var base_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
base_layer,
|
||||
&"chunk_0000",
|
||||
&"chunk_0005" if is_transition else &"chunk_0000",
|
||||
)
|
||||
var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
overlay,
|
||||
stable_id,
|
||||
&"chunk_0015" if is_transition else stable_id,
|
||||
)
|
||||
assert(base_mesh != null and base_mesh.mesh != null)
|
||||
assert(overlay_mesh != null and overlay_mesh.mesh != null)
|
||||
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
|
||||
assert(overlay_mesh.has_node("TerrainCollision"))
|
||||
assert(layered_count == 9)
|
||||
assert(layered_count == (31 if has_coastal_feature else 25))
|
||||
var stacked_keys := generator.stacked_elevated_placement_keys()
|
||||
assert(stacked_keys.size() in [0, 4])
|
||||
if stacked_keys.is_empty():
|
||||
|
|
@ -441,6 +560,28 @@ func _validate_ocean_facing_record(
|
|||
)
|
||||
|
||||
|
||||
func _validate_complete_coastline(generator: TerrainChunkGenerator) -> void:
|
||||
for record: Dictionary in generator.placement_records():
|
||||
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||
var ocean_edges := int(record.get("ocean_facing_edges", 0))
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
var neighbor := (
|
||||
coordinate
|
||||
+ TerrainChunkTopology.grid_offset(
|
||||
edge_value as TerrainChunkTopology.Edge
|
||||
)
|
||||
)
|
||||
var outside := (
|
||||
neighbor.x < 0
|
||||
or neighbor.y < 0
|
||||
or neighbor.x >= generator.grid_size.x
|
||||
or neighbor.y >= generator.grid_size.y
|
||||
)
|
||||
assert(
|
||||
((ocean_edges & (1 << edge_value)) != 0) == outside
|
||||
)
|
||||
|
||||
|
||||
func _ocean_direction(ocean_edges: int) -> Vector3:
|
||||
var result := Vector3.ZERO
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -522,13 +663,19 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
|||
+ catalog.definition_for_id(&"prop_tree_3").selection_weight
|
||||
)
|
||||
assert(forest_rule.allows_prop(large_tree))
|
||||
assert(large_tree.selection_weight > small_tree_weight)
|
||||
assert(large_tree.selection_weight <= small_tree_weight)
|
||||
var pine_forest := region.get_biome_catalog().definition_for_id(
|
||||
&"biome_pine_forest"
|
||||
)
|
||||
var pine_rule := pine_forest.prop_rule_for_group(&"grass_tree")
|
||||
assert(pine_rule.allows_prop(large_pine))
|
||||
assert(large_pine.selection_weight > pine.selection_weight)
|
||||
assert(
|
||||
is_equal_approx(large_pine.selection_weight, pine.selection_weight)
|
||||
)
|
||||
assert(forest_rule.minimum_placements == 4)
|
||||
assert(forest_rule.placement_attempts_per_chunk == 4)
|
||||
assert(pine_rule.minimum_placements == 4)
|
||||
assert(pine_rule.placement_attempts_per_chunk == 4)
|
||||
|
||||
|
||||
func _validate_biome_catalog(
|
||||
|
|
@ -623,6 +770,57 @@ func _validate_decoration_transform(
|
|||
assert(absf(ground_position.y - prop.global_position.y) <= 0.01)
|
||||
|
||||
|
||||
func _validate_tree_gatherable_anchors(
|
||||
region: GeneratedWorldRegion,
|
||||
decorations: Node3D,
|
||||
anchors: GatherableAnchorSet3D,
|
||||
) -> void:
|
||||
var eligible_props: Array[Node3D] = []
|
||||
for child: Node in decorations.get_children():
|
||||
var prop := child as Node3D
|
||||
if prop == null:
|
||||
continue
|
||||
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
||||
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||
if definition != null and definition.gatherable_anchor_height > 0.0:
|
||||
eligible_props.append(prop)
|
||||
var positions := anchors.get_spawn_positions()
|
||||
assert(positions.size() == eligible_props.size())
|
||||
for prop: Node3D in eligible_props:
|
||||
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
||||
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||
var visual_scale := float(
|
||||
prop.get_meta(&"terrain_prop_visual_scale", 1.0)
|
||||
)
|
||||
var nearest_anchor := Vector3(INF, INF, INF)
|
||||
var nearest_distance_squared := INF
|
||||
for position: Vector3 in positions:
|
||||
var distance_squared := position.distance_squared_to(
|
||||
prop.global_position
|
||||
)
|
||||
if distance_squared < nearest_distance_squared:
|
||||
nearest_distance_squared = distance_squared
|
||||
nearest_anchor = position
|
||||
assert(nearest_anchor.is_finite())
|
||||
var horizontal_distance := Vector2(
|
||||
nearest_anchor.x - prop.global_position.x,
|
||||
nearest_anchor.z - prop.global_position.z,
|
||||
).length()
|
||||
assert(
|
||||
horizontal_distance
|
||||
>= definition.gatherable_anchor_surface_radius() * visual_scale
|
||||
)
|
||||
assert(
|
||||
absf(
|
||||
nearest_anchor.y
|
||||
- (
|
||||
prop.global_position.y
|
||||
+ definition.gatherable_anchor_height * visual_scale
|
||||
)
|
||||
) <= 0.001
|
||||
)
|
||||
|
||||
|
||||
func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
|
||||
if root_node is MeshInstance3D:
|
||||
return root_node as MeshInstance3D
|
||||
|
|
@ -662,13 +860,15 @@ func _validate_authored_chunk_surfaces(
|
|||
var generated := generator.get_generated_chunks_root()
|
||||
var found_beach := false
|
||||
var found_stream := false
|
||||
var found_alternate_stream := false
|
||||
var found_pond := false
|
||||
var found_grass_ocean_edge := false
|
||||
var found_grass_beach_transition := false
|
||||
var found_grass_ocean_corner := false
|
||||
var found_beach_ocean_corner := false
|
||||
var found_grass_sand_diagonal := false
|
||||
var beach_family := (
|
||||
_placement_count(generator.placement_keys(), "chunk_0002") > 0
|
||||
)
|
||||
for chunk_root: Node in generated.get_children():
|
||||
if chunk_root.name.begins_with("chunk_0002r"):
|
||||
var beach := chunk_root.find_child(
|
||||
|
|
@ -695,9 +895,7 @@ func _validate_authored_chunk_surfaces(
|
|||
assert("dirt" in stream_materials)
|
||||
assert("grass_lite" in stream_materials)
|
||||
assert("dirt_wall" in stream_materials)
|
||||
if stream_id == &"chunk_0016":
|
||||
found_alternate_stream = true
|
||||
else:
|
||||
if stream_id == &"chunk_0003":
|
||||
found_stream = true
|
||||
elif chunk_root.name.begins_with("chunk_0004r"):
|
||||
var pond := chunk_root.find_child(
|
||||
|
|
@ -761,15 +959,20 @@ func _validate_authored_chunk_surfaces(
|
|||
assert("grass_lite" in diagonal_materials)
|
||||
assert("sand" in diagonal_materials)
|
||||
found_grass_sand_diagonal = true
|
||||
assert(found_beach)
|
||||
assert(found_stream)
|
||||
assert(found_alternate_stream)
|
||||
assert(found_pond)
|
||||
assert(found_grass_ocean_edge)
|
||||
assert(found_grass_beach_transition)
|
||||
assert(found_grass_ocean_corner)
|
||||
assert(found_beach_ocean_corner)
|
||||
assert(found_grass_sand_diagonal)
|
||||
if beach_family:
|
||||
assert(found_beach)
|
||||
assert(found_beach_ocean_corner)
|
||||
assert(not found_grass_ocean_edge)
|
||||
assert(not found_grass_ocean_corner)
|
||||
else:
|
||||
assert(not found_beach)
|
||||
assert(not found_beach_ocean_corner)
|
||||
assert(found_grass_ocean_edge)
|
||||
assert(found_grass_ocean_corner)
|
||||
assert(not found_grass_beach_transition)
|
||||
|
||||
|
||||
func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func _run() -> void:
|
|||
func _make_generator() -> TerrainChunkGenerator:
|
||||
var generator := TerrainChunkGenerator.new()
|
||||
generator.catalog = CATALOG
|
||||
generator.grid_size = Vector2i(7, 7)
|
||||
generator.grid_size = Vector2i(9, 9)
|
||||
generator.generation_seed = 13001
|
||||
generator.generate_on_ready = false
|
||||
generator.build_collision = false
|
||||
|
|
@ -113,13 +113,8 @@ func _make_generator() -> TerrainChunkGenerator:
|
|||
[
|
||||
"chunk_spawn",
|
||||
"chunk_0001",
|
||||
"chunk_0002",
|
||||
"chunk_0003",
|
||||
"chunk_0004",
|
||||
"chunk_0005",
|
||||
"chunk_0006",
|
||||
"chunk_0007",
|
||||
"chunk_0008",
|
||||
]
|
||||
)
|
||||
generator.grass_sand_smoothing_enabled = true
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ shadow_enabled = true
|
|||
[node name="TerrainChunkGenerator" type="Node3D" parent="."]
|
||||
script = ExtResource("2_generator")
|
||||
catalog = ExtResource("3_catalog")
|
||||
grid_size = Vector2i(11, 9)
|
||||
grid_size = Vector2i(15, 13)
|
||||
generation_seed = 13001
|
||||
build_collision = true
|
||||
show_chunk_labels = false
|
||||
force_center_chunk_id = &"chunk_spawn"
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008")
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0003", "chunk_0004")
|
||||
grass_sand_smoothing_enabled = true
|
||||
elevated_cliff_feature_enabled = true
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,15 @@ const EXPECTED_IDS: Array[String] = [
|
|||
"chunk_0014",
|
||||
"chunk_0015",
|
||||
"chunk_0016",
|
||||
"chunk_0017",
|
||||
"chunk_0018",
|
||||
"chunk_spawn",
|
||||
]
|
||||
const REQUIRED_IDS: Array[String] = [
|
||||
"chunk_spawn",
|
||||
"chunk_0001",
|
||||
"chunk_0002",
|
||||
"chunk_0003",
|
||||
"chunk_0004",
|
||||
"chunk_0005",
|
||||
"chunk_0006",
|
||||
"chunk_0007",
|
||||
"chunk_0008",
|
||||
]
|
||||
const TEST_SEED := 13001
|
||||
const CONNECTOR_STRESS_SEED := 287
|
||||
|
|
@ -55,6 +52,8 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
|
|||
&"chunk_0014": 4,
|
||||
&"chunk_0015": 4,
|
||||
&"chunk_0016": 4,
|
||||
&"chunk_0017": 4,
|
||||
&"chunk_0018": 4,
|
||||
&"chunk_spawn": 1,
|
||||
}
|
||||
|
||||
|
|
@ -97,8 +96,11 @@ func _validate_catalog() -> void:
|
|||
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
|
||||
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
|
||||
var grass_sand_diagonal := CATALOG.definition_for_id(&"chunk_0013")
|
||||
var pond := CATALOG.definition_for_id(&"chunk_0004")
|
||||
var cliff_sea_edge := CATALOG.definition_for_id(&"chunk_0014")
|
||||
var cliff_sea_corner := CATALOG.definition_for_id(&"chunk_0015")
|
||||
var cliff_sea_transition_right := CATALOG.definition_for_id(&"chunk_0017")
|
||||
var cliff_sea_transition_left := CATALOG.definition_for_id(&"chunk_0018")
|
||||
var stream_variant := CATALOG.definition_for_id(&"chunk_0016")
|
||||
var cliff_top := CATALOG.definition_for_id(&"chunk_0009")
|
||||
var cliff_corner := CATALOG.definition_for_id(&"chunk_0010")
|
||||
|
|
@ -115,8 +117,11 @@ func _validate_catalog() -> void:
|
|||
and grass_ocean_corner != null
|
||||
and beach_ocean_corner != null
|
||||
and grass_sand_diagonal != null
|
||||
and pond != null
|
||||
and cliff_sea_edge != null
|
||||
and cliff_sea_corner != null
|
||||
and cliff_sea_transition_right != null
|
||||
and cliff_sea_transition_left != null
|
||||
and stream_variant != null
|
||||
and cliff_top != null
|
||||
and cliff_corner != null
|
||||
|
|
@ -135,8 +140,11 @@ func _validate_catalog() -> void:
|
|||
and grass_ocean_corner != null
|
||||
and beach_ocean_corner != null
|
||||
and grass_sand_diagonal != null
|
||||
and pond != null
|
||||
and cliff_sea_edge != null
|
||||
and cliff_sea_corner != null
|
||||
and cliff_sea_transition_right != null
|
||||
and cliff_sea_transition_left != null
|
||||
and stream_variant != null
|
||||
and cliff_top != null
|
||||
and cliff_corner != null
|
||||
|
|
@ -157,8 +165,11 @@ func _validate_catalog() -> void:
|
|||
"Grass must permit the inland side of a flat-sand transition.",
|
||||
)
|
||||
_check(
|
||||
"coast" in sand.required_neighbor_tags,
|
||||
"Every flat-sand chunk must remain attached to an authored beach.",
|
||||
(
|
||||
"sand" in sand.required_neighbor_tags
|
||||
and "coast" in sand.required_neighbor_tags
|
||||
),
|
||||
"Every flat-sand chunk must remain attached to sand or authored coast.",
|
||||
)
|
||||
_check(
|
||||
spawn.minimum_required_neighbors == 3,
|
||||
|
|
@ -281,6 +292,12 @@ func _validate_catalog() -> void:
|
|||
"The diagonal's authored edges must retain two sand and two grass sides.",
|
||||
)
|
||||
var stream := CATALOG.definition_for_id(&"chunk_0003")
|
||||
_check(
|
||||
pond.water_inlet_edges == pond.water_outlet_edges
|
||||
and pond.water_inlet_edges
|
||||
== (1 << int(TerrainChunkTopology.Edge.NORTH)),
|
||||
"A pond must terminate either end of an inland freshwater chain.",
|
||||
)
|
||||
_check(
|
||||
stream != null
|
||||
and is_equal_approx(stream.selection_weight, 0.3)
|
||||
|
|
@ -331,6 +348,34 @@ func _validate_catalog() -> void:
|
|||
+ "overlay without an inland grass base."
|
||||
),
|
||||
)
|
||||
for transition: TerrainChunkDefinition in [
|
||||
cliff_sea_transition_right,
|
||||
cliff_sea_transition_left,
|
||||
]:
|
||||
_check(
|
||||
transition.overlay_only
|
||||
and transition.base_layer_scene != null
|
||||
and transition.base_layer_mesh_name == &"chunk_0005"
|
||||
and not transition.must_be_interior
|
||||
and transition.prefers_map_boundary
|
||||
and "transition" in transition.tags,
|
||||
(
|
||||
"Each raised-coast endpoint must layer the deep corner "
|
||||
+ "over an authored level-one coastline."
|
||||
),
|
||||
)
|
||||
_check(
|
||||
cliff_sea_transition_right.base_layer_quarter_turns == 3
|
||||
and cliff_sea_transition_right.ocean_facing_edges
|
||||
== (1 << int(TerrainChunkTopology.Edge.SOUTH)),
|
||||
"The right coastal transition must align its south-facing base coast.",
|
||||
)
|
||||
_check(
|
||||
cliff_sea_transition_left.base_layer_quarter_turns == 0
|
||||
and cliff_sea_transition_left.ocean_facing_edges
|
||||
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
||||
"The left coastal transition must align its east-facing base coast.",
|
||||
)
|
||||
_check(
|
||||
cliff_sea_edge.ocean_facing_edges
|
||||
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
||||
|
|
@ -486,21 +531,17 @@ func _validate_generation() -> void:
|
|||
var first := _make_generator()
|
||||
root.add_child(first)
|
||||
var first_solved := first.generate()
|
||||
_check(first_solved, "Generator must solve the prototype 7x7 grid.")
|
||||
_check(first_solved, "Generator must solve the prototype 9x9 grid.")
|
||||
if not first_solved:
|
||||
first.free()
|
||||
return
|
||||
var first_keys := first.placement_keys()
|
||||
_check(first_keys.size() == 49, "Generator must place exactly 49 chunks.")
|
||||
_check(first_keys.size() == 81, "Generator must place exactly 81 chunks.")
|
||||
for stable_id: String in REQUIRED_IDS:
|
||||
_check(
|
||||
_placements_contain(first_keys, stable_id),
|
||||
"Generated diagnostic must contain %s." % stable_id,
|
||||
)
|
||||
_check(
|
||||
_placements_contain(first_keys, "chunk_0013"),
|
||||
"Generated terrain must replace sharp grass-sand corners with diagonals.",
|
||||
)
|
||||
_check(
|
||||
_all_neighbor_edges_match(first),
|
||||
"Every generated neighboring edge must match.",
|
||||
|
|
@ -510,7 +551,7 @@ func _validate_generation() -> void:
|
|||
"Generated layouts must contain exactly one spawn chunk.",
|
||||
)
|
||||
_check(
|
||||
first_keys[24].begins_with("chunk_spawn@"),
|
||||
first_keys[40].begins_with("chunk_spawn@"),
|
||||
"The spawn chunk must occupy the center cell.",
|
||||
)
|
||||
_validate_layout_rules(first)
|
||||
|
|
@ -837,18 +878,24 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
"The beach corner's west side must join a rotated straight beach.",
|
||||
)
|
||||
var cliff_top_zero := _find_variant(generator, &"chunk_0009", 0)
|
||||
var cliff_edge_zero := _find_variant(generator, &"chunk_0011", 0)
|
||||
var cliff_edge_south := _find_variant(generator, &"chunk_0011", 3)
|
||||
var cliff_sea_edge_zero := _find_variant(generator, &"chunk_0014", 0)
|
||||
var cliff_sea_edge_south := _find_variant(generator, &"chunk_0014", 3)
|
||||
var cliff_sea_corner_zero := _find_variant(generator, &"chunk_0015", 0)
|
||||
_check(
|
||||
cliff_top_zero != null
|
||||
and cliff_edge_zero != null
|
||||
and cliff_edge_south != null
|
||||
and cliff_sea_edge_zero != null
|
||||
and cliff_sea_edge_south != null
|
||||
and cliff_sea_corner_zero != null,
|
||||
"The second-tier sea edge variants must be available.",
|
||||
"The inland and coastal second-tier cliff variants must be available.",
|
||||
)
|
||||
if (
|
||||
cliff_top_zero != null
|
||||
and cliff_edge_zero != null
|
||||
and cliff_edge_south != null
|
||||
and cliff_sea_edge_zero != null
|
||||
and cliff_sea_edge_south != null
|
||||
and cliff_sea_corner_zero != null
|
||||
|
|
@ -892,6 +939,42 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
),
|
||||
"The second-tier sea corner must join two straight sea edges.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
cliff_sea_edge_zero,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
cliff_edge_zero,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
cliff_sea_edge_zero,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
cliff_edge_zero,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
),
|
||||
(
|
||||
"A coastal cliff edge must transition directly into its "
|
||||
+ "inland edge counterpart."
|
||||
),
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
cliff_sea_corner_zero,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
cliff_edge_zero,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
cliff_sea_corner_zero,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
cliff_edge_south,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
),
|
||||
(
|
||||
"A coastal cliff corner must transition directly into "
|
||||
+ "ordinary inland cliff edges."
|
||||
),
|
||||
)
|
||||
var flat_grass := _find_variant(generator, &"chunk_0000", 0)
|
||||
var flat_sand := _find_variant(generator, &"chunk_0001", 0)
|
||||
_check(
|
||||
|
|
@ -981,6 +1064,8 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
&"chunk_0008",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
]:
|
||||
for quarter_turns: int in 4:
|
||||
var coast_variant := _find_variant(
|
||||
|
|
@ -1032,8 +1117,8 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
|
||||
var summary := generator._build_summary()
|
||||
_check(
|
||||
int(summary.get("variant_count", 0)) == 69,
|
||||
"The current catalog must expose all 69 authored rotations.",
|
||||
int(summary.get("variant_count", 0)) == 77,
|
||||
"The current catalog must expose all 77 authored rotations.",
|
||||
)
|
||||
_check(
|
||||
int(summary.get("solver_variant_count", 0))
|
||||
|
|
@ -1080,7 +1165,7 @@ func _find_variant(
|
|||
func _make_generator() -> TerrainChunkGenerator:
|
||||
var generator := TerrainChunkGenerator.new()
|
||||
generator.catalog = CATALOG
|
||||
generator.grid_size = Vector2i(7, 7)
|
||||
generator.grid_size = Vector2i(9, 9)
|
||||
generator.generation_seed = TEST_SEED
|
||||
generator.generate_on_ready = false
|
||||
generator.build_collision = false
|
||||
|
|
@ -1185,6 +1270,23 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|||
),
|
||||
"Every coastline chunk must direct its ocean edge outside the grid.",
|
||||
)
|
||||
var ocean_edges := placement.rotated_edge_mask(
|
||||
placement.definition.ocean_facing_edges
|
||||
)
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
var neighbor_coordinate := (
|
||||
coordinate
|
||||
+ TerrainChunkTopology.grid_offset(
|
||||
edge_value as TerrainChunkTopology.Edge
|
||||
)
|
||||
)
|
||||
var outside := not generator._coordinate_is_inside_grid(
|
||||
neighbor_coordinate
|
||||
)
|
||||
_check(
|
||||
((ocean_edges & (1 << edge_value)) != 0) == outside,
|
||||
"Every map-boundary edge must use authored coastline geometry.",
|
||||
)
|
||||
if placement.definition.stable_id == &"chunk_0006":
|
||||
var touches_beach := false
|
||||
var touches_grass_edge := false
|
||||
|
|
@ -1257,7 +1359,7 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|||
)
|
||||
if placement.definition.stable_id != &"chunk_0001":
|
||||
continue
|
||||
var touches_coast := false
|
||||
var touches_sand_or_coast := false
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
var neighbor_coordinate := (
|
||||
coordinate
|
||||
|
|
@ -1271,11 +1373,14 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|||
neighbor_coordinate.y * generator.grid_size.x
|
||||
+ neighbor_coordinate.x
|
||||
]
|
||||
if "coast" in neighbor.definition.tags:
|
||||
touches_coast = true
|
||||
if (
|
||||
"sand" in neighbor.definition.tags
|
||||
or "coast" in neighbor.definition.tags
|
||||
):
|
||||
touches_sand_or_coast = true
|
||||
_check(
|
||||
touches_coast,
|
||||
"Every flat-sand chunk must remain directly attached to a beach.",
|
||||
touches_sand_or_coast,
|
||||
"Every flat-sand chunk must remain attached to sand or coast.",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
[sub_resource type="Resource" id="Rule_grass_tree"]
|
||||
script = ExtResource("2_rule")
|
||||
procedural_group = &"grass_tree"
|
||||
placement_chance = 8200
|
||||
adjacency_bonus = 1200
|
||||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
placement_chance = 9400
|
||||
adjacency_bonus = 600
|
||||
maximum_density = 9500
|
||||
minimum_placements = 4
|
||||
placement_attempts_per_chunk = 4
|
||||
allowed_prop_ids = PackedStringArray("prop_tree_large", "prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
[sub_resource type="Resource" id="Rule_grass_tree"]
|
||||
script = ExtResource("2_rule")
|
||||
procedural_group = &"grass_tree"
|
||||
placement_chance = 8200
|
||||
adjacency_bonus = 1200
|
||||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
placement_chance = 9400
|
||||
adjacency_bonus = 600
|
||||
maximum_density = 9500
|
||||
minimum_placements = 4
|
||||
placement_attempts_per_chunk = 4
|
||||
allowed_prop_ids = PackedStringArray("prop_pine_large", "prop_pine")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ allowed_rotation_mask = 15
|
|||
selection_weight = 1.5
|
||||
tags = PackedStringArray("land", "walkable", "sand", "flat")
|
||||
allowed_neighbor_tags = PackedStringArray("sand", "grass")
|
||||
required_neighbor_tags = PackedStringArray("coast")
|
||||
required_neighbor_tags = PackedStringArray("sand", "coast")
|
||||
minimum_required_neighbors = 1
|
||||
preferred_neighbor_tags = PackedStringArray("coast", "sand")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ label = "beach"
|
|||
packed_scene = ExtResource("2_scene")
|
||||
primary_mesh_name = &"chunk_0002"
|
||||
selection_weight = 0.75
|
||||
maximum_placements = 20
|
||||
maximum_placements = 64
|
||||
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope", "beach_ocean_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("sand", "grass")
|
||||
required_neighbor_tags = PackedStringArray("sand")
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ tags = PackedStringArray("land", "walkable", "fresh_water", "pond")
|
|||
allowed_neighbor_tags = PackedStringArray("grass")
|
||||
preferred_neighbor_tags = PackedStringArray("grass")
|
||||
water_inlet_edges = 1
|
||||
water_outlet_edges = 1
|
||||
water_surface_size = Vector2(7, 8)
|
||||
water_surface_offset = Vector2(-0.32, -1.03)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ label = "grass ocean edge"
|
|||
packed_scene = ExtResource("2_scene")
|
||||
primary_mesh_name = &"chunk_0005"
|
||||
selection_weight = 1.0
|
||||
maximum_placements = 20
|
||||
maximum_placements = 64
|
||||
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "grass_ocean_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
|
||||
north_allowed_neighbor_tags = PackedStringArray("coast")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ base_layer_scene = ExtResource("3_base_grass")
|
|||
base_layer_mesh_name = &"chunk_0000"
|
||||
overlay_only = true
|
||||
selection_weight = 0.22
|
||||
maximum_placements = 8
|
||||
maximum_placements = 32
|
||||
tags = PackedStringArray("land", "walkable", "grass", "elevation_2", "elevation_2_top")
|
||||
allowed_neighbor_tags = PackedStringArray("elevation_2")
|
||||
required_neighbor_tags = PackedStringArray("elevation_2")
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ selection_weight = 0.28
|
|||
maximum_placements = 32
|
||||
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "edge", "elevation_2", "elevation_2_border", "elevation_2_sea_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("elevation_2")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge")
|
||||
south_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
south_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
west_allowed_neighbor_tags = PackedStringArray("elevation_2_top")
|
||||
required_neighbor_tags = PackedStringArray("elevation_2_top")
|
||||
minimum_required_neighbors = 1
|
||||
preferred_neighbor_tags = PackedStringArray("elevation_2", "coast")
|
||||
prefers_map_boundary = true
|
||||
ocean_facing_edges = 2
|
||||
buried_cliff_seam_edges = 5
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ selection_weight = 0.12
|
|||
maximum_placements = 16
|
||||
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner", "elevation_2", "elevation_2_border", "elevation_2_sea_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("elevation_2")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge")
|
||||
west_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
west_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
required_neighbor_tags = PackedStringArray("elevation_2_sea_edge")
|
||||
minimum_required_neighbors = 2
|
||||
preferred_neighbor_tags = PackedStringArray("elevation_2", "coast")
|
||||
prefers_map_boundary = true
|
||||
ocean_facing_edges = 6
|
||||
buried_cliff_seam_edges = 9
|
||||
|
|
|
|||
29
world/generation/chunks/definitions/chunk_0017.tres
Normal file
29
world/generation/chunks/definitions/chunk_0017.tres
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0005.glb" id="3_base_coast"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"chunk_0017"
|
||||
label = "second-tier grass sea transition right"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
primary_mesh_name = &"chunk_0015"
|
||||
base_layer_scene = ExtResource("3_base_coast")
|
||||
base_layer_mesh_name = &"chunk_0005"
|
||||
base_layer_quarter_turns = 3
|
||||
overlay_only = true
|
||||
selection_weight = 0.12
|
||||
maximum_placements = 8
|
||||
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner", "transition", "elevation_2", "elevation_2_border", "elevation_2_sea_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("elevation_2")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
east_allowed_neighbor_tags = PackedStringArray("grass")
|
||||
west_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
required_neighbor_tags = PackedStringArray("elevation_2_border")
|
||||
minimum_required_neighbors = 2
|
||||
preferred_neighbor_tags = PackedStringArray("elevation_2", "coast")
|
||||
prefers_map_boundary = true
|
||||
ocean_facing_edges = 4
|
||||
buried_cliff_seam_edges = 9
|
||||
28
world/generation/chunks/definitions/chunk_0018.tres
Normal file
28
world/generation/chunks/definitions/chunk_0018.tres
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"]
|
||||
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0005.glb" id="3_base_coast"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"chunk_0018"
|
||||
label = "second-tier grass sea transition left"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
primary_mesh_name = &"chunk_0015"
|
||||
base_layer_scene = ExtResource("3_base_coast")
|
||||
base_layer_mesh_name = &"chunk_0005"
|
||||
overlay_only = true
|
||||
selection_weight = 0.12
|
||||
maximum_placements = 8
|
||||
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner", "transition", "elevation_2", "elevation_2_border", "elevation_2_sea_edge")
|
||||
allowed_neighbor_tags = PackedStringArray("elevation_2")
|
||||
north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
south_allowed_neighbor_tags = PackedStringArray("grass")
|
||||
west_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge", "elevation_2_border")
|
||||
required_neighbor_tags = PackedStringArray("elevation_2_border")
|
||||
minimum_required_neighbors = 2
|
||||
preferred_neighbor_tags = PackedStringArray("elevation_2", "coast")
|
||||
prefers_map_boundary = true
|
||||
ocean_facing_edges = 2
|
||||
buried_cliff_seam_edges = 9
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=21 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=23 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
|
||||
|
|
@ -20,8 +20,10 @@
|
|||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0014.tres" id="18_cliff_sea_edge"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0015.tres" id="19_cliff_sea_corner"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0016.tres" id="20_stream_variant"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0017.tres" id="21_cliff_sea_transition_right"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0018.tres" id="22_cliff_sea_transition_left"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_catalog")
|
||||
chunk_size = 10.0
|
||||
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("20_stream_variant"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("17_grass_sand_diagonal"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("18_cliff_sea_edge"), ExtResource("19_cliff_sea_corner"), ExtResource("8_spawn")])
|
||||
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("20_stream_variant"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("17_grass_sand_diagonal"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("18_cliff_sea_edge"), ExtResource("19_cliff_sea_corner"), ExtResource("21_cliff_sea_transition_right"), ExtResource("22_cliff_sea_transition_left"), ExtResource("8_spawn")])
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
|
|||
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||
const PROP_CHANCE_SCALE := 10000
|
||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||
const GATHERABLE_ANCHOR_SURFACE_CLEARANCE := 0.02
|
||||
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
||||
&"grass_tree",
|
||||
&"grass_detail",
|
||||
|
|
@ -665,10 +666,21 @@ func _instantiate_prop(
|
|||
if definition.gatherable_anchor_height > 0.0:
|
||||
var anchor := Marker3D.new()
|
||||
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
||||
anchor.position = prop.position + Vector3(
|
||||
0.0,
|
||||
definition.gatherable_anchor_height * visual_scale,
|
||||
0.0,
|
||||
var local_anchor_position := (
|
||||
definition.collision_offset * visual_scale
|
||||
+ Vector3(
|
||||
0.0,
|
||||
definition.gatherable_anchor_height * visual_scale,
|
||||
-(
|
||||
definition.gatherable_anchor_surface_radius()
|
||||
* visual_scale
|
||||
+ GATHERABLE_ANCHOR_SURFACE_CLEARANCE
|
||||
),
|
||||
)
|
||||
)
|
||||
anchor.position = prop.position + local_anchor_position.rotated(
|
||||
Vector3.UP,
|
||||
yaw,
|
||||
)
|
||||
_tree_anchors.add_child(anchor)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ generation_seed = 13001
|
|||
generate_on_ready = false
|
||||
build_collision = true
|
||||
force_center_chunk_id = &"chunk_spawn"
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008")
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0003", "chunk_0004")
|
||||
grass_sand_smoothing_enabled = true
|
||||
elevated_cliff_feature_enabled = true
|
||||
required_chunk_weight_multiplier = 64.0
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ label = "small pine"
|
|||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 0.7
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.55
|
||||
minimum_visual_scale = 0.65
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ label = "large pine"
|
|||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 3.0
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.65
|
||||
minimum_visual_scale = 0.75
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ label = "large deciduous tree"
|
|||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 12.0
|
||||
selection_weight = 3.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 2.5
|
||||
minimum_visual_scale = 0.75
|
||||
|
|
|
|||
|
|
@ -51,7 +51,18 @@ static func create_variants(
|
|||
boundary_points.append_array(
|
||||
_collect_boundary_points(
|
||||
base_layer_mesh.mesh,
|
||||
_transform_relative_to(base_layer_mesh, base_layer_root),
|
||||
Transform3D(
|
||||
Basis(
|
||||
Vector3.UP,
|
||||
float(definition.base_layer_quarter_turns)
|
||||
* PI
|
||||
* 0.5,
|
||||
),
|
||||
Vector3.ZERO,
|
||||
) * _transform_relative_to(
|
||||
base_layer_mesh,
|
||||
base_layer_root,
|
||||
),
|
||||
chunk_size,
|
||||
edge_epsilon,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ extends Resource
|
|||
@export_group("Layered Terrain")
|
||||
@export var base_layer_scene: PackedScene
|
||||
@export var base_layer_mesh_name: StringName
|
||||
## Optional canonical rotation applied to a layered base before the authored
|
||||
## chunk variant rotation. Coastal transition overlays use this to align one
|
||||
## shared coastline scene with either downhill side of a corner.
|
||||
@export_range(0, 3, 1) var base_layer_quarter_turns := 0
|
||||
## Overlay-only chunks replace a reserved base-terrain placement after the
|
||||
## ordinary terrain solve. Inland overlays normally provide base_layer_scene;
|
||||
## complete coastal overlays can instead carry their own descent to sea level.
|
||||
|
|
@ -72,6 +76,10 @@ var allowed_rotation_mask := 15
|
|||
## in this mask must face outside the generated grid; it may never meet another
|
||||
## terrain chunk.
|
||||
@export_flags("North", "East", "South", "West") var ocean_facing_edges := 0
|
||||
## Lateral seams where a deep coastal cliff may join its ordinary inland
|
||||
## counterpart. Geometry below level-one ground is intentionally ignored on
|
||||
## these authored edges because the neighboring base terrain buries it.
|
||||
@export_flags("North", "East", "South", "West") var buried_cliff_seam_edges := 0
|
||||
@export_flags("North", "East", "South", "West") var water_inlet_edges := 0
|
||||
@export_flags("North", "East", "South", "West") var water_outlet_edges := 0
|
||||
## Optional generated water footprint in the chunk's unrotated local X/Z plane.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,38 @@ func matches(other: TerrainChunkEdgeProfile, tolerance: float) -> bool:
|
|||
return false
|
||||
var first_surface := _upper_surface_points()
|
||||
var second_surface := other._upper_surface_points()
|
||||
return _surfaces_match(first_surface, second_surface, tolerance)
|
||||
|
||||
|
||||
## Compares only the visible raised portion of two edge profiles. Authored
|
||||
## deep-water cliff pieces continue below ordinary ground level; when their
|
||||
## lateral end meets an inland cliff, that lower extension is buried by the
|
||||
## neighboring base terrain and must not create a false seam mismatch.
|
||||
func matches_above_height(
|
||||
other: TerrainChunkEdgeProfile,
|
||||
minimum_height: float,
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
if other == null:
|
||||
return false
|
||||
var first_surface := _points_above_height(
|
||||
_upper_surface_points(),
|
||||
minimum_height,
|
||||
tolerance,
|
||||
)
|
||||
var second_surface := _points_above_height(
|
||||
other._upper_surface_points(),
|
||||
minimum_height,
|
||||
tolerance,
|
||||
)
|
||||
return _surfaces_match(first_surface, second_surface, tolerance)
|
||||
|
||||
|
||||
func _surfaces_match(
|
||||
first_surface: PackedVector2Array,
|
||||
second_surface: PackedVector2Array,
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
if first_surface.size() < 2 or second_surface.size() < 2:
|
||||
return false
|
||||
if (
|
||||
|
|
@ -43,6 +75,18 @@ func matches(other: TerrainChunkEdgeProfile, tolerance: float) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _points_above_height(
|
||||
surface: PackedVector2Array,
|
||||
minimum_height: float,
|
||||
tolerance: float,
|
||||
) -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
for point: Vector2 in surface:
|
||||
if point.y > minimum_height + tolerance:
|
||||
result.append(point)
|
||||
return result
|
||||
|
||||
|
||||
func _upper_surface_points() -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ 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
|
||||
)
|
||||
# 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
|
||||
|
|
@ -33,7 +40,12 @@ const MAX_PACKED_SOLVER_VARIANTS := 62
|
|||
@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_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 0.75
|
||||
@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_0017"
|
||||
@export var elevated_cliff_sea_transition_left_chunk_id: StringName = &"chunk_0018"
|
||||
@export var elevated_cliff_coast_base_chunk_id: StringName = &"chunk_0005"
|
||||
@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 1.0
|
||||
## A smaller third tier reuses the existing corner geometry at the authored
|
||||
## two-meter level interval. It is nested into the lower cliff assembly rather
|
||||
## than occupying or replacing additional terrain-grid cells.
|
||||
|
|
@ -72,6 +84,7 @@ 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 _stacked_elevated_placements: Array[Dictionary] = []
|
||||
|
||||
|
|
@ -186,7 +199,7 @@ func _resolved_layout_validation_error(
|
|||
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."
|
||||
"cell %d does not exactly match the authored ocean boundary."
|
||||
% index
|
||||
)
|
||||
if coordinate.x + 1 < grid_size.x:
|
||||
|
|
@ -1177,6 +1190,13 @@ func _candidate_can_occupy_cell(
|
|||
if (
|
||||
_elevated_feature_reserved_grass_indices.has(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)
|
||||
|
|
@ -1195,6 +1215,7 @@ func _candidate_can_occupy_cell(
|
|||
|
||||
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:
|
||||
|
|
@ -1206,49 +1227,106 @@ func _prepare_elevated_feature_region() -> bool:
|
|||
elevated_cliff_corner_chunk_id,
|
||||
elevated_cliff_edge_chunk_id,
|
||||
elevated_cliff_ramp_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,
|
||||
]:
|
||||
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 < 9:
|
||||
if grid_size.x < 11 or grid_size.y < 11:
|
||||
push_error(
|
||||
"Elevated cliff feature requires at least an 11x9 terrain grid."
|
||||
"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(2, grid_size.y - 2):
|
||||
for column: int in range(2, grid_size.x - 2):
|
||||
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) <= 2
|
||||
and absi(candidate.y - forced_center.y) <= 2
|
||||
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 3x3 cliff feature location.")
|
||||
push_error("Terrain grid has no inland 5x5 cliff feature location.")
|
||||
return false
|
||||
var feature_random := RandomNumberGenerator.new()
|
||||
feature_random.seed = generation_seed ^ 0x2E1E7A7ED
|
||||
_elevated_feature_center = candidates[
|
||||
feature_random.randi_range(0, candidates.size() - 1)
|
||||
]
|
||||
# Solve the authored 3x3 cliff assembly into a one-cell ring of ordinary
|
||||
# level-one flat grass. Every raised piece still occupies one of the inner
|
||||
# nine grass cells; the outer ring prevents a stream, pond, or coast profile
|
||||
# from being exposed directly against its downhill edge.
|
||||
for row_offset: int in range(-2, 3):
|
||||
for column_offset: int in range(-2, 3):
|
||||
var coordinate := (
|
||||
_elevated_feature_center
|
||||
+ Vector2i(column_offset, row_offset)
|
||||
# Solve the authored 5x5 plateau into a one-cell ring of ordinary level-one
|
||||
# flat grass. The larger top provides a real landing beyond the ramp and
|
||||
# leaves usable second-tier terrain around the optional third tier.
|
||||
_reserve_elevated_feature_base(
|
||||
_elevated_feature_center,
|
||||
ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
||||
)
|
||||
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
|
||||
)
|
||||
)
|
||||
if not secondary_candidates.is_empty():
|
||||
_secondary_elevated_feature_center = secondary_candidates[
|
||||
feature_random.randi_range(0, secondary_candidates.size() - 1)
|
||||
]
|
||||
_reserve_elevated_feature_base(
|
||||
_secondary_elevated_feature_center,
|
||||
SECONDARY_ELEVATED_FEATURE_CLEARANCE_RADIUS,
|
||||
)
|
||||
_build_grid_solver_caches()
|
||||
return true
|
||||
|
||||
|
||||
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 _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
|
||||
_build_grid_solver_caches()
|
||||
return true
|
||||
|
||||
|
||||
func _build_grid_solver_caches() -> void:
|
||||
|
|
@ -1283,6 +1361,14 @@ func _build_grid_solver_caches() -> void:
|
|||
_elevated_feature_reserved_grass_indices.has(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 (
|
||||
|
|
@ -1402,35 +1488,25 @@ func _apply_elevated_feature() -> bool:
|
|||
if _elevated_feature_center.x < 0 or _elevated_feature_center.y < 0:
|
||||
push_error("Elevated cliff feature has no reserved base region.")
|
||||
return false
|
||||
var edge_specs: Array[Dictionary] = [
|
||||
{"offset": Vector2i(0, -1), "turns": 1},
|
||||
{"offset": Vector2i(1, 0), "turns": 0},
|
||||
{"offset": Vector2i(0, 1), "turns": 3},
|
||||
{"offset": Vector2i(-1, 0), "turns": 2},
|
||||
]
|
||||
var feature_random := RandomNumberGenerator.new()
|
||||
feature_random.seed = generation_seed ^ 0x51A7C11FF
|
||||
var ramp_edge := -1
|
||||
if feature_random.randf() < elevated_cliff_ramp_chance:
|
||||
ramp_edge = feature_random.randi_range(0, edge_specs.size() - 1)
|
||||
var placements: Array[Dictionary] = [
|
||||
{"offset": Vector2i(-1, -1), "id": elevated_cliff_corner_chunk_id, "turns": 2},
|
||||
{"offset": Vector2i(1, -1), "id": elevated_cliff_corner_chunk_id, "turns": 1},
|
||||
{"offset": Vector2i(0, 0), "id": elevated_cliff_top_chunk_id, "turns": 0},
|
||||
{"offset": Vector2i(-1, 1), "id": elevated_cliff_corner_chunk_id, "turns": 3},
|
||||
{"offset": Vector2i(1, 1), "id": elevated_cliff_corner_chunk_id, "turns": 0},
|
||||
]
|
||||
for edge_index: int in edge_specs.size():
|
||||
var edge_spec := edge_specs[edge_index]
|
||||
placements.append({
|
||||
"offset": edge_spec["offset"],
|
||||
"id": (
|
||||
elevated_cliff_ramp_chunk_id
|
||||
if edge_index == ramp_edge
|
||||
else elevated_cliff_edge_chunk_id
|
||||
),
|
||||
"turns": edge_spec["turns"],
|
||||
})
|
||||
var placements := _elevated_feature_placement_specs(
|
||||
_elevated_feature_center,
|
||||
ELEVATED_FEATURE_RADIUS,
|
||||
feature_random,
|
||||
)
|
||||
if (
|
||||
_secondary_elevated_feature_center.x >= 0
|
||||
and int(
|
||||
_placement_counts.get(elevated_cliff_coast_base_chunk_id, 0)
|
||||
) > 0
|
||||
):
|
||||
placements.append_array(
|
||||
_coastal_elevated_feature_placement_specs(
|
||||
_secondary_elevated_feature_center,
|
||||
feature_random,
|
||||
)
|
||||
)
|
||||
for spec: Dictionary in placements:
|
||||
var definition := catalog.definition_for_id(spec["id"] as StringName)
|
||||
var variant := _authored_variant(definition, int(spec["turns"]))
|
||||
|
|
@ -1440,7 +1516,7 @@ func _apply_elevated_feature() -> bool:
|
|||
% [spec["id"], spec["turns"]]
|
||||
)
|
||||
return false
|
||||
var coordinate := _elevated_feature_center + (spec["offset"] as Vector2i)
|
||||
var coordinate := spec["coordinate"] as Vector2i
|
||||
_set_placement(
|
||||
coordinate.y * grid_size.x + coordinate.x,
|
||||
variant,
|
||||
|
|
@ -1452,6 +1528,174 @@ func _apply_elevated_feature() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _elevated_feature_placement_specs(
|
||||
center: Vector2i,
|
||||
radius: int,
|
||||
feature_random: RandomNumberGenerator,
|
||||
) -> Array[Dictionary]:
|
||||
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 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": elevated_cliff_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 elevated_cliff_edge_chunk_id
|
||||
),
|
||||
"turns": edge_specs[edge_index]["turns"],
|
||||
})
|
||||
continue
|
||||
placements.append({
|
||||
"coordinate": center + offset,
|
||||
"id": elevated_cliff_top_chunk_id,
|
||||
"turns": 0,
|
||||
})
|
||||
return placements
|
||||
|
||||
|
||||
func _coastal_elevated_feature_placement_specs(
|
||||
center: Vector2i,
|
||||
feature_random: RandomNumberGenerator,
|
||||
) -> Array[Dictionary]:
|
||||
var placements := _elevated_feature_placement_specs(
|
||||
center,
|
||||
SECONDARY_ELEVATED_FEATURE_RADIUS,
|
||||
feature_random,
|
||||
)
|
||||
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"]),
|
||||
)
|
||||
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,
|
||||
) -> StringName:
|
||||
for stable_id: StringName in [
|
||||
elevated_cliff_sea_transition_right_chunk_id,
|
||||
elevated_cliff_sea_transition_left_chunk_id,
|
||||
]:
|
||||
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 (
|
||||
|
|
@ -1459,17 +1703,19 @@ func _configure_stacked_elevated_feature() -> bool:
|
|||
or elevated_cliff_third_level_chance <= 0.0
|
||||
):
|
||||
return true
|
||||
var top_coordinate := Vector2i(-1, -1)
|
||||
for index: int in _placements.size():
|
||||
var placement := _placements[index]
|
||||
if (
|
||||
placement != null
|
||||
and placement.definition.stable_id == elevated_cliff_top_chunk_id
|
||||
):
|
||||
top_coordinate = Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
break
|
||||
var top_coordinate := _elevated_feature_center
|
||||
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
|
||||
or _placements[top_index].definition.stable_id
|
||||
!= elevated_cliff_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:
|
||||
|
|
@ -1515,25 +1761,28 @@ func _variant_respects_ocean_boundary(
|
|||
variant: TerrainChunkVariant,
|
||||
coordinate: Vector2i,
|
||||
) -> bool:
|
||||
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
|
||||
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 edge := edge_value as TerrainChunkTopology.Edge
|
||||
var ocean_coordinate := (
|
||||
coordinate
|
||||
+ TerrainChunkTopology.grid_offset(
|
||||
edge_value as TerrainChunkTopology.Edge
|
||||
)
|
||||
+ TerrainChunkTopology.grid_offset(edge)
|
||||
)
|
||||
var edge_faces_ocean := (ocean_edges & (1 << edge_value)) != 0
|
||||
if _coordinate_is_inside_grid(ocean_coordinate):
|
||||
return false
|
||||
if edge_faces_ocean:
|
||||
return false
|
||||
continue
|
||||
if edge_faces_ocean:
|
||||
continue
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -1616,10 +1865,28 @@ func _calculate_edge_compatibility(
|
|||
second: TerrainChunkVariant,
|
||||
second_edge: TerrainChunkTopology.Edge,
|
||||
) -> bool:
|
||||
if not first.profile(first_edge).matches(
|
||||
var profiles_match := first.profile(first_edge).matches(
|
||||
second.profile(second_edge),
|
||||
edge_match_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,
|
||||
edge_match_tolerance,
|
||||
)
|
||||
if not profiles_match:
|
||||
return false
|
||||
var first_inlet := _variant_edge_has_connector(
|
||||
first,
|
||||
|
|
@ -2195,6 +2462,9 @@ func _instantiate_chunk_root(
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -48,14 +48,18 @@ func topology_signature(quantization: float) -> String:
|
|||
|
||||
|
||||
func constraint_signature(quantization: float) -> String:
|
||||
return "%s|neighbors:%s|surfaces:%s|ocean:%d|in:%d|out:%d" % [
|
||||
return (
|
||||
"%s|neighbors:%s|surfaces:%s|ocean:%d|buried:%d|in:%d|out:%d"
|
||||
% [
|
||||
topology_signature(quantization),
|
||||
_neighbor_rule_signature(),
|
||||
_surface_rule_signature(),
|
||||
rotated_edge_mask(definition.ocean_facing_edges),
|
||||
rotated_edge_mask(definition.buried_cliff_seam_edges),
|
||||
rotated_edge_mask(definition.water_inlet_edges),
|
||||
rotated_edge_mask(definition.water_outlet_edges),
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _neighbor_rule_signature() -> String:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ func validation_errors() -> PackedStringArray:
|
|||
"%s defines more than one collision shape."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.gatherable_anchor_height > 0.0
|
||||
and definition.gatherable_anchor_surface_radius() <= 0.0
|
||||
):
|
||||
errors.append(
|
||||
"%s is gatherable but has no trunk-surface radius."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.collision_box_size.x < 0.0
|
||||
or definition.collision_box_size.y < 0.0
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ extends Resource
|
|||
@export var collision_offset := Vector3.ZERO
|
||||
## Values above zero add this prop to the tree-gathering anchor set.
|
||||
@export_range(0.0, 20.0, 0.05) var gatherable_anchor_height := 0.0
|
||||
## Optional distance from the prop origin to the visible trunk surface. A
|
||||
## zero value derives the distance from the authored collision shape.
|
||||
@export_range(0.0, 5.0, 0.05) var gatherable_anchor_radius := 0.0
|
||||
|
||||
|
||||
func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool:
|
||||
|
|
@ -74,5 +77,15 @@ func has_box_collision() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func gatherable_anchor_surface_radius() -> float:
|
||||
if gatherable_anchor_radius > 0.0:
|
||||
return gatherable_anchor_radius
|
||||
if has_cylinder_collision():
|
||||
return collision_radius
|
||||
if has_box_collision():
|
||||
return maxf(collision_box_size.x, collision_box_size.z) * 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
func is_procedural() -> bool:
|
||||
return procedural_group != &""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue