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,
) -> void:
var placements := generator.placement_keys()
assert(placements.size() == 49)
assert(placements[24].begins_with("chunk_spawn@"))
var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
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_0001") >= 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_0006") >= 1)
assert(_placement_count(placements, "chunk_0007") >= 1)
assert(generator.get_generated_chunks_root().get_child_count() == 49)
assert(generator.get_primary_terrain_meshes().size() == 49)
assert(_placement_count(placements, "chunk_0008") >= 1)
assert(_placement_count(placements, "chunk_0009") == 1)
assert(_placement_count(placements, "chunk_0010") == 4)
assert(
_placement_count(placements, "chunk_0011")
+ _placement_count(placements, "chunk_0012")
== 4
)
assert(_placement_count(placements, "chunk_0012") <= 1)
assert(
generator.get_generated_chunks_root().get_child_count()
== expected_chunk_count
)
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 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
assert(ocean != null and fresh_root != null)
assert(ocean.water_type == WaterType.Type.SALT_WATER)
assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT))
var fresh_placement_count := 0
for record: Dictionary in generator.placement_records():
var tags: PackedStringArray = record.get("tags", PackedStringArray())
@ -123,6 +146,7 @@ func _validate_generated_region(
var fresh := child as WaterBodyAuthoring
assert(fresh != null)
assert(fresh.water_type == WaterType.Type.FRESH_WATER)
assert(is_equal_approx(fresh.position.y, GeneratedWorldRegion.WATER_HEIGHT))
assert(fresh.visible)
assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0)
assert(not fresh.visual_surface_enabled)
@ -155,13 +179,13 @@ func _validate_generated_region(
&"terrain_chunk_coordinate",
Vector2i.ZERO,
)
var center := Vector2i(
var spawn_coordinate := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
)
var spawn_distance := (
absi(coordinate.x - center.x)
+ absi(coordinate.y - center.y)
absi(coordinate.x - spawn_coordinate.x)
+ absi(coordinate.y - spawn_coordinate.y)
)
assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
_validate_decoration_transform(
@ -169,6 +193,13 @@ func _validate_generated_region(
child as Node3D,
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, &"sand_tree") > 0)
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(
record: Dictionary,
grid_size: Vector2i,
@ -224,6 +318,23 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
assert(instance.position.is_zero_approx())
assert(_find_mesh_instance(instance) != null)
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(
@ -276,7 +387,16 @@ func _validate_decoration_transform(
var collision := prop.get_node_or_null(
"TrunkCollision/CollisionShape"
) 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)
if definition.has_collision():
assert(collision != null)
@ -319,6 +439,26 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
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(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
@ -329,6 +469,7 @@ func _validate_authored_chunk_surfaces(
var found_grass_ocean_edge := false
var found_grass_beach_transition := false
var found_grass_ocean_corner := false
var found_beach_ocean_corner := false
for chunk_root: Node in generated.get_children():
if chunk_root.name.begins_with("chunk_0002r"):
var beach := chunk_root.find_child(
@ -383,11 +524,20 @@ func _validate_authored_chunk_surfaces(
assert("grass_lite" in corner_materials)
assert("cliff_wall" in corner_materials)
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_pond)
assert(found_grass_ocean_edge)
assert(found_grass_beach_transition)
assert(found_grass_ocean_corner)
assert(found_beach_ocean_corner)
func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:

View file

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

View file

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

View file

@ -12,6 +12,11 @@ const EXPECTED_IDS: Array[String] = [
"chunk_0005",
"chunk_0006",
"chunk_0007",
"chunk_0008",
"chunk_0009",
"chunk_0010",
"chunk_0011",
"chunk_0012",
"chunk_spawn",
]
const REQUIRED_IDS: Array[String] = [
@ -23,10 +28,11 @@ const REQUIRED_IDS: Array[String] = [
"chunk_0005",
"chunk_0006",
"chunk_0007",
"chunk_0008",
]
const TEST_SEED := 13001
const CONNECTOR_STRESS_SEED := 287
const EXPECTED_SOLVER_VARIANT_COUNT := 27
const EXPECTED_SOLVER_VARIANT_COUNT := 31
const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0000": 4,
&"chunk_0001": 4,
@ -36,6 +42,11 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0005": 4,
&"chunk_0006": 4,
&"chunk_0007": 4,
&"chunk_0008": 4,
&"chunk_0009": 4,
&"chunk_0010": 4,
&"chunk_0011": 4,
&"chunk_0012": 4,
&"chunk_spawn": 1,
}
@ -75,6 +86,11 @@ func _validate_catalog() -> void:
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
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")
_check(
(
@ -84,6 +100,11 @@ func _validate_catalog() -> void:
and grass_ocean_edge != null
and grass_beach_transition != 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
),
"The authored biome-rule definitions must be available.",
@ -95,6 +116,11 @@ func _validate_catalog() -> void:
and grass_ocean_edge != null
and grass_beach_transition != 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
):
_check(
@ -193,6 +219,45 @@ func _validate_catalog() -> void:
),
"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:
@ -443,11 +508,13 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
_check(
(
grass_ocean_edge != null
and grass_beach_transition != null
and grass_ocean_corner != null
and beach_ocean_corner != null
),
"The authored coastline additions must be available.",
)
@ -455,6 +522,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
grass_ocean_edge == null
or grass_beach_transition == null
or grass_ocean_corner == null
or beach_ocean_corner == null
):
return
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 transition_zero := _find_variant(generator, &"chunk_0006", 0)
var corner_zero := _find_variant(generator, &"chunk_0007", 0)
var beach_corner_zero := _find_variant(generator, &"chunk_0008", 0)
_check(
beach_zero != null
and grass_edge_zero != null
and grass_edge_south != 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.",
)
if (
@ -476,6 +546,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and grass_edge_south != null
and transition_zero != null
and corner_zero != null
and beach_corner_zero != null
):
_check(
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.",
)
_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_sand := _find_variant(generator, &"chunk_0001", 0)
_check(
@ -560,7 +649,12 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
),
"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:
var coast_variant := _find_variant(
generator,
@ -580,11 +674,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
),
"%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
var boundary_coordinate := Vector2i(3, 3)
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():
if (ocean_edges & (1 << edge_value)) == 0:
@ -603,15 +701,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
coast_variant,
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:
var summary := generator._build_summary()
_check(
int(summary.get("variant_count", 0)) == 33,
"The current catalog must expose all 33 authored rotations.",
int(summary.get("variant_count", 0)) == 53,
"The current catalog must expose all 53 authored rotations.",
)
_check(
int(summary.get("solver_variant_count", 0))
@ -811,6 +909,27 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
straight_edge_neighbors == 2,
"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":
continue
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,
# leaving the surrounding ocean visible. Positive X is the canonical
# 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
# 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(
bounds,
tolerance,

View file

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

View file

@ -6,10 +6,11 @@
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 9000
adjacency_bonus = 1000
maximum_density = 10000
placement_chance = 8200
adjacency_bonus = 1200
maximum_density = 8500
minimum_placements = 2
placement_attempts_per_chunk = 3
allowed_prop_ids = PackedStringArray("prop_pine")
[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/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
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"
selection_weight = 0.75
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")
required_neighbor_tags = PackedStringArray("sand")
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="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_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_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]
script = ExtResource("1_catalog")
chunk_size = 10.0
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("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:
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
return false
if (
seed == _current_seed
and _generator.get_generated_chunks_root() != null
):
return true
_current_seed = seed
_generator.generation_seed = seed
return _generator.generate()
@ -237,29 +242,34 @@ func _on_generation_completed(summary: Dictionary) -> void:
_prop_definitions_for(rule, tags),
coordinate,
)
if (
definitions.is_empty()
or not _prop_group_roll_succeeds(
rule,
group_key,
for _placement_attempt: int in (
rule.placement_attempts_per_chunk
):
if group_counts.get(group_key, 0) >= maximum:
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,
biome.stable_id,
random,
)
):
continue
if _maybe_add_prop(
definitions,
record.get("position", Vector3.ZERO),
coordinate,
biome.stable_id,
random,
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
_ensure_minimum_props(
eligible_records,
group_counts,
@ -322,7 +332,7 @@ func _configure_static_water() -> void:
_ocean.fish_pool = OCEAN_POOL
_ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"]
_ocean.surface_size = Vector2(10000.0, 10000.0)
_ocean.position.y = WATER_HEIGHT - 0.025
_ocean.position.y = WATER_HEIGHT
_apply_water_materials()
@ -451,6 +461,11 @@ func _add_prop(
) -> bool:
if definition == null or definition.packed_scene == null:
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 found_surface := false
for attempt: int in PROP_PLACEMENT_ATTEMPTS:
@ -470,7 +485,7 @@ func _add_prop(
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
and _has_prop_clearance(
placement,
definition.clearance_radius,
scaled_clearance,
)
):
placement.y = surface_height
@ -492,22 +507,26 @@ func _add_prop(
prop.set_meta(&"terrain_prop_group", definition.procedural_group)
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
prop.set_meta(&"terrain_biome_id", biome_id)
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
prop.position = placement
prop.rotation.y = random.randf_range(-PI, PI)
_decorations.add_child(prop)
visual_root.name = "Visual"
prop.add_child(visual_root)
visual_root.position = definition.visual_offset
visual_root.scale = Vector3.ONE * visual_scale
_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_clearance_radii.append(definition.clearance_radius)
_placed_prop_clearance_radii.append(scaled_clearance)
_placed_prop_groups.append(definition.procedural_group)
if definition.gatherable_anchor_height > 0.0:
var anchor := Marker3D.new()
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
anchor.position = prop.position + Vector3(
0.0,
definition.gatherable_anchor_height,
definition.gatherable_anchor_height * visual_scale,
0.0,
)
_tree_anchors.add_child(anchor)
@ -624,7 +643,11 @@ func _prop_group_maximum(
if eligible_count <= 0:
return 0
var maximum := ceili(
float(eligible_count * rule.maximum_density)
float(
eligible_count
* rule.placement_attempts_per_chunk
* rule.maximum_density
)
/ float(PROP_CHANCE_SCALE)
)
return maxi(maximum, rule.minimum_placements)
@ -777,6 +800,51 @@ func _configure_prop_visuals(root_node: Node) -> void:
_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]:
var triangles: Array[PackedVector3Array] = []
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
@ -818,6 +886,7 @@ func _surface_height_at(
func _add_prop_collision(
prop: Node3D,
definition: TerrainPropDefinition,
visual_scale: float,
) -> void:
if not definition.has_collision():
return
@ -830,16 +899,16 @@ func _add_prop_collision(
collision.name = "CollisionShape"
if definition.has_box_collision():
var box := BoxShape3D.new()
box.size = definition.collision_box_size
box.size = definition.collision_box_size * visual_scale
collision.shape = box
collision.position = definition.collision_offset
collision.position = definition.collision_offset * visual_scale
else:
var cylinder := CylinderShape3D.new()
cylinder.radius = definition.collision_radius
cylinder.height = definition.collision_height
cylinder.radius = definition.collision_radius * visual_scale
cylinder.height = definition.collision_height * visual_scale
collision.shape = cylinder
collision.position = (
definition.collision_offset
definition.collision_offset * visual_scale
+ Vector3.UP * cylinder.height * 0.5
)
body.add_child(collision)

View file

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

View file

@ -16,3 +16,7 @@ preferred_nearby_prop_groups = PackedStringArray("grass_tree")
preferred_nearby_radius = 12.0
nearby_preference_weight_multiplier = 3.0
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
minimum_spawn_chunk_distance = 2
clearance_radius = 1.55
minimum_visual_scale = 0.75
maximum_visual_scale = 1.22
collision_radius = 0.5
collision_height = 4.0
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="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]
script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
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_height = 3.2
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="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]
script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
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_height = 3.8
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="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]
script = ExtResource("1_definition")
@ -12,6 +15,10 @@ procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
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_height = 4.2
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 maximum_density := 10000
@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.
@export var allowed_prop_ids := PackedStringArray()

View file

@ -34,6 +34,29 @@ static func create_variants(
chunk_size,
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:
if not definition.allows_quarter_turn(quarter_turns):
continue

View file

@ -30,6 +30,19 @@ func validation_errors() -> PackedStringArray:
errors.append("%s has no PackedScene." % definition.stable_id)
if definition.primary_mesh_name == &"":
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:
errors.append("%s allows no rotations." % definition.stable_id)
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."
% 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_water_footprint := (
definition.water_surface_size.x > 0.0

View file

@ -5,6 +5,17 @@ extends Resource
@export var label := ""
@export var packed_scene: PackedScene
@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")
var allowed_rotation_mask := 15
@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
## generation constraint.
@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
## region's perimeter while remaining legal in the interior.
@export var prefers_map_boundary := false

View file

@ -5,6 +5,8 @@ signal generation_completed(summary: Dictionary)
const CONSTRAINT_PROFILE_QUANTIZATION := 0.001
const CANDIDATE_WEIGHT_SCALE := 10000.0
const LARGE_GRID_LIGHTWEIGHT_THRESHOLD := 128
const LARGE_GRID_PROPAGATION_INTERVAL := 8
@export var catalog: TerrainChunkCatalog
@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 force_center_chunk_id: StringName = &"chunk_0000"
@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, 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
@ -33,6 +44,8 @@ var _placements: Array[TerrainChunkVariant] = []
var _random := RandomNumberGenerator.new()
var _generated_chunks: Node3D
var _backtrack_count := 0
var _elevated_feature_center := Vector2i(-1, -1)
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
func _ready() -> void:
@ -49,6 +62,9 @@ func generate() -> bool:
_backtrack_count = 0
_placements.clear()
_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):
_placements.assign(previous_placements)
push_error(
@ -57,6 +73,9 @@ func generate() -> bool:
)
return false
_resolve_equivalent_rotations()
if not _apply_elevated_feature():
_placements.assign(previous_placements)
return false
var solution_root := _build_solution_root()
if solution_root == null:
_placements.assign(previous_placements)
@ -195,6 +214,8 @@ func _prepare_catalog() -> bool:
return false
for variant: TerrainChunkVariant in analyzed:
_variants.append(variant)
if definition.overlay_only:
continue
var constraint_key := _constraint_key(variant)
var rotations: PackedInt32Array = _equivalent_rotations.get(
constraint_key,
@ -207,6 +228,9 @@ func _prepare_catalog() -> bool:
if _variants.is_empty():
push_error("Terrain chunk catalog produced no usable variants.")
return false
if _solver_variants.is_empty():
push_error("Terrain chunk catalog produced no base-terrain solver variants.")
return false
for index: int in _solver_variants.size():
_solver_variant_indices[_solver_variants[index]] = index
_build_edge_compatibility_cache()
@ -303,7 +327,7 @@ func _solve_cell(placed_count: int) -> bool:
return false
if not _requirements_can_still_be_satisfied():
return false
var next_cell := _select_next_cell()
var next_cell := _select_next_cell(placed_count)
var index := int(next_cell.get("index", -1))
if index < 0:
return false
@ -325,7 +349,12 @@ func _solve_cell(placed_count: int) -> bool:
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
# unsupported connector chains before they turn into a deep recursive dead
# end, while keeping placement state simple and deterministic.
@ -356,6 +385,54 @@ func _select_next_cell() -> Dictionary:
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:
var changed := true
while changed:
@ -568,6 +645,11 @@ func _candidate_can_occupy_cell(
if not _definition_has_capacity(candidate.definition):
return false
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_index := center.y * grid_size.x + center.x
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(
variant: TerrainChunkVariant,
coordinate: Vector2i,
) -> bool:
if (
variant.definition.must_be_interior
and _coordinate_is_on_boundary(coordinate)
):
return false
var ocean_edges := variant.rotated_edge_mask(
variant.definition.ocean_facing_edges
)
@ -1231,7 +1426,7 @@ func _build_solution_root() -> Node3D:
if variant == null:
continue
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var chunk_root := variant.definition.packed_scene.instantiate() as Node3D
var chunk_root := _instantiate_chunk_root(variant.definition)
if chunk_root == null:
push_error("%s does not instantiate as Node3D." % variant.stable_key())
solution_root.free()
@ -1260,6 +1455,27 @@ func _build_solution_root() -> Node3D:
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:
_clear_generated_chunks()
_generated_chunks = solution_root
@ -1274,11 +1490,34 @@ func _add_collision(
chunk_root,
definition.primary_mesh_name,
)
_add_collision_to_mesh(
primary_mesh,
definition.stable_id,
"TerrainCollision",
)
if definition.base_layer_scene == null:
return
var base_layer_mesh := TerrainChunkAnalyzer.find_primary_mesh(
chunk_root,
definition.base_layer_mesh_name,
)
_add_collision_to_mesh(
base_layer_mesh,
definition.stable_id,
"TerrainBaseLayerCollision",
)
func _add_collision_to_mesh(
primary_mesh: MeshInstance3D,
stable_id: StringName,
collision_name: String,
) -> void:
if primary_mesh == null or primary_mesh.mesh == null:
return
var terrain_shape := primary_mesh.mesh.create_trimesh_shape()
if terrain_shape == null or terrain_shape.get_faces().is_empty():
push_warning("%s produced no terrain collision." % definition.stable_id)
push_warning("%s produced no terrain collision." % stable_id)
return
var concave_shape := terrain_shape as ConcavePolygonShape3D
if concave_shape != null:
@ -1287,7 +1526,7 @@ func _add_collision(
# can never become an invisible collision gap.
concave_shape.backface_collision = true
var collision_body := StaticBody3D.new()
collision_body.name = "TerrainCollision"
collision_body.name = collision_name
collision_body.collision_layer = 1
collision_body.collision_mask = 0
primary_mesh.add_child(collision_body)

View file

@ -82,4 +82,23 @@ func validation_errors() -> PackedStringArray:
"%s has a negative box collision dimension."
% 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

View file

@ -18,6 +18,15 @@ extends Resource
@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(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, 20.0, 0.05) var collision_height := 0.0
@export var collision_box_size := Vector3.ZERO