Add turning rivers and lake edge variation

This commit is contained in:
Alexander Sellite 2026-08-22 22:56:21 -04:00
parent f2b96f2c1f
commit 6fd1aa0388
53 changed files with 2039 additions and 386 deletions

View file

@ -136,17 +136,30 @@ func _validate_generated_region(
assert(maxi(
generator._river_feature_footprint_size.x,
generator._river_feature_footprint_size.y,
) >= 7)
) >= 4)
assert(mini(
generator._river_feature_footprint_size.x,
generator._river_feature_footprint_size.y,
) == 2)
) >= 4)
assert(generator._river_feature_flow_direction in [
Vector2i.RIGHT,
Vector2i.DOWN,
Vector2i.LEFT,
Vector2i.UP,
])
assert(generator._river_feature_outlet_direction in [
Vector2i.RIGHT,
Vector2i.DOWN,
Vector2i.LEFT,
Vector2i.UP,
])
assert(
generator._river_feature_flow_direction.x
* generator._river_feature_outlet_direction.x
+ generator._river_feature_flow_direction.y
* generator._river_feature_outlet_direction.y
== 0
)
assert(generator.elevated_cliff_coastal_feature_required)
assert(placements.size() == expected_chunk_count)
assert(placements[center_index].begins_with("chunk_spawn@"))
@ -226,6 +239,10 @@ func _validate_generated_region(
assert(_placement_count(placements, "chunk_0030") == 1)
assert(_placement_count(placements, "chunk_0031") == 1)
assert(_placement_count(placements, "chunk_0032") == 1)
assert(_placement_count(placements, "chunk_0033") == 1)
assert(_placement_count(placements, "chunk_0034") == 1)
assert(_placement_count(placements, "chunk_0035") == 1)
assert(_placement_count(placements, "chunk_0036") == 1)
assert(
_placement_count(placements, "chunk_0014")
== (2 if has_secondary_elevation else 0)
@ -239,12 +256,15 @@ func _validate_generated_region(
assert(_placement_count(placements, "chunk_0022") == 4)
assert(_placement_count(placements, "chunk_0023") == 4)
var lake_edge_count := _placement_count(placements, "chunk_0017")
var lake_edge_variant_count := _placement_count(placements, "chunk_0037")
var lake_fill_count := _placement_count(placements, "chunk_0019")
var lake_narrow_count := _placement_count(placements, "chunk_0021")
var lake_size := generator._lake_feature_footprint_size.x
assert(lake_size in [7, 8])
assert(lake_fill_count == (lake_size - 2) * (lake_size - 2))
assert(lake_edge_count == (lake_size - 6) * 4)
assert(lake_edge_count + lake_edge_variant_count == (lake_size - 6) * 4)
assert(lake_edge_count > 0)
assert(lake_edge_variant_count > 0)
assert(lake_narrow_count == 8)
assert(_placement_count(placements, "chunk_9999") == 0)
assert(_placement_count(placements, "chunk_9998") == 0)
@ -257,13 +277,14 @@ func _validate_generated_region(
== (1 if has_secondary_elevation else 0)
)
assert(_placement_count(placements, "chunk_9995") == 0)
var river_length := maxi(
generator._river_feature_footprint_size.x,
generator._river_feature_footprint_size.y,
var river_coordinate_count := generator._river_feature_coordinates.size()
var river_edge_count := (
_placement_count(placements, "chunk_0024")
+ _placement_count(placements, "chunk_0025")
)
assert(river_length in [7, 8])
assert(_placement_count(placements, "chunk_0024") == river_length - 2)
assert(_placement_count(placements, "chunk_0025") == river_length - 2)
assert(river_coordinate_count >= 12)
assert(generator._river_feature_placements.size() == river_coordinate_count)
assert(river_edge_count == river_coordinate_count - 8)
var river_source_coordinates := generator._river_source_coordinates(
generator._river_feature_origin,
generator._river_feature_footprint_size,
@ -297,7 +318,7 @@ func _validate_generated_region(
assert(
not generator._coordinate_is_inside_grid(
outlet_coordinate
+ generator._river_feature_flow_direction
+ generator._river_feature_outlet_direction
)
)
assert(outlet_coordinates.size() == 2)
@ -379,7 +400,7 @@ func _validate_generated_region(
] = surface_polygon.size()
_validate_complete_coastline(generator)
assert(fresh_root.get_child_count() == fresh_placement_count)
assert(river_placement_count == river_length * 2)
assert(river_placement_count == river_coordinate_count)
assert(
polygon_sizes_by_coordinate.size()
== 20 + river_placement_count
@ -1025,19 +1046,48 @@ func _validate_decoration_transform(
prop.global_position + Vector3.DOWN * 8.0,
1,
)
var prop_body := prop.get_node_or_null(
"TrunkCollision",
) as CollisionObject3D
if prop_body != null:
query.exclude = [prop_body.get_rid()]
var excluded_colliders: Array[RID] = []
var decorations := region.get_node("Decorations") as Node3D
for decoration: Node in decorations.get_children():
var prop_body := decoration.get_node_or_null(
"TrunkCollision",
) as CollisionObject3D
if prop_body != null:
excluded_colliders.append(prop_body.get_rid())
query.exclude = excluded_colliders
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
# Stacked cliff walls can overhang a neighboring cell. Match the placement
# sampler by ignoring steep faces until the ray reaches walkable terrain.
while (
not hit.is_empty()
and absf((hit.get("normal", Vector3.ZERO) as Vector3).dot(Vector3.UP))
< 0.35
):
var steep_collider := hit.get("collider") as CollisionObject3D
assert(steep_collider != null)
excluded_colliders.append(steep_collider.get_rid())
query.exclude = excluded_colliders
hit = region.get_world_3d().direct_space_state.intersect_ray(query)
assert(
not hit.is_empty(),
"No terrain collision below %s (%s) at %s."
% [prop.name, definition.stable_id, prop.global_position],
)
var ground_position: Vector3 = hit["position"]
assert(absf(ground_position.y - prop.global_position.y) <= 0.01)
assert(
absf(ground_position.y - prop.global_position.y) <= 0.01,
"Terrain height %s does not match %s (%s) at %s."
% [
ground_position.y,
prop.name,
definition.stable_id,
"%s in chunk %s; hit %s" % [
prop.global_position,
prop.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)),
(hit.get("collider") as Node).get_path(),
],
],
)
func _validate_tree_gatherable_anchors(

View file

@ -37,6 +37,11 @@ const EXPECTED_IDS: Array[String] = [
"chunk_0030",
"chunk_0031",
"chunk_0032",
"chunk_0033",
"chunk_0034",
"chunk_0035",
"chunk_0036",
"chunk_0037",
"chunk_9999",
"chunk_9998",
"chunk_9997",
@ -88,6 +93,11 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0030": 4,
&"chunk_0031": 4,
&"chunk_0032": 4,
&"chunk_0033": 4,
&"chunk_0034": 4,
&"chunk_0035": 4,
&"chunk_0036": 4,
&"chunk_0037": 4,
&"chunk_9999": 4,
&"chunk_9998": 4,
&"chunk_9997": 4,
@ -144,6 +154,7 @@ func _validate_catalog() -> void:
var cliff_beach_transition_left := CATALOG.definition_for_id(&"chunk_9996")
var stream_variant := CATALOG.definition_for_id(&"chunk_0016")
var lake_edge := CATALOG.definition_for_id(&"chunk_0017")
var lake_edge_variant := CATALOG.definition_for_id(&"chunk_0037")
var lake_corner := CATALOG.definition_for_id(&"chunk_0018")
var lake_fill := CATALOG.definition_for_id(&"chunk_0019")
var lake_diagonal := CATALOG.definition_for_id(&"chunk_0020")
@ -156,6 +167,10 @@ func _validate_catalog() -> void:
var river_source_left := CATALOG.definition_for_id(&"chunk_0030")
var river_outlet_right := CATALOG.definition_for_id(&"chunk_0031")
var river_outlet_left := CATALOG.definition_for_id(&"chunk_0032")
var river_bend_c := CATALOG.definition_for_id(&"chunk_0033")
var river_bend_b := CATALOG.definition_for_id(&"chunk_0034")
var river_bend_a := CATALOG.definition_for_id(&"chunk_0035")
var river_bend_d := CATALOG.definition_for_id(&"chunk_0036")
var river_cap := CATALOG.definition_for_id(&"chunk_9995")
var cliff_top := CATALOG.definition_for_id(&"chunk_0009")
var cliff_corner := CATALOG.definition_for_id(&"chunk_0010")
@ -184,6 +199,7 @@ func _validate_catalog() -> void:
and cliff_beach_transition_left != null
and stream_variant != null
and lake_edge != null
and lake_edge_variant != null
and lake_corner != null
and lake_fill != null
and lake_diagonal != null
@ -196,6 +212,10 @@ func _validate_catalog() -> void:
and river_source_left != null
and river_outlet_right != null
and river_outlet_left != null
and river_bend_a != null
and river_bend_b != null
and river_bend_c != null
and river_bend_d != null
and river_cap != null
and cliff_top != null
and cliff_corner != null
@ -226,6 +246,7 @@ func _validate_catalog() -> void:
and cliff_beach_transition_left != null
and stream_variant != null
and lake_edge != null
and lake_edge_variant != null
and lake_corner != null
and lake_fill != null
and lake_diagonal != null
@ -238,6 +259,10 @@ func _validate_catalog() -> void:
and river_source_left != null
and river_outlet_right != null
and river_outlet_left != null
and river_bend_a != null
and river_bend_b != null
and river_bend_c != null
and river_bend_d != null
and river_cap != null
and cliff_top != null
and cliff_corner != null
@ -431,6 +456,7 @@ func _validate_catalog() -> void:
_check(
(
not lake_edge.participates_in_base_solver
and not lake_edge_variant.participates_in_base_solver
and not lake_corner.participates_in_base_solver
and not lake_fill.participates_in_base_solver
and not lake_diagonal.participates_in_base_solver
@ -439,6 +465,14 @@ func _validate_catalog() -> void:
and not lake_narrow_to_regular.participates_in_base_solver
and lake_edge.water_inlet_edges == lake_edge.water_outlet_edges
and lake_edge.water_inlet_edges == 7
and lake_edge_variant.water_inlet_edges
== lake_edge.water_inlet_edges
and lake_edge_variant.water_outlet_edges
== lake_edge.water_outlet_edges
and lake_edge_variant.water_surface_size
== lake_edge.water_surface_size
and lake_edge_variant.water_surface_offset
== lake_edge.water_surface_offset
and lake_corner.water_inlet_edges
== lake_corner.water_outlet_edges
and lake_corner.water_inlet_edges == 6
@ -461,6 +495,7 @@ func _validate_catalog() -> void:
and lake_narrow_to_regular.water_outlet_edges == 14
and lake_narrow_to_regular.water_surface_polygon.size() == 5
and "lake" in lake_edge.tags
and "lake" in lake_edge_variant.tags
and "lake" in lake_corner.tags
and "lake" in lake_fill.tags
and "lake" in lake_diagonal.tags
@ -478,6 +513,10 @@ func _validate_catalog() -> void:
and not river_source_left.participates_in_base_solver
and not river_outlet_right.participates_in_base_solver
and not river_outlet_left.participates_in_base_solver
and not river_bend_a.participates_in_base_solver
and not river_bend_b.participates_in_base_solver
and not river_bend_c.participates_in_base_solver
and not river_bend_d.participates_in_base_solver
and not river_cap.participates_in_base_solver
and river_edge.water_inlet_edges == 14
and river_edge.water_outlet_edges == 14
@ -491,6 +530,16 @@ func _validate_catalog() -> void:
and river_outlet_right.water_outlet_edges == 14
and river_outlet_left.water_inlet_edges == 11
and river_outlet_left.water_outlet_edges == 11
and river_bend_a.water_inlet_edges == 14
and river_bend_a.water_outlet_edges == 14
and river_bend_b.water_inlet_edges == 12
and river_bend_b.water_outlet_edges == 12
and river_bend_c.water_inlet_edges == 15
and river_bend_c.water_outlet_edges == 15
and river_bend_c.continuous_water_seam_edges == 15
and river_bend_d.water_inlet_edges == 13
and river_bend_d.water_outlet_edges == 13
and river_bend_d.continuous_water_seam_edges == 1
and river_cap.water_inlet_edges == 6
and river_cap.water_outlet_edges == 6
and river_edge.water_surface_polygon.size() == 13
@ -509,11 +558,15 @@ func _validate_catalog() -> void:
and "river_source_left" in river_source_left.tags
and "river_outlet_right" in river_outlet_right.tags
and "river_outlet_left" in river_outlet_left.tags
and "river_bend_a" in river_bend_a.tags
and "river_bend_b" in river_bend_b.tags
and "river_bend_c" in river_bend_c.tags
and "river_bend_d" in river_bend_d.tags
and "river" in river_cap.tags
),
(
"The river banks, cliff source, ocean outlet, and legacy caps "
+ "must remain connected freshwater chunks."
"The river banks, bend, cliff source, ocean outlet, and legacy "
+ "caps must remain connected freshwater chunks."
),
)
for cliff_definition: TerrainChunkDefinition in [
@ -1307,6 +1360,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var lake_regular_to_narrow := _find_variant(generator, &"chunk_0022", 0)
var lake_narrow_to_regular := _find_variant(generator, &"chunk_0023", 0)
var lake_regular_top := _find_variant(generator, &"chunk_0017", 3)
var lake_regular_variant_top := _find_variant(generator, &"chunk_0037", 3)
_check(
(
lake_diagonal_northwest != null
@ -1316,6 +1370,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and lake_regular_to_narrow != null
and lake_narrow_to_regular != null
and lake_regular_top != null
and lake_regular_variant_top != null
),
"Every authored diagonal lake perimeter variant must be available.",
)
@ -1327,7 +1382,13 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and lake_regular_to_narrow != null
and lake_narrow_to_regular != null
and lake_regular_top != null
and lake_regular_variant_top != null
):
_check(
lake_regular_variant_top.topology_signature(0.001)
== lake_regular_top.topology_signature(0.001),
"Both regular lake edge visuals must expose identical seams.",
)
_check(
generator._edges_are_compatible(
lake_diagonal_northwest,
@ -1398,11 +1459,36 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var river_edge_top := _find_variant(generator, &"chunk_0024", 0)
var river_edge_variant_top := _find_variant(generator, &"chunk_0025", 0)
var river_edge_variant_bottom := _find_variant(generator, &"chunk_0025", 2)
var river_edge_vertical_left := _find_variant(generator, &"chunk_0024", 1)
var river_edge_vertical_right := _find_variant(generator, &"chunk_0025", 3)
var river_source_right := _find_variant(generator, &"chunk_0029", 0)
var river_source_left := _find_variant(generator, &"chunk_0030", 0)
var river_outlet_right := _find_variant(generator, &"chunk_0031", 0)
var river_outlet_left := _find_variant(generator, &"chunk_0032", 0)
var river_bend_a := _find_variant(generator, &"chunk_0035", 0)
var river_bend_b := _find_variant(generator, &"chunk_0034", 0)
var river_bend_c := _find_variant(generator, &"chunk_0033", 2)
var river_bend_d := _find_variant(generator, &"chunk_0036", 0)
var beach_east := _find_variant(generator, &"chunk_0002", 0)
var canonical_bend_specs := generator._river_bend_placement_specs(
Vector2i(5, 5),
0,
)
var rotated_bend_specs := generator._river_bend_placement_specs(
Vector2i(5, 5),
1,
)
_check(
(
canonical_bend_specs.size() == 4
and canonical_bend_specs[2]["id"] == &"chunk_0033"
and int(canonical_bend_specs[2]["turns"]) == 2
and rotated_bend_specs.size() == 4
and rotated_bend_specs[2]["id"] == &"chunk_0033"
and int(rotated_bend_specs[2]["turns"]) == 3
),
"River bend C must retain its authored half-turn at every bend rotation.",
)
_check(
(
river_cap_northwest != null
@ -1412,15 +1498,21 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and river_edge_top != null
and river_edge_variant_top != null
and river_edge_variant_bottom != null
and river_edge_vertical_left != null
and river_edge_vertical_right != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
and river_outlet_left != null
and river_bend_a != null
and river_bend_b != null
and river_bend_c != null
and river_bend_d != null
and beach_east != null
),
(
"Every authored river-bank, source, outlet, and legacy-cap variant "
+ "must be available."
"Every authored river-bank, bend, source, outlet, and legacy-cap "
+ "variant must be available."
),
)
if (
@ -1431,10 +1523,16 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and river_edge_top != null
and river_edge_variant_top != null
and river_edge_variant_bottom != null
and river_edge_vertical_left != null
and river_edge_vertical_right != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
and river_outlet_left != null
and river_bend_a != null
and river_bend_b != null
and river_bend_c != null
and river_bend_d != null
and beach_east != null
):
_check(
@ -1458,6 +1556,60 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
),
"The paired cliff source must open directly into both river banks.",
)
_check(
generator._edges_are_compatible(
river_bend_a,
TerrainChunkTopology.Edge.EAST,
river_bend_b,
TerrainChunkTopology.Edge.WEST,
)
and generator._edges_are_compatible(
river_bend_a,
TerrainChunkTopology.Edge.SOUTH,
river_bend_c,
TerrainChunkTopology.Edge.NORTH,
)
and generator._edges_are_compatible(
river_bend_b,
TerrainChunkTopology.Edge.SOUTH,
river_bend_d,
TerrainChunkTopology.Edge.NORTH,
)
and generator._edges_are_compatible(
river_bend_c,
TerrainChunkTopology.Edge.EAST,
river_bend_d,
TerrainChunkTopology.Edge.WEST,
),
"The four authored river-bend quadrants must form one continuous turn.",
)
_check(
generator._edges_are_compatible(
river_edge_top,
TerrainChunkTopology.Edge.EAST,
river_bend_a,
TerrainChunkTopology.Edge.WEST,
)
and generator._edges_are_compatible(
river_edge_variant_bottom,
TerrainChunkTopology.Edge.EAST,
river_bend_c,
TerrainChunkTopology.Edge.WEST,
)
and generator._edges_are_compatible(
river_bend_c,
TerrainChunkTopology.Edge.SOUTH,
river_edge_vertical_left,
TerrainChunkTopology.Edge.NORTH,
)
and generator._edges_are_compatible(
river_bend_d,
TerrainChunkTopology.Edge.SOUTH,
river_edge_vertical_right,
TerrainChunkTopology.Edge.NORTH,
),
"The bend must connect both incoming and outgoing authored river banks.",
)
_check(
generator._edges_are_compatible(
river_edge_top,
@ -1598,8 +1750,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)) == 150,
"The current catalog must expose all 150 authored rotations.",
int(summary.get("variant_count", 0)) == 170,
"The current catalog must expose all 170 authored rotations.",
)
_check(
int(summary.get("solver_variant_count", 0))