Add layered inland cliff generation

This commit is contained in:
Alexander Sellite 2026-08-20 17:53:38 -04:00
parent 2710544070
commit 86372a652e
66 changed files with 1552 additions and 79 deletions

View file

@ -82,8 +82,14 @@ func _validate_generated_region(
generator: TerrainChunkGenerator, generator: TerrainChunkGenerator,
) -> void: ) -> void:
var placements := generator.placement_keys() var placements := generator.placement_keys()
assert(placements.size() == 49) var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
assert(placements[24].begins_with("chunk_spawn@")) var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
)
var center_index := center.y * generator.grid_size.x + center.x
assert(placements.size() == expected_chunk_count)
assert(placements[center_index].begins_with("chunk_spawn@"))
assert(_placement_count(placements, "chunk_spawn") == 1) assert(_placement_count(placements, "chunk_spawn") == 1)
assert(_placement_count(placements, "chunk_0001") >= 1) assert(_placement_count(placements, "chunk_0001") >= 1)
assert(_placement_count(placements, "chunk_0002") >= 1) assert(_placement_count(placements, "chunk_0002") >= 1)
@ -92,8 +98,24 @@ func _validate_generated_region(
assert(_placement_count(placements, "chunk_0005") >= 1) assert(_placement_count(placements, "chunk_0005") >= 1)
assert(_placement_count(placements, "chunk_0006") >= 1) assert(_placement_count(placements, "chunk_0006") >= 1)
assert(_placement_count(placements, "chunk_0007") >= 1) assert(_placement_count(placements, "chunk_0007") >= 1)
assert(generator.get_generated_chunks_root().get_child_count() == 49) assert(_placement_count(placements, "chunk_0008") >= 1)
assert(generator.get_primary_terrain_meshes().size() == 49) 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
)
assert(
generator.get_primary_terrain_meshes().size()
== expected_chunk_count
)
_validate_elevated_cliff_feature(generator)
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
@ -111,6 +133,7 @@ func _validate_generated_region(
var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
assert(ocean != null and fresh_root != null) assert(ocean != null and fresh_root != null)
assert(ocean.water_type == WaterType.Type.SALT_WATER) assert(ocean.water_type == WaterType.Type.SALT_WATER)
assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT))
var fresh_placement_count := 0 var fresh_placement_count := 0
for record: Dictionary in generator.placement_records(): for record: Dictionary in generator.placement_records():
var tags: PackedStringArray = record.get("tags", PackedStringArray()) var tags: PackedStringArray = record.get("tags", PackedStringArray())
@ -123,6 +146,7 @@ func _validate_generated_region(
var fresh := child as WaterBodyAuthoring var fresh := child as WaterBodyAuthoring
assert(fresh != null) assert(fresh != null)
assert(fresh.water_type == WaterType.Type.FRESH_WATER) assert(fresh.water_type == WaterType.Type.FRESH_WATER)
assert(is_equal_approx(fresh.position.y, GeneratedWorldRegion.WATER_HEIGHT))
assert(fresh.visible) assert(fresh.visible)
assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0) assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0)
assert(not fresh.visual_surface_enabled) assert(not fresh.visual_surface_enabled)
@ -155,13 +179,13 @@ func _validate_generated_region(
&"terrain_chunk_coordinate", &"terrain_chunk_coordinate",
Vector2i.ZERO, Vector2i.ZERO,
) )
var center := Vector2i( var spawn_coordinate := Vector2i(
generator.grid_size.x / 2, generator.grid_size.x / 2,
generator.grid_size.y / 2, generator.grid_size.y / 2,
) )
var spawn_distance := ( var spawn_distance := (
absi(coordinate.x - center.x) absi(coordinate.x - spawn_coordinate.x)
+ absi(coordinate.y - center.y) + absi(coordinate.y - spawn_coordinate.y)
) )
assert(spawn_distance >= definition.minimum_spawn_chunk_distance) assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
_validate_decoration_transform( _validate_decoration_transform(
@ -169,6 +193,13 @@ func _validate_generated_region(
child as Node3D, child as Node3D,
definition, definition,
) )
if prop_id in [&"prop_tree_1", &"prop_tree_2", &"prop_tree_3"]:
assert(
_has_material_variant_override(
child.get_node_or_null("Visual"),
definition.material_variants,
)
)
assert(_decoration_group_count(region, &"grass_tree") > 0) assert(_decoration_group_count(region, &"grass_tree") > 0)
assert(_decoration_group_count(region, &"sand_tree") > 0) assert(_decoration_group_count(region, &"sand_tree") > 0)
assert( assert(
@ -187,6 +218,69 @@ func _validate_generated_region(
) )
func _validate_elevated_cliff_feature(
generator: TerrainChunkGenerator,
) -> void:
var elevated_coordinates: Dictionary[Vector2i, StringName] = {}
var top_coordinate := Vector2i(-1, -1)
for record: Dictionary in generator.placement_records():
var stable_id: StringName = record.get("stable_id", &"")
if stable_id not in [
&"chunk_0009",
&"chunk_0010",
&"chunk_0011",
&"chunk_0012",
]:
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)
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):
assert(
elevated_coordinates.has(
top_coordinate + Vector2i(column_offset, row_offset)
)
)
var generated := generator.get_generated_chunks_root()
assert(generated != null)
var layered_count := 0
for chunk_root: Node in generated.get_children():
var stable_id := StringName(
chunk_root.get_meta(&"terrain_chunk_id", &"")
)
if stable_id not in [
&"chunk_0009",
&"chunk_0010",
&"chunk_0011",
&"chunk_0012",
]:
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 base_mesh := TerrainChunkAnalyzer.find_primary_mesh(
base_layer,
&"chunk_0000",
)
var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh(
overlay,
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)
func _validate_ocean_facing_record( func _validate_ocean_facing_record(
record: Dictionary, record: Dictionary,
grid_size: Vector2i, grid_size: Vector2i,
@ -224,6 +318,23 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
assert(instance.position.is_zero_approx()) assert(instance.position.is_zero_approx())
assert(_find_mesh_instance(instance) != null) assert(_find_mesh_instance(instance) != null)
instance.free() instance.free()
var mushroom := catalog.definition_for_id(&"prop_mushroom")
assert(mushroom != null and mushroom.has_collision())
assert(mushroom.visual_offset.y < 0.0)
for tree_id: StringName in [
&"prop_tree_1",
&"prop_tree_2",
&"prop_tree_3",
]:
var tree := catalog.definition_for_id(tree_id)
assert(tree != null)
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
assert(tree.material_variants.size() >= 3)
assert(not tree.variant_material_slot_names.is_empty())
var pine := catalog.definition_for_id(&"prop_pine")
assert(pine != null)
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
assert(pine.material_variants.is_empty())
func _validate_biome_catalog( func _validate_biome_catalog(
@ -276,7 +387,16 @@ func _validate_decoration_transform(
var collision := prop.get_node_or_null( var collision := prop.get_node_or_null(
"TrunkCollision/CollisionShape" "TrunkCollision/CollisionShape"
) as CollisionShape3D ) as CollisionShape3D
assert(visual_root != null and visual_root.position.is_zero_approx()) assert(visual_root != null)
assert(visual_root.position.is_equal_approx(definition.visual_offset))
var visual_scale := float(
prop.get_meta(&"terrain_prop_visual_scale", 1.0)
)
assert(visual_scale >= definition.minimum_visual_scale - 0.001)
assert(visual_scale <= definition.maximum_visual_scale + 0.001)
assert(
visual_root.scale.is_equal_approx(Vector3.ONE * visual_scale)
)
assert(visual != null and visual.mesh != null) assert(visual != null and visual.mesh != null)
if definition.has_collision(): if definition.has_collision():
assert(collision != null) assert(collision != null)
@ -319,6 +439,26 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
return null return null
func _has_material_variant_override(
root_node: Node,
variants: Array[Material],
) -> bool:
if root_node == null:
return false
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var override := mesh_instance.get_surface_override_material(
surface_index
)
if override != null and override in variants:
return true
for child: Node in root_node.get_children():
if _has_material_variant_override(child, variants):
return true
return false
func _validate_authored_chunk_surfaces( func _validate_authored_chunk_surfaces(
region: GeneratedWorldRegion, region: GeneratedWorldRegion,
generator: TerrainChunkGenerator, generator: TerrainChunkGenerator,
@ -329,6 +469,7 @@ func _validate_authored_chunk_surfaces(
var found_grass_ocean_edge := false var found_grass_ocean_edge := false
var found_grass_beach_transition := false var found_grass_beach_transition := false
var found_grass_ocean_corner := false var found_grass_ocean_corner := false
var found_beach_ocean_corner := false
for chunk_root: Node in generated.get_children(): for chunk_root: Node in generated.get_children():
if chunk_root.name.begins_with("chunk_0002r"): if chunk_root.name.begins_with("chunk_0002r"):
var beach := chunk_root.find_child( var beach := chunk_root.find_child(
@ -383,11 +524,20 @@ func _validate_authored_chunk_surfaces(
assert("grass_lite" in corner_materials) assert("grass_lite" in corner_materials)
assert("cliff_wall" in corner_materials) assert("cliff_wall" in corner_materials)
found_grass_ocean_corner = true found_grass_ocean_corner = true
elif chunk_root.name.begins_with("chunk_0008r"):
var beach_ocean_corner := chunk_root.find_child(
"chunk_0008", true, false
) as MeshInstance3D
assert(beach_ocean_corner != null and beach_ocean_corner.mesh != null)
var beach_corner_materials := _material_names(beach_ocean_corner)
assert("sand" in beach_corner_materials)
found_beach_ocean_corner = true
assert(found_beach) assert(found_beach)
assert(found_pond) assert(found_pond)
assert(found_grass_ocean_edge) assert(found_grass_ocean_edge)
assert(found_grass_beach_transition) assert(found_grass_beach_transition)
assert(found_grass_ocean_corner) assert(found_grass_ocean_corner)
assert(found_beach_ocean_corner)
func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray: func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:

View file

@ -119,6 +119,7 @@ func _make_generator() -> TerrainChunkGenerator:
"chunk_0005", "chunk_0005",
"chunk_0006", "chunk_0006",
"chunk_0007", "chunk_0007",
"chunk_0008",
] ]
) )
return generator return generator

View file

@ -21,12 +21,13 @@ shadow_enabled = true
[node name="TerrainChunkGenerator" type="Node3D" parent="."] [node name="TerrainChunkGenerator" type="Node3D" parent="."]
script = ExtResource("2_generator") script = ExtResource("2_generator")
catalog = ExtResource("3_catalog") catalog = ExtResource("3_catalog")
grid_size = Vector2i(7, 7) grid_size = Vector2i(11, 9)
generation_seed = 13001 generation_seed = 13001
build_collision = true build_collision = true
show_chunk_labels = false show_chunk_labels = false
force_center_chunk_id = &"chunk_spawn" 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") required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008")
elevated_cliff_feature_enabled = true
[node name="Player" parent="." instance=ExtResource("4_player")] [node name="Player" parent="." instance=ExtResource("4_player")]
position = Vector3(0, 0.3, 0) position = Vector3(0, 0.3, 0)

View file

@ -12,6 +12,11 @@ const EXPECTED_IDS: Array[String] = [
"chunk_0005", "chunk_0005",
"chunk_0006", "chunk_0006",
"chunk_0007", "chunk_0007",
"chunk_0008",
"chunk_0009",
"chunk_0010",
"chunk_0011",
"chunk_0012",
"chunk_spawn", "chunk_spawn",
] ]
const REQUIRED_IDS: Array[String] = [ const REQUIRED_IDS: Array[String] = [
@ -23,10 +28,11 @@ const REQUIRED_IDS: Array[String] = [
"chunk_0005", "chunk_0005",
"chunk_0006", "chunk_0006",
"chunk_0007", "chunk_0007",
"chunk_0008",
] ]
const TEST_SEED := 13001 const TEST_SEED := 13001
const CONNECTOR_STRESS_SEED := 287 const CONNECTOR_STRESS_SEED := 287
const EXPECTED_SOLVER_VARIANT_COUNT := 27 const EXPECTED_SOLVER_VARIANT_COUNT := 31
const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = { const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0000": 4, &"chunk_0000": 4,
&"chunk_0001": 4, &"chunk_0001": 4,
@ -36,6 +42,11 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0005": 4, &"chunk_0005": 4,
&"chunk_0006": 4, &"chunk_0006": 4,
&"chunk_0007": 4, &"chunk_0007": 4,
&"chunk_0008": 4,
&"chunk_0009": 4,
&"chunk_0010": 4,
&"chunk_0011": 4,
&"chunk_0012": 4,
&"chunk_spawn": 1, &"chunk_spawn": 1,
} }
@ -75,6 +86,11 @@ func _validate_catalog() -> void:
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005") var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006") var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007") var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
var cliff_top := CATALOG.definition_for_id(&"chunk_0009")
var cliff_corner := CATALOG.definition_for_id(&"chunk_0010")
var cliff_edge := CATALOG.definition_for_id(&"chunk_0011")
var cliff_ramp := CATALOG.definition_for_id(&"chunk_0012")
var spawn := CATALOG.definition_for_id(&"chunk_spawn") var spawn := CATALOG.definition_for_id(&"chunk_spawn")
_check( _check(
( (
@ -84,6 +100,11 @@ func _validate_catalog() -> void:
and grass_ocean_edge != null and grass_ocean_edge != null
and grass_beach_transition != null and grass_beach_transition != null
and grass_ocean_corner != null and grass_ocean_corner != null
and beach_ocean_corner != null
and cliff_top != null
and cliff_corner != null
and cliff_edge != null
and cliff_ramp != null
and spawn != null and spawn != null
), ),
"The authored biome-rule definitions must be available.", "The authored biome-rule definitions must be available.",
@ -95,6 +116,11 @@ func _validate_catalog() -> void:
and grass_ocean_edge != null and grass_ocean_edge != null
and grass_beach_transition != null and grass_beach_transition != null
and grass_ocean_corner != null and grass_ocean_corner != null
and beach_ocean_corner != null
and cliff_top != null
and cliff_corner != null
and cliff_edge != null
and cliff_ramp != null
and spawn != null and spawn != null
): ):
_check( _check(
@ -193,6 +219,45 @@ func _validate_catalog() -> void:
), ),
"Both corner land seams must accept straight grass ocean edges.", "Both corner land seams must accept straight grass ocean edges.",
) )
_check(
beach_ocean_corner.ocean_facing_edges
== (
(1 << int(TerrainChunkTopology.Edge.EAST))
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
),
"The authored beach corner must expose east and south to ocean.",
)
_check(
beach_ocean_corner.minimum_required_neighbors == 2
and (
"beach_ocean_edge"
in beach_ocean_corner.required_neighbor_tags
),
"The beach corner must join two straight beach ocean edges.",
)
for cliff_definition: TerrainChunkDefinition in [
cliff_top,
cliff_corner,
cliff_edge,
cliff_ramp,
]:
_check(
cliff_definition.overlay_only
and cliff_definition.must_be_interior
and cliff_definition.base_layer_scene != null
and cliff_definition.base_layer_mesh_name == &"chunk_0000",
"Every second-tier piece must overlay level-one grass inland.",
)
_check(
cliff_top.minimum_required_neighbors == 4
and "elevation_2" in cliff_top.required_neighbor_tags,
"The second-tier top must be enclosed on all four sides.",
)
_check(
"ramp" in cliff_ramp.tags
and cliff_ramp.maximum_placements > 0,
"The second-tier ramp must remain available without being required.",
)
func _validate_profiles() -> void: func _validate_profiles() -> void:
@ -443,11 +508,13 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005") var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006") var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007") var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
_check( _check(
( (
grass_ocean_edge != null grass_ocean_edge != null
and grass_beach_transition != null and grass_beach_transition != null
and grass_ocean_corner != null and grass_ocean_corner != null
and beach_ocean_corner != null
), ),
"The authored coastline additions must be available.", "The authored coastline additions must be available.",
) )
@ -455,6 +522,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
grass_ocean_edge == null grass_ocean_edge == null
or grass_beach_transition == null or grass_beach_transition == null
or grass_ocean_corner == null or grass_ocean_corner == null
or beach_ocean_corner == null
): ):
return return
var beach_zero := _find_variant(generator, &"chunk_0002", 0) var beach_zero := _find_variant(generator, &"chunk_0002", 0)
@ -462,12 +530,14 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var grass_edge_south := _find_variant(generator, &"chunk_0005", 3) var grass_edge_south := _find_variant(generator, &"chunk_0005", 3)
var transition_zero := _find_variant(generator, &"chunk_0006", 0) var transition_zero := _find_variant(generator, &"chunk_0006", 0)
var corner_zero := _find_variant(generator, &"chunk_0007", 0) var corner_zero := _find_variant(generator, &"chunk_0007", 0)
var beach_corner_zero := _find_variant(generator, &"chunk_0008", 0)
_check( _check(
beach_zero != null beach_zero != null
and grass_edge_zero != null and grass_edge_zero != null
and grass_edge_south != null and grass_edge_south != null
and transition_zero != null and transition_zero != null
and corner_zero != null, and corner_zero != null
and beach_corner_zero != null,
"The unrotated coastline variants must be available.", "The unrotated coastline variants must be available.",
) )
if ( if (
@ -476,6 +546,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and grass_edge_south != null and grass_edge_south != null
and transition_zero != null and transition_zero != null
and corner_zero != null and corner_zero != null
and beach_corner_zero != null
): ):
_check( _check(
grass_edge_zero.profile( grass_edge_zero.profile(
@ -522,6 +593,24 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
), ),
"The corner's west side must join a straight grass ocean edge.", "The corner's west side must join a straight grass ocean edge.",
) )
_check(
beach_corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
generator.edge_match_tolerance,
),
"The beach corner's north side must join a straight beach.",
)
var beach_south := _find_variant(generator, &"chunk_0002", 3)
_check(
beach_south != null
and beach_corner_zero.profile(
TerrainChunkTopology.Edge.WEST
).matches(
beach_south.profile(TerrainChunkTopology.Edge.EAST),
generator.edge_match_tolerance,
),
"The beach corner's west side must join a rotated straight beach.",
)
var flat_grass := _find_variant(generator, &"chunk_0000", 0) var flat_grass := _find_variant(generator, &"chunk_0000", 0)
var flat_sand := _find_variant(generator, &"chunk_0001", 0) var flat_sand := _find_variant(generator, &"chunk_0001", 0)
_check( _check(
@ -560,7 +649,12 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
), ),
"No transition rotation may accept sand behind it.", "No transition rotation may accept sand behind it.",
) )
for stable_id: StringName in [&"chunk_0005", &"chunk_0006", &"chunk_0007"]: for stable_id: StringName in [
&"chunk_0005",
&"chunk_0006",
&"chunk_0007",
&"chunk_0008",
]:
for quarter_turns: int in 4: for quarter_turns: int in 4:
var coast_variant := _find_variant( var coast_variant := _find_variant(
generator, generator,
@ -580,11 +674,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
), ),
"%s must never place its ocean edge inside the map." % stable_id, "%s must never place its ocean edge inside the map." % stable_id,
) )
if stable_id != &"chunk_0007": if stable_id != &"chunk_0007" and stable_id != &"chunk_0008":
continue continue
var boundary_coordinate := Vector2i(3, 3) var boundary_coordinate := Vector2i(3, 3)
var ocean_edges := coast_variant.rotated_edge_mask( var ocean_edges := coast_variant.rotated_edge_mask(
grass_ocean_corner.ocean_facing_edges (
grass_ocean_corner.ocean_facing_edges
if stable_id == &"chunk_0007"
else beach_ocean_corner.ocean_facing_edges
)
) )
for edge_value: int in TerrainChunkTopology.Edge.values(): for edge_value: int in TerrainChunkTopology.Edge.values():
if (ocean_edges & (1 << edge_value)) == 0: if (ocean_edges & (1 << edge_value)) == 0:
@ -603,15 +701,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
coast_variant, coast_variant,
boundary_coordinate, boundary_coordinate,
), ),
"Each grass corner rotation must fit its matching map corner.", "Each coast corner rotation must fit its matching map corner.",
) )
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void: func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
var summary := generator._build_summary() var summary := generator._build_summary()
_check( _check(
int(summary.get("variant_count", 0)) == 33, int(summary.get("variant_count", 0)) == 53,
"The current catalog must expose all 33 authored rotations.", "The current catalog must expose all 53 authored rotations.",
) )
_check( _check(
int(summary.get("solver_variant_count", 0)) int(summary.get("solver_variant_count", 0))
@ -811,6 +909,27 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
straight_edge_neighbors == 2, straight_edge_neighbors == 2,
"Every grass corner must join two straight grass ocean edges.", "Every grass corner must join two straight grass ocean edges.",
) )
if placement.definition.stable_id == &"chunk_0008":
var straight_beach_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
continue
var neighbor := generator._placements[
neighbor_coordinate.y * generator.grid_size.x
+ neighbor_coordinate.x
]
if neighbor.definition.stable_id == &"chunk_0002":
straight_beach_neighbors += 1
_check(
straight_beach_neighbors == 2,
"Every beach corner must join two straight beach ocean edges.",
)
if placement.definition.stable_id != &"chunk_0001": if placement.definition.stable_id != &"chunk_0001":
continue continue
var touches_coast := false var touches_coast := false

View file

@ -282,10 +282,16 @@ def _discover_chunks(tolerance: float) -> list[ChunkSource]:
# Coastline meshes may intentionally stop before the positive-X edge, # Coastline meshes may intentionally stop before the positive-X edge,
# leaving the surrounding ocean visible. Positive X is the canonical # leaving the surrounding ocean visible. Positive X is the canonical
# authored ocean direction; Godot supplies the quarter-turn variants. # authored ocean direction; Godot supplies the quarter-turn variants.
allow_open_east_edge = "ocean_edge" in label # Elevated inland cliff overlays intentionally stop at their downhill
# edge. The runtime places them over the existing flat grass chunk, so
# these omissions are not coastline openings or holes in the final cell.
layered_inland_cliff = "cliff_2" in label
allow_open_east_edge = "ocean_edge" in label or layered_inland_cliff
# Corner pieces additionally leave canonical negative Y open. Their # Corner pieces additionally leave canonical negative Y open. Their
# two marked ocean edges rotate together at runtime. # two marked ocean edges rotate together at runtime.
allow_open_south_edge = "ocean_edge_corner" in label allow_open_south_edge = (
"ocean_edge_corner" in label or layered_inland_cliff
)
bound_problems = _validate_bounds( bound_problems = _validate_bounds(
bounds, bounds,
tolerance, tolerance,

View file

@ -6,10 +6,11 @@
[sub_resource type="Resource" id="Rule_grass_tree"] [sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule") script = ExtResource("2_rule")
procedural_group = &"grass_tree" procedural_group = &"grass_tree"
placement_chance = 9000 placement_chance = 8200
adjacency_bonus = 1000 adjacency_bonus = 1200
maximum_density = 10000 maximum_density = 8500
minimum_placements = 2 minimum_placements = 2
placement_attempts_per_chunk = 3
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3") allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
[sub_resource type="Resource" id="Rule_grass_detail"] [sub_resource type="Resource" id="Rule_grass_detail"]

View file

@ -6,10 +6,11 @@
[sub_resource type="Resource" id="Rule_grass_tree"] [sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule") script = ExtResource("2_rule")
procedural_group = &"grass_tree" procedural_group = &"grass_tree"
placement_chance = 9000 placement_chance = 8200
adjacency_bonus = 1000 adjacency_bonus = 1200
maximum_density = 10000 maximum_density = 8500
minimum_placements = 2 minimum_placements = 2
placement_attempts_per_chunk = 3
allowed_prop_ids = PackedStringArray("prop_pine") allowed_prop_ids = PackedStringArray("prop_pine")
[sub_resource type="Resource" id="Rule_grass_detail"] [sub_resource type="Resource" id="Rule_grass_detail"]

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bm1teitx5ijxl"
path="res://.godot/imported/chunk_0008.glb-0b0a6712e5384c3f72c65e6ef1fc12c4.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0008.glb"
dest_files=["res://.godot/imported/chunk_0008.glb-0b0a6712e5384c3f72c65e6ef1fc12c4.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dj24j2d1u8712"
path.s3tc="res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "adfa66b8fe9da7f3450d3ddfd432b86a"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0008_sand.png"
dest_files=["res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.s3tc.ctex", "res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://hasempwso5gi"
path="res://.godot/imported/chunk_0009.glb-397f642bee3da14146c56fddf8e21369.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0009.glb"
dest_files=["res://.godot/imported/chunk_0009.glb-397f642bee3da14146c56fddf8e21369.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djjyaoih5vcnd"
path="res://.godot/imported/chunk_0009_grass_lite.png-5d49eb465b8a8f8739796ce13c646f7d.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0009_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0009_grass_lite.png-5d49eb465b8a8f8739796ce13c646f7d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b5tf5e1g5auu7"
path="res://.godot/imported/chunk_0010.glb-6ffffa0064f5930acc433afe5e4334e4.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0010.glb"
dest_files=["res://.godot/imported/chunk_0010.glb-6ffffa0064f5930acc433afe5e4334e4.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cwwraho3pj5aa"
path="res://.godot/imported/chunk_0010_cliff_wall.png-78830d69b22e3c32cc295e574d596c3a.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0010_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0010_cliff_wall.png-78830d69b22e3c32cc295e574d596c3a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://q7k2r4mb3bsr"
path="res://.godot/imported/chunk_0010_grass_lite.png-694b30e6e36cc16c1cf98b2f3823a0c2.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0010_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0010_grass_lite.png-694b30e6e36cc16c1cf98b2f3823a0c2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cym6ilrdikjcj"
path="res://.godot/imported/chunk_0011.glb-cda3ab701cf5e469b53437c043314d05.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0011.glb"
dest_files=["res://.godot/imported/chunk_0011.glb-cda3ab701cf5e469b53437c043314d05.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dggc1jbgenl11"
path="res://.godot/imported/chunk_0011_cliff_wall.png-9a4d64a03d75f23c2ef3614386484a4e.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0011_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0011_cliff_wall.png-9a4d64a03d75f23c2ef3614386484a4e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bhuhcaowvcyrk"
path="res://.godot/imported/chunk_0011_grass_lite.png-3a0a1a7ad2d55bd853b238730af91a7c.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0011_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0011_grass_lite.png-3a0a1a7ad2d55bd853b238730af91a7c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c4i32u0bdtix5"
path="res://.godot/imported/chunk_0012.glb-1c818ae539bef19d5ca4fbe3ace96887.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0012.glb"
dest_files=["res://.godot/imported/chunk_0012.glb-1c818ae539bef19d5ca4fbe3ace96887.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cf27tcolkmfog"
path="res://.godot/imported/chunk_0012_cliff_wall.png-3d27d4041790b1c3dbc838d6a0c94d40.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0012_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0012_cliff_wall.png-3d27d4041790b1c3dbc838d6a0c94d40.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3f1w276lmg7i"
path="res://.godot/imported/chunk_0012_dirt.png-5c3a2a10642f9d078ad1d1c6333d8264.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0012_dirt.png"
dest_files=["res://.godot/imported/chunk_0012_dirt.png-5c3a2a10642f9d078ad1d1c6333d8264.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lv8xn2moas2s"
path="res://.godot/imported/chunk_0012_grass_lite.png-ccfdc53acb9a967eb5d00e669258fc3c.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0012_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0012_grass_lite.png-ccfdc53acb9a967eb5d00e669258fc3c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -11,7 +11,7 @@ packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0002" primary_mesh_name = &"chunk_0002"
selection_weight = 0.75 selection_weight = 0.75
maximum_placements = 20 maximum_placements = 20
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope") tags = PackedStringArray("land", "walkable", "sand", "coast", "slope", "beach_ocean_edge")
allowed_neighbor_tags = PackedStringArray("sand", "grass") allowed_neighbor_tags = PackedStringArray("sand", "grass")
required_neighbor_tags = PackedStringArray("sand") required_neighbor_tags = PackedStringArray("sand")
minimum_required_neighbors = 1 minimum_required_neighbors = 1

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 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_0008.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0008"
label = "beach corner"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0008"
selection_weight = 0.2
maximum_placements = 4
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope", "corner")
allowed_neighbor_tags = PackedStringArray("sand", "coast")
north_allowed_neighbor_tags = PackedStringArray("beach_ocean_edge")
west_allowed_neighbor_tags = PackedStringArray("beach_ocean_edge")
required_neighbor_tags = PackedStringArray("beach_ocean_edge")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("coast")
prefers_map_boundary = true
ocean_facing_edges = 6

View file

@ -0,0 +1,23 @@
[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_0009.glb" id="2_scene"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0009"
label = "second-tier grass top"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0009"
base_layer_scene = ExtResource("3_base_grass")
base_layer_mesh_name = &"chunk_0000"
overlay_only = true
selection_weight = 0.22
maximum_placements = 8
tags = PackedStringArray("land", "walkable", "grass", "elevation_2", "elevation_2_top")
allowed_neighbor_tags = PackedStringArray("elevation_2")
required_neighbor_tags = PackedStringArray("elevation_2")
minimum_required_neighbors = 4
preferred_neighbor_tags = PackedStringArray("elevation_2_top")
must_be_interior = true

View file

@ -0,0 +1,27 @@
[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_0010.glb" id="2_scene"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0010"
label = "second-tier grass corner"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0010"
base_layer_scene = ExtResource("3_base_grass")
base_layer_mesh_name = &"chunk_0000"
overlay_only = true
selection_weight = 0.12
maximum_placements = 16
tags = PackedStringArray("land", "walkable", "grass", "cliff", "corner", "elevation_2", "elevation_2_border")
allowed_neighbor_tags = PackedStringArray("grass", "elevation_2")
north_allowed_neighbor_tags = PackedStringArray("elevation_2_border")
east_allowed_neighbor_tags = PackedStringArray("grass")
south_allowed_neighbor_tags = PackedStringArray("grass")
west_allowed_neighbor_tags = PackedStringArray("elevation_2_border")
required_neighbor_tags = PackedStringArray("elevation_2_border")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("elevation_2")
must_be_interior = true

View file

@ -0,0 +1,27 @@
[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_0011.glb" id="2_scene"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0011"
label = "second-tier grass edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0011"
base_layer_scene = ExtResource("3_base_grass")
base_layer_mesh_name = &"chunk_0000"
overlay_only = true
selection_weight = 0.28
maximum_placements = 32
tags = PackedStringArray("land", "walkable", "grass", "cliff", "edge", "elevation_2", "elevation_2_border")
allowed_neighbor_tags = PackedStringArray("grass", "elevation_2")
north_allowed_neighbor_tags = PackedStringArray("elevation_2_border")
east_allowed_neighbor_tags = PackedStringArray("grass")
south_allowed_neighbor_tags = PackedStringArray("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")
must_be_interior = true

View file

@ -0,0 +1,27 @@
[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_0012.glb" id="2_scene"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0012"
label = "second-tier grass ramp"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0012"
base_layer_scene = ExtResource("3_base_grass")
base_layer_mesh_name = &"chunk_0000"
overlay_only = true
selection_weight = 0.12
maximum_placements = 8
tags = PackedStringArray("land", "walkable", "grass", "cliff", "edge", "ramp", "elevation_2", "elevation_2_border")
allowed_neighbor_tags = PackedStringArray("grass", "elevation_2")
north_allowed_neighbor_tags = PackedStringArray("elevation_2_border")
east_allowed_neighbor_tags = PackedStringArray("grass")
south_allowed_neighbor_tags = PackedStringArray("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")
must_be_interior = true

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=12 format=3] [gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=17 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"] [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"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
@ -11,8 +11,13 @@
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0005.tres" id="9_grass_ocean_edge"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0005.tres" id="9_grass_ocean_edge"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0006.tres" id="10_grass_beach_transition"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0006.tres" id="10_grass_beach_transition"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0007.tres" id="11_grass_ocean_corner"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0007.tres" id="11_grass_ocean_corner"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0008.tres" id="12_beach_ocean_corner"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0009.tres" id="13_cliff_top"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0010.tres" id="14_cliff_corner"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0011.tres" id="15_cliff_edge"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0012.tres" id="16_cliff_ramp"]
[resource] [resource]
script = ExtResource("1_catalog") script = ExtResource("1_catalog")
chunk_size = 10.0 chunk_size = 10.0
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("8_spawn")]) definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("8_spawn")])

View file

@ -78,6 +78,11 @@ func _ready() -> void:
func generate_world(seed: int) -> bool: func generate_world(seed: int) -> bool:
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED: if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
return false return false
if (
seed == _current_seed
and _generator.get_generated_chunks_root() != null
):
return true
_current_seed = seed _current_seed = seed
_generator.generation_seed = seed _generator.generation_seed = seed
return _generator.generate() return _generator.generate()
@ -237,29 +242,34 @@ func _on_generation_completed(summary: Dictionary) -> void:
_prop_definitions_for(rule, tags), _prop_definitions_for(rule, tags),
coordinate, coordinate,
) )
if ( for _placement_attempt: int in (
definitions.is_empty() rule.placement_attempts_per_chunk
or not _prop_group_roll_succeeds( ):
rule, if group_counts.get(group_key, 0) >= maximum:
group_key, break
if (
definitions.is_empty()
or not _prop_group_roll_succeeds(
rule,
group_key,
coordinate,
random,
)
):
continue
if _maybe_add_prop(
definitions,
record.get("position", Vector3.ZERO),
coordinate, coordinate,
biome.stable_id,
random, random,
) terrain_triangles,
): ):
continue group_counts[group_key] += 1
if _maybe_add_prop( _record_prop_group_coordinate(
definitions, group_key,
record.get("position", Vector3.ZERO), coordinate,
coordinate, )
biome.stable_id,
random,
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
_ensure_minimum_props( _ensure_minimum_props(
eligible_records, eligible_records,
group_counts, group_counts,
@ -322,7 +332,7 @@ func _configure_static_water() -> void:
_ocean.fish_pool = OCEAN_POOL _ocean.fish_pool = OCEAN_POOL
_ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"] _ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"]
_ocean.surface_size = Vector2(10000.0, 10000.0) _ocean.surface_size = Vector2(10000.0, 10000.0)
_ocean.position.y = WATER_HEIGHT - 0.025 _ocean.position.y = WATER_HEIGHT
_apply_water_materials() _apply_water_materials()
@ -451,6 +461,11 @@ func _add_prop(
) -> bool: ) -> bool:
if definition == null or definition.packed_scene == null: if definition == null or definition.packed_scene == null:
return false return false
var visual_scale := random.randf_range(
definition.minimum_visual_scale,
definition.maximum_visual_scale,
)
var scaled_clearance := definition.clearance_radius * visual_scale
var placement := Vector3.ZERO var placement := Vector3.ZERO
var found_surface := false var found_surface := false
for attempt: int in PROP_PLACEMENT_ATTEMPTS: for attempt: int in PROP_PLACEMENT_ATTEMPTS:
@ -470,7 +485,7 @@ func _add_prop(
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE > WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
and _has_prop_clearance( and _has_prop_clearance(
placement, placement,
definition.clearance_radius, scaled_clearance,
) )
): ):
placement.y = surface_height placement.y = surface_height
@ -492,22 +507,26 @@ func _add_prop(
prop.set_meta(&"terrain_prop_group", definition.procedural_group) prop.set_meta(&"terrain_prop_group", definition.procedural_group)
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate) prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
prop.set_meta(&"terrain_biome_id", biome_id) prop.set_meta(&"terrain_biome_id", biome_id)
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
prop.position = placement prop.position = placement
prop.rotation.y = random.randf_range(-PI, PI) prop.rotation.y = random.randf_range(-PI, PI)
_decorations.add_child(prop) _decorations.add_child(prop)
visual_root.name = "Visual" visual_root.name = "Visual"
prop.add_child(visual_root) prop.add_child(visual_root)
visual_root.position = definition.visual_offset
visual_root.scale = Vector3.ONE * visual_scale
_configure_prop_visuals(visual_root) _configure_prop_visuals(visual_root)
_add_prop_collision(prop, definition) _apply_prop_material_variant(visual_root, definition, random)
_add_prop_collision(prop, definition, visual_scale)
_placed_prop_positions.append(placement) _placed_prop_positions.append(placement)
_placed_prop_clearance_radii.append(definition.clearance_radius) _placed_prop_clearance_radii.append(scaled_clearance)
_placed_prop_groups.append(definition.procedural_group) _placed_prop_groups.append(definition.procedural_group)
if definition.gatherable_anchor_height > 0.0: if definition.gatherable_anchor_height > 0.0:
var anchor := Marker3D.new() var anchor := Marker3D.new()
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count() anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
anchor.position = prop.position + Vector3( anchor.position = prop.position + Vector3(
0.0, 0.0,
definition.gatherable_anchor_height, definition.gatherable_anchor_height * visual_scale,
0.0, 0.0,
) )
_tree_anchors.add_child(anchor) _tree_anchors.add_child(anchor)
@ -624,7 +643,11 @@ func _prop_group_maximum(
if eligible_count <= 0: if eligible_count <= 0:
return 0 return 0
var maximum := ceili( var maximum := ceili(
float(eligible_count * rule.maximum_density) float(
eligible_count
* rule.placement_attempts_per_chunk
* rule.maximum_density
)
/ float(PROP_CHANCE_SCALE) / float(PROP_CHANCE_SCALE)
) )
return maxi(maximum, rule.minimum_placements) return maxi(maximum, rule.minimum_placements)
@ -777,6 +800,51 @@ func _configure_prop_visuals(root_node: Node) -> void:
_configure_prop_visuals(child) _configure_prop_visuals(child)
func _apply_prop_material_variant(
root_node: Node,
definition: TerrainPropDefinition,
random: RandomNumberGenerator,
) -> void:
if (
definition.material_variants.is_empty()
or definition.variant_material_slot_names.is_empty()
):
return
var variant := definition.material_variants[
random.randi_range(0, definition.material_variants.size() - 1)
]
_apply_material_variant_to_meshes(
root_node,
definition.variant_material_slot_names,
variant,
)
func _apply_material_variant_to_meshes(
root_node: Node,
target_material_names: PackedStringArray,
variant: Material,
) -> void:
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var active_material := mesh_instance.get_active_material(surface_index)
if (
active_material != null
and active_material.resource_name in target_material_names
):
mesh_instance.set_surface_override_material(
surface_index,
variant,
)
for child: Node in root_node.get_children():
_apply_material_variant_to_meshes(
child,
target_material_names,
variant,
)
func _terrain_surface_triangles() -> Array[PackedVector3Array]: func _terrain_surface_triangles() -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = [] var triangles: Array[PackedVector3Array] = []
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes(): for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
@ -818,6 +886,7 @@ func _surface_height_at(
func _add_prop_collision( func _add_prop_collision(
prop: Node3D, prop: Node3D,
definition: TerrainPropDefinition, definition: TerrainPropDefinition,
visual_scale: float,
) -> void: ) -> void:
if not definition.has_collision(): if not definition.has_collision():
return return
@ -830,16 +899,16 @@ func _add_prop_collision(
collision.name = "CollisionShape" collision.name = "CollisionShape"
if definition.has_box_collision(): if definition.has_box_collision():
var box := BoxShape3D.new() var box := BoxShape3D.new()
box.size = definition.collision_box_size box.size = definition.collision_box_size * visual_scale
collision.shape = box collision.shape = box
collision.position = definition.collision_offset collision.position = definition.collision_offset * visual_scale
else: else:
var cylinder := CylinderShape3D.new() var cylinder := CylinderShape3D.new()
cylinder.radius = definition.collision_radius cylinder.radius = definition.collision_radius * visual_scale
cylinder.height = definition.collision_height cylinder.height = definition.collision_height * visual_scale
collision.shape = cylinder collision.shape = cylinder
collision.position = ( collision.position = (
definition.collision_offset definition.collision_offset * visual_scale
+ Vector3.UP * cylinder.height * 0.5 + Vector3.UP * cylinder.height * 0.5
) )
body.add_child(collision) body.add_child(collision)

View file

@ -31,12 +31,13 @@ gatherable_anchor_root = NodePath("GatherableAnchors")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("2_generator") script = ExtResource("2_generator")
catalog = ExtResource("3_catalog") catalog = ExtResource("3_catalog")
grid_size = Vector2i(7, 7) grid_size = Vector2i(15, 13)
generation_seed = 13001 generation_seed = 13001
generate_on_ready = false generate_on_ready = false
build_collision = true build_collision = true
force_center_chunk_id = &"chunk_spawn" 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") required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008")
elevated_cliff_feature_enabled = true
required_chunk_weight_multiplier = 64.0 required_chunk_weight_multiplier = 64.0
maximum_backtracks = 100000 maximum_backtracks = 100000
@ -60,7 +61,7 @@ script = ExtResource("8_diggable")
area_id = &"starter_beach" area_id = &"starter_beach"
terrain_source = NodePath("../../Terrain/TerrainChunkGenerator") terrain_source = NodePath("../../Terrain/TerrainChunkGenerator")
surface_materials = Array[StringName]([&"sand"]) surface_materials = Array[StringName]([&"sand"])
generation_bounds = Rect2(-35, -35, 70, 70) generation_bounds = Rect2(-55, -45, 110, 90)
minimum_global_y = -0.24 minimum_global_y = -0.24
maximum_global_y = 0.5 maximum_global_y = 0.5
minimum_up_dot = 0.72 minimum_up_dot = 0.72

View file

@ -16,3 +16,7 @@ preferred_nearby_prop_groups = PackedStringArray("grass_tree")
preferred_nearby_radius = 12.0 preferred_nearby_radius = 12.0
nearby_preference_weight_multiplier = 3.0 nearby_preference_weight_multiplier = 3.0
clearance_radius = 0.8 clearance_radius = 0.8
visual_offset = Vector3(0, -0.08, 0)
collision_radius = 0.65
collision_height = 1.0
collision_offset = Vector3(0, -0.08, 0)

View file

@ -13,6 +13,8 @@ required_chunk_tags = PackedStringArray("grass")
selection_weight = 0.7 selection_weight = 0.7
minimum_spawn_chunk_distance = 2 minimum_spawn_chunk_distance = 2
clearance_radius = 1.55 clearance_radius = 1.55
minimum_visual_scale = 0.75
maximum_visual_scale = 1.22
collision_radius = 0.5 collision_radius = 0.5
collision_height = 4.0 collision_height = 4.0
gatherable_anchor_height = 2.15 gatherable_anchor_height = 2.15

View file

@ -1,7 +1,10 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] [gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
[resource] [resource]
script = ExtResource("1_definition") script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass") required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2 minimum_spawn_chunk_distance = 2
clearance_radius = 1.3 clearance_radius = 1.3
minimum_visual_scale = 0.75
maximum_visual_scale = 1.15
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
collision_radius = 0.4 collision_radius = 0.4
collision_height = 3.2 collision_height = 3.2
gatherable_anchor_height = 2.15 gatherable_anchor_height = 2.15

View file

@ -1,7 +1,10 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] [gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
[resource] [resource]
script = ExtResource("1_definition") script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass") required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2 minimum_spawn_chunk_distance = 2
clearance_radius = 1.5 clearance_radius = 1.5
minimum_visual_scale = 0.8
maximum_visual_scale = 1.18
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
collision_radius = 0.5 collision_radius = 0.5
collision_height = 3.8 collision_height = 3.8
gatherable_anchor_height = 2.15 gatherable_anchor_height = 2.15

View file

@ -1,7 +1,10 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] [gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
[resource] [resource]
script = ExtResource("1_definition") script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass") required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2 minimum_spawn_chunk_distance = 2
clearance_radius = 1.8 clearance_radius = 1.8
minimum_visual_scale = 0.85
maximum_visual_scale = 1.15
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
collision_radius = 0.55 collision_radius = 0.55
collision_height = 4.2 collision_height = 4.2
gatherable_anchor_height = 2.15 gatherable_anchor_height = 2.15

View file

@ -0,0 +1,9 @@
[gd_resource type="StandardMaterial3D" format=3]
[resource]
resource_name = "leaf_variant_dark"
albedo_color = Color(0.1712, 0.252, 0, 1)
metallic = 0.0
roughness = 1.0
shading_mode = 1
texture_filter = 0

View file

@ -0,0 +1,9 @@
[gd_resource type="StandardMaterial3D" format=3]
[resource]
resource_name = "leaf_variant_light"
albedo_color = Color(0.3388, 0.7187, 0, 1)
metallic = 0.0
roughness = 1.0
shading_mode = 1
texture_filter = 0

View file

@ -0,0 +1,9 @@
[gd_resource type="StandardMaterial3D" format=3]
[resource]
resource_name = "leaf_variant_mid"
albedo_color = Color(0.3147, 0.4631, 0, 1)
metallic = 0.0
roughness = 1.0
shading_mode = 1
texture_filter = 0

View file

@ -6,6 +6,9 @@ extends Resource
@export_range(0, 10000, 1) var adjacency_bonus := 0 @export_range(0, 10000, 1) var adjacency_bonus := 0
@export_range(0, 10000, 1) var maximum_density := 10000 @export_range(0, 10000, 1) var maximum_density := 10000
@export_range(0, 64, 1) var minimum_placements := 0 @export_range(0, 64, 1) var minimum_placements := 0
## More than one attempt permits intentionally dense biomes without changing
## the density of every biome that uses the same procedural prop group.
@export_range(1, 8, 1) var placement_attempts_per_chunk := 1
## Empty permits every compatible prop in the procedural group. ## Empty permits every compatible prop in the procedural group.
@export var allowed_prop_ids := PackedStringArray() @export var allowed_prop_ids := PackedStringArray()

View file

@ -34,6 +34,29 @@ static func create_variants(
chunk_size, chunk_size,
edge_epsilon, edge_epsilon,
) )
if definition.base_layer_scene != null:
var base_layer_root := definition.base_layer_scene.instantiate()
var base_layer_mesh := find_primary_mesh(
base_layer_root,
definition.base_layer_mesh_name,
)
if base_layer_mesh == null or base_layer_mesh.mesh == null:
push_error(
"Terrain chunk %s has no base-layer MeshInstance3D named %s."
% [definition.stable_id, definition.base_layer_mesh_name]
)
base_layer_root.free()
chunk_root.free()
return variants
boundary_points.append_array(
_collect_boundary_points(
base_layer_mesh.mesh,
_transform_relative_to(base_layer_mesh, base_layer_root),
chunk_size,
edge_epsilon,
)
)
base_layer_root.free()
for quarter_turns: int in 4: for quarter_turns: int in 4:
if not definition.allows_quarter_turn(quarter_turns): if not definition.allows_quarter_turn(quarter_turns):
continue continue

View file

@ -30,6 +30,19 @@ func validation_errors() -> PackedStringArray:
errors.append("%s has no PackedScene." % definition.stable_id) errors.append("%s has no PackedScene." % definition.stable_id)
if definition.primary_mesh_name == &"": if definition.primary_mesh_name == &"":
errors.append("%s has no primary mesh name." % definition.stable_id) errors.append("%s has no primary mesh name." % definition.stable_id)
if (
(definition.base_layer_scene == null)
!= (definition.base_layer_mesh_name == &"")
):
errors.append(
"%s must define both base-layer scene and mesh name."
% definition.stable_id
)
if definition.overlay_only and definition.base_layer_scene == null:
errors.append(
"%s is overlay-only but has no base-layer scene."
% definition.stable_id
)
if definition.allowed_rotation_mask == 0: if definition.allowed_rotation_mask == 0:
errors.append("%s allows no rotations." % definition.stable_id) errors.append("%s allows no rotations." % definition.stable_id)
var has_coast := "coast" in definition.tags var has_coast := "coast" in definition.tags
@ -44,6 +57,11 @@ func validation_errors() -> PackedStringArray:
"%s has an ocean-facing edge without the coast tag." "%s has an ocean-facing edge without the coast tag."
% definition.stable_id % definition.stable_id
) )
if definition.must_be_interior and definition.prefers_map_boundary:
errors.append(
"%s cannot require the interior and prefer the boundary."
% definition.stable_id
)
var has_fresh_water := "fresh_water" in definition.tags var has_fresh_water := "fresh_water" in definition.tags
var has_water_footprint := ( var has_water_footprint := (
definition.water_surface_size.x > 0.0 definition.water_surface_size.x > 0.0

View file

@ -5,6 +5,17 @@ extends Resource
@export var label := "" @export var label := ""
@export var packed_scene: PackedScene @export var packed_scene: PackedScene
@export var primary_mesh_name: StringName @export var primary_mesh_name: StringName
## Optional existing level-one chunk placed beneath an authored elevated
## overlay. This lets cliff edges leave their downhill side open while the
## ordinary base terrain remains present in the same generated cell.
@export_group("Layered Terrain")
@export var base_layer_scene: PackedScene
@export var base_layer_mesh_name: StringName
## Overlay-only chunks replace a reserved base-terrain placement after the
## ordinary terrain solve. They remain in placement manifests and runtime
## output, but do not inflate every cell's global solver domain.
@export var overlay_only := false
@export_group("")
@export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees") @export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees")
var allowed_rotation_mask := 15 var allowed_rotation_mask := 15
@export_range(0.01, 100.0, 0.01) var selection_weight := 1.0 @export_range(0.01, 100.0, 0.01) var selection_weight := 1.0
@ -36,6 +47,9 @@ var allowed_rotation_mask := 15
## weight. This shapes regions without turning a visual preference into a hard ## weight. This shapes regions without turning a visual preference into a hard
## generation constraint. ## generation constraint.
@export var preferred_neighbor_tags := PackedStringArray() @export var preferred_neighbor_tags := PackedStringArray()
## Interior-only pieces may not occupy any outer grid cell. Coastal elevated
## pieces use separate authored definitions rather than weakening this rule.
@export var must_be_interior := false
## Coastal transition pieces should normally migrate toward the generated ## Coastal transition pieces should normally migrate toward the generated
## region's perimeter while remaining legal in the interior. ## region's perimeter while remaining legal in the interior.
@export var prefers_map_boundary := false @export var prefers_map_boundary := false

View file

@ -5,6 +5,8 @@ signal generation_completed(summary: Dictionary)
const CONSTRAINT_PROFILE_QUANTIZATION := 0.001 const CONSTRAINT_PROFILE_QUANTIZATION := 0.001
const CANDIDATE_WEIGHT_SCALE := 10000.0 const CANDIDATE_WEIGHT_SCALE := 10000.0
const LARGE_GRID_LIGHTWEIGHT_THRESHOLD := 128
const LARGE_GRID_PROPAGATION_INTERVAL := 8
@export var catalog: TerrainChunkCatalog @export var catalog: TerrainChunkCatalog
@export var grid_size := Vector2i(7, 7) @export var grid_size := Vector2i(7, 7)
@ -14,6 +16,15 @@ const CANDIDATE_WEIGHT_SCALE := 10000.0
@export var show_chunk_labels := false @export var show_chunk_labels := false
@export var force_center_chunk_id: StringName = &"chunk_0000" @export var force_center_chunk_id: StringName = &"chunk_0000"
@export var required_chunk_ids := PackedStringArray() @export var required_chunk_ids := PackedStringArray()
@export_group("Elevated Inland Feature")
@export var elevated_cliff_feature_enabled := false
@export var elevated_cliff_base_chunk_id: StringName = &"chunk_0000"
@export var elevated_cliff_top_chunk_id: StringName = &"chunk_0009"
@export var elevated_cliff_corner_chunk_id: StringName = &"chunk_0010"
@export var elevated_cliff_edge_chunk_id: StringName = &"chunk_0011"
@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012"
@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 0.75
@export_group("")
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0 @export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
@export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6 @export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6
@export_range(0.01, 1.0, 0.01) var long_repeat_weight_multiplier := 0.3 @export_range(0.01, 1.0, 0.01) var long_repeat_weight_multiplier := 0.3
@ -33,6 +44,8 @@ var _placements: Array[TerrainChunkVariant] = []
var _random := RandomNumberGenerator.new() var _random := RandomNumberGenerator.new()
var _generated_chunks: Node3D var _generated_chunks: Node3D
var _backtrack_count := 0 var _backtrack_count := 0
var _elevated_feature_center := Vector2i(-1, -1)
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
func _ready() -> void: func _ready() -> void:
@ -49,6 +62,9 @@ func generate() -> bool:
_backtrack_count = 0 _backtrack_count = 0
_placements.clear() _placements.clear()
_placements.resize(grid_size.x * grid_size.y) _placements.resize(grid_size.x * grid_size.y)
if not _prepare_elevated_feature_region():
_placements.assign(previous_placements)
return false
if not _solve_cell(0): if not _solve_cell(0):
_placements.assign(previous_placements) _placements.assign(previous_placements)
push_error( push_error(
@ -57,6 +73,9 @@ func generate() -> bool:
) )
return false return false
_resolve_equivalent_rotations() _resolve_equivalent_rotations()
if not _apply_elevated_feature():
_placements.assign(previous_placements)
return false
var solution_root := _build_solution_root() var solution_root := _build_solution_root()
if solution_root == null: if solution_root == null:
_placements.assign(previous_placements) _placements.assign(previous_placements)
@ -195,6 +214,8 @@ func _prepare_catalog() -> bool:
return false return false
for variant: TerrainChunkVariant in analyzed: for variant: TerrainChunkVariant in analyzed:
_variants.append(variant) _variants.append(variant)
if definition.overlay_only:
continue
var constraint_key := _constraint_key(variant) var constraint_key := _constraint_key(variant)
var rotations: PackedInt32Array = _equivalent_rotations.get( var rotations: PackedInt32Array = _equivalent_rotations.get(
constraint_key, constraint_key,
@ -207,6 +228,9 @@ func _prepare_catalog() -> bool:
if _variants.is_empty(): if _variants.is_empty():
push_error("Terrain chunk catalog produced no usable variants.") push_error("Terrain chunk catalog produced no usable variants.")
return false return false
if _solver_variants.is_empty():
push_error("Terrain chunk catalog produced no base-terrain solver variants.")
return false
for index: int in _solver_variants.size(): for index: int in _solver_variants.size():
_solver_variant_indices[_solver_variants[index]] = index _solver_variant_indices[_solver_variants[index]] = index
_build_edge_compatibility_cache() _build_edge_compatibility_cache()
@ -303,7 +327,7 @@ func _solve_cell(placed_count: int) -> bool:
return false return false
if not _requirements_can_still_be_satisfied(): if not _requirements_can_still_be_satisfied():
return false return false
var next_cell := _select_next_cell() var next_cell := _select_next_cell(placed_count)
var index := int(next_cell.get("index", -1)) var index := int(next_cell.get("index", -1))
if index < 0: if index < 0:
return false return false
@ -325,7 +349,12 @@ func _solve_cell(placed_count: int) -> bool:
return false return false
func _select_next_cell() -> Dictionary: func _select_next_cell(placed_count: int) -> Dictionary:
if (
_placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD
and placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0
):
return _select_next_large_grid_cell()
# Rebuild and propagate the small domain table on every branch. This catches # Rebuild and propagate the small domain table on every branch. This catches
# unsupported connector chains before they turn into a deep recursive dead # unsupported connector chains before they turn into a deep recursive dead
# end, while keeping placement state simple and deterministic. # end, while keeping placement state simple and deterministic.
@ -356,6 +385,54 @@ func _select_next_cell() -> Dictionary:
return {"index": best_index, "candidates": best_candidates} return {"index": best_index, "candidates": best_candidates}
func _select_next_large_grid_cell() -> Dictionary:
# Full all-cell arc propagation scales cubically as the map grows. Large
# worlds instead use the same compatibility checks with a frontier-aware MRV
# pass. Exact placement validation and backtracking remain unchanged.
var best_index := -1
var best_candidates: Array[TerrainChunkVariant] = []
var best_placed_neighbors := -1
for index: int in _placements.size():
if _placements[index] != null:
continue
var candidates := _compatible_candidates(index)
if candidates.is_empty():
return {"index": index, "candidates": candidates}
var placed_neighbors := _placed_neighbor_count(index)
if (
best_index < 0
or candidates.size() < best_candidates.size()
or (
candidates.size() == best_candidates.size()
and placed_neighbors > best_placed_neighbors
)
):
best_index = index
best_candidates = candidates
best_placed_neighbors = placed_neighbors
return {"index": best_index, "candidates": best_candidates}
func _placed_neighbor_count(index: int) -> int:
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var count := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
if not _coordinate_is_inside_grid(neighbor_coordinate):
continue
var neighbor_index := (
neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x
)
if _placements[neighbor_index] != null:
count += 1
return count
func _propagate_domains(domains: Dictionary) -> bool: func _propagate_domains(domains: Dictionary) -> bool:
var changed := true var changed := true
while changed: while changed:
@ -568,6 +645,11 @@ func _candidate_can_occupy_cell(
if not _definition_has_capacity(candidate.definition): if not _definition_has_capacity(candidate.definition):
return false return false
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
if (
_elevated_feature_reserved_grass_indices.has(index)
and candidate.definition.stable_id != elevated_cliff_base_chunk_id
):
return false
var center := Vector2i(grid_size.x / 2, grid_size.y / 2) var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
var center_index := center.y * grid_size.x + center.x var center_index := center.y * grid_size.x + center.x
if ( if (
@ -599,10 +681,123 @@ func _candidate_can_occupy_cell(
) )
func _prepare_elevated_feature_region() -> bool:
_elevated_feature_center = Vector2i(-1, -1)
_elevated_feature_reserved_grass_indices.clear()
if not elevated_cliff_feature_enabled:
return true
for stable_id: StringName in [
elevated_cliff_base_chunk_id,
elevated_cliff_top_chunk_id,
elevated_cliff_corner_chunk_id,
elevated_cliff_edge_chunk_id,
elevated_cliff_ramp_chunk_id,
]:
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:
push_error(
"Elevated cliff feature requires at least an 11x9 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):
var candidate := Vector2i(column, row)
if (
absi(candidate.x - forced_center.x) <= 2
and absi(candidate.y - forced_center.y) <= 2
):
continue
candidates.append(candidate)
if candidates.is_empty():
push_error("Terrain grid has no inland 3x3 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)
)
_elevated_feature_reserved_grass_indices[
coordinate.y * grid_size.x + coordinate.x
] = true
return true
func _apply_elevated_feature() -> bool:
if not elevated_cliff_feature_enabled:
return true
if _elevated_feature_center.x < 0 or _elevated_feature_center.y < 0:
push_error("Elevated cliff feature has no reserved base region.")
return false
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"],
})
for spec: Dictionary in placements:
var definition := catalog.definition_for_id(spec["id"] as StringName)
var variant := _authored_variant(definition, int(spec["turns"]))
if variant == null:
push_error(
"Elevated cliff feature cannot resolve %s rotation %d."
% [spec["id"], spec["turns"]]
)
return false
var coordinate := _elevated_feature_center + (spec["offset"] as Vector2i)
_placements[coordinate.y * grid_size.x + coordinate.x] = variant
var validation_error := _resolved_layout_validation_error(_placements)
if not validation_error.is_empty():
push_error("Elevated cliff feature is invalid: " + validation_error)
return false
return true
func _variant_respects_ocean_boundary( func _variant_respects_ocean_boundary(
variant: TerrainChunkVariant, variant: TerrainChunkVariant,
coordinate: Vector2i, coordinate: Vector2i,
) -> bool: ) -> bool:
if (
variant.definition.must_be_interior
and _coordinate_is_on_boundary(coordinate)
):
return false
var ocean_edges := variant.rotated_edge_mask( var ocean_edges := variant.rotated_edge_mask(
variant.definition.ocean_facing_edges variant.definition.ocean_facing_edges
) )
@ -1231,7 +1426,7 @@ func _build_solution_root() -> Node3D:
if variant == null: if variant == null:
continue continue
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var chunk_root := variant.definition.packed_scene.instantiate() as Node3D var chunk_root := _instantiate_chunk_root(variant.definition)
if chunk_root == null: if chunk_root == null:
push_error("%s does not instantiate as Node3D." % variant.stable_key()) push_error("%s does not instantiate as Node3D." % variant.stable_key())
solution_root.free() solution_root.free()
@ -1260,6 +1455,27 @@ func _build_solution_root() -> Node3D:
return solution_root return solution_root
func _instantiate_chunk_root(
definition: TerrainChunkDefinition,
) -> Node3D:
var terrain_visual := definition.packed_scene.instantiate() as Node3D
if terrain_visual == null:
return null
if definition.base_layer_scene == null:
return terrain_visual
var base_layer_visual := definition.base_layer_scene.instantiate() as Node3D
if base_layer_visual == null:
terrain_visual.free()
return null
var layered_root := Node3D.new()
layered_root.name = "LayeredTerrainChunk"
base_layer_visual.name = "TerrainBaseLayer"
terrain_visual.name = "TerrainOverlay"
layered_root.add_child(base_layer_visual)
layered_root.add_child(terrain_visual)
return layered_root
func _replace_generated_chunks(solution_root: Node3D) -> void: func _replace_generated_chunks(solution_root: Node3D) -> void:
_clear_generated_chunks() _clear_generated_chunks()
_generated_chunks = solution_root _generated_chunks = solution_root
@ -1274,11 +1490,34 @@ func _add_collision(
chunk_root, chunk_root,
definition.primary_mesh_name, definition.primary_mesh_name,
) )
_add_collision_to_mesh(
primary_mesh,
definition.stable_id,
"TerrainCollision",
)
if definition.base_layer_scene == null:
return
var base_layer_mesh := TerrainChunkAnalyzer.find_primary_mesh(
chunk_root,
definition.base_layer_mesh_name,
)
_add_collision_to_mesh(
base_layer_mesh,
definition.stable_id,
"TerrainBaseLayerCollision",
)
func _add_collision_to_mesh(
primary_mesh: MeshInstance3D,
stable_id: StringName,
collision_name: String,
) -> void:
if primary_mesh == null or primary_mesh.mesh == null: if primary_mesh == null or primary_mesh.mesh == null:
return return
var terrain_shape := primary_mesh.mesh.create_trimesh_shape() var terrain_shape := primary_mesh.mesh.create_trimesh_shape()
if terrain_shape == null or terrain_shape.get_faces().is_empty(): if terrain_shape == null or terrain_shape.get_faces().is_empty():
push_warning("%s produced no terrain collision." % definition.stable_id) push_warning("%s produced no terrain collision." % stable_id)
return return
var concave_shape := terrain_shape as ConcavePolygonShape3D var concave_shape := terrain_shape as ConcavePolygonShape3D
if concave_shape != null: if concave_shape != null:
@ -1287,7 +1526,7 @@ func _add_collision(
# can never become an invisible collision gap. # can never become an invisible collision gap.
concave_shape.backface_collision = true concave_shape.backface_collision = true
var collision_body := StaticBody3D.new() var collision_body := StaticBody3D.new()
collision_body.name = "TerrainCollision" collision_body.name = collision_name
collision_body.collision_layer = 1 collision_body.collision_layer = 1
collision_body.collision_mask = 0 collision_body.collision_mask = 0
primary_mesh.add_child(collision_body) primary_mesh.add_child(collision_body)

View file

@ -82,4 +82,23 @@ func validation_errors() -> PackedStringArray:
"%s has a negative box collision dimension." "%s has a negative box collision dimension."
% definition.stable_id % definition.stable_id
) )
if definition.minimum_visual_scale > definition.maximum_visual_scale:
errors.append(
"%s has an inverted visual scale range."
% definition.stable_id
)
if (
not definition.material_variants.is_empty()
and definition.variant_material_slot_names.is_empty()
):
errors.append(
"%s has material variants but no target material slots."
% definition.stable_id
)
for material: Material in definition.material_variants:
if material == null:
errors.append(
"%s contains an empty material variant."
% definition.stable_id
)
return errors return errors

View file

@ -18,6 +18,15 @@ extends Resource
@export_range(0.0, 100.0, 0.5) var preferred_nearby_radius := 0.0 @export_range(0.0, 100.0, 0.5) var preferred_nearby_radius := 0.0
@export_range(1.0, 10.0, 0.1) var nearby_preference_weight_multiplier := 1.0 @export_range(1.0, 10.0, 0.1) var nearby_preference_weight_multiplier := 1.0
@export_range(0.0, 10.0, 0.05) var clearance_radius := 0.5 @export_range(0.0, 10.0, 0.05) var clearance_radius := 0.5
## Procedural instances use a deterministic uniform scale in this range.
@export_range(0.1, 4.0, 0.05) var minimum_visual_scale := 1.0
@export_range(0.1, 4.0, 0.05) var maximum_visual_scale := 1.0
## Presentation-only offset. The prop root remains exactly terrain-aligned.
@export var visual_offset := Vector3.ZERO
## A single material is chosen per prop and applied only to matching imported
## material slots. This keeps trunks untouched while varying foliage.
@export var variant_material_slot_names := PackedStringArray()
@export var material_variants: Array[Material] = []
@export_range(0.0, 5.0, 0.05) var collision_radius := 0.0 @export_range(0.0, 5.0, 0.05) var collision_radius := 0.0
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0 @export_range(0.0, 20.0, 0.05) var collision_height := 0.0
@export var collision_box_size := Vector3.ZERO @export var collision_box_size := Vector3.ZERO