diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 3a7a297..501deb9 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -94,11 +94,13 @@ func _validate_generated_region( assert(_placement_count(placements, "chunk_0001") >= 1) assert(_placement_count(placements, "chunk_0002") >= 1) assert(_placement_count(placements, "chunk_0003") >= 1) + assert(_placement_count(placements, "chunk_0016") >= 1) assert(_placement_count(placements, "chunk_0004") >= 1) assert(_placement_count(placements, "chunk_0005") >= 1) assert(_placement_count(placements, "chunk_0006") >= 1) assert(_placement_count(placements, "chunk_0007") >= 1) assert(_placement_count(placements, "chunk_0008") >= 1) + assert(_placement_count(placements, "chunk_0013") >= 1) assert(_placement_count(placements, "chunk_0009") == 1) assert(_placement_count(placements, "chunk_0010") == 4) assert( @@ -350,7 +352,9 @@ func _validate_biome_catalog( var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO) var biome_id := region.get_biome_at(coordinate) var tags: PackedStringArray = record.get("tags", PackedStringArray()) - if "grass" in tags or "sand" in tags: + if "mixed_surface" in tags: + assert(biome_id == &"") + elif "grass" in tags or "sand" in tags: assert(biome_id != &"") var biome := catalog.definition_for_id(biome_id) assert(biome != null and biome.supports_chunk_tags(tags)) @@ -465,11 +469,14 @@ func _validate_authored_chunk_surfaces( ) -> void: var generated := generator.get_generated_chunks_root() var found_beach := false + var found_stream := false + var found_alternate_stream := false var found_pond := false var found_grass_ocean_edge := false var found_grass_beach_transition := false var found_grass_ocean_corner := false var found_beach_ocean_corner := false + var found_grass_sand_diagonal := false for chunk_root: Node in generated.get_children(): if chunk_root.name.begins_with("chunk_0002r"): var beach := chunk_root.find_child( @@ -479,6 +486,27 @@ func _validate_authored_chunk_surfaces( var material := beach.get_active_material(0) assert(material != null and material.resource_name == "sand") found_beach = true + elif ( + chunk_root.name.begins_with("chunk_0003r") + or chunk_root.name.begins_with("chunk_0016r") + ): + var stream_id := ( + &"chunk_0016" + if chunk_root.name.begins_with("chunk_0016r") + else &"chunk_0003" + ) + var stream := chunk_root.find_child( + String(stream_id), true, false + ) as MeshInstance3D + assert(stream != null and stream.mesh != null) + var stream_materials := _material_names(stream) + assert("dirt" in stream_materials) + assert("grass_lite" in stream_materials) + assert("dirt_wall" in stream_materials) + if stream_id == &"chunk_0016": + found_alternate_stream = true + else: + found_stream = true elif chunk_root.name.begins_with("chunk_0004r"): var pond := chunk_root.find_child( "chunk_0004", true, false @@ -532,12 +560,24 @@ func _validate_authored_chunk_surfaces( var beach_corner_materials := _material_names(beach_ocean_corner) assert("sand" in beach_corner_materials) found_beach_ocean_corner = true + elif chunk_root.name.begins_with("chunk_0013r"): + var grass_sand_diagonal := chunk_root.find_child( + "chunk_0013", true, false + ) as MeshInstance3D + assert(grass_sand_diagonal != null and grass_sand_diagonal.mesh != null) + var diagonal_materials := _material_names(grass_sand_diagonal) + assert("grass_lite" in diagonal_materials) + assert("sand" in diagonal_materials) + found_grass_sand_diagonal = true assert(found_beach) + assert(found_stream) + assert(found_alternate_stream) assert(found_pond) assert(found_grass_ocean_edge) assert(found_grass_beach_transition) assert(found_grass_ocean_corner) assert(found_beach_ocean_corner) + assert(found_grass_sand_diagonal) func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray: diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd b/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd index 04976a9..94b557e 100644 --- a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd +++ b/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd @@ -122,4 +122,5 @@ func _make_generator() -> TerrainChunkGenerator: "chunk_0008", ] ) + generator.grass_sand_smoothing_enabled = true return generator diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn index 7e12544..34a74e2 100644 --- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn +++ b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn @@ -27,6 +27,7 @@ build_collision = true show_chunk_labels = false force_center_chunk_id = &"chunk_spawn" required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008") +grass_sand_smoothing_enabled = true elevated_cliff_feature_enabled = true [node name="Player" parent="." instance=ExtResource("4_player")] diff --git a/tests/terrain_chunk_generator_validation.gd b/tests/terrain_chunk_generator_validation.gd index 467c154..b583a1f 100644 --- a/tests/terrain_chunk_generator_validation.gd +++ b/tests/terrain_chunk_generator_validation.gd @@ -17,6 +17,10 @@ const EXPECTED_IDS: Array[String] = [ "chunk_0010", "chunk_0011", "chunk_0012", + "chunk_0013", + "chunk_0014", + "chunk_0015", + "chunk_0016", "chunk_spawn", ] const REQUIRED_IDS: Array[String] = [ @@ -32,7 +36,7 @@ const REQUIRED_IDS: Array[String] = [ ] const TEST_SEED := 13001 const CONNECTOR_STRESS_SEED := 287 -const EXPECTED_SOLVER_VARIANT_COUNT := 31 +const EXPECTED_SOLVER_VARIANT_COUNT := 35 const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = { &"chunk_0000": 4, &"chunk_0001": 4, @@ -47,6 +51,10 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = { &"chunk_0010": 4, &"chunk_0011": 4, &"chunk_0012": 4, + &"chunk_0013": 4, + &"chunk_0014": 4, + &"chunk_0015": 4, + &"chunk_0016": 4, &"chunk_spawn": 1, } @@ -60,6 +68,7 @@ func _initialize() -> void: func _run() -> void: _validate_catalog() _validate_profiles() + _validate_second_tier_coastal_layout() _validate_generation() _finish() @@ -87,6 +96,10 @@ func _validate_catalog() -> void: 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 grass_sand_diagonal := CATALOG.definition_for_id(&"chunk_0013") + var cliff_sea_edge := CATALOG.definition_for_id(&"chunk_0014") + var cliff_sea_corner := CATALOG.definition_for_id(&"chunk_0015") + var stream_variant := CATALOG.definition_for_id(&"chunk_0016") var cliff_top := CATALOG.definition_for_id(&"chunk_0009") var cliff_corner := CATALOG.definition_for_id(&"chunk_0010") var cliff_edge := CATALOG.definition_for_id(&"chunk_0011") @@ -101,6 +114,10 @@ func _validate_catalog() -> void: and grass_beach_transition != null and grass_ocean_corner != null and beach_ocean_corner != null + and grass_sand_diagonal != null + and cliff_sea_edge != null + and cliff_sea_corner != null + and stream_variant != null and cliff_top != null and cliff_corner != null and cliff_edge != null @@ -117,6 +134,10 @@ func _validate_catalog() -> void: and grass_beach_transition != null and grass_ocean_corner != null and beach_ocean_corner != null + and grass_sand_diagonal != null + and cliff_sea_edge != null + and cliff_sea_corner != null + and stream_variant != null and cliff_top != null and cliff_corner != null and cliff_edge != null @@ -235,6 +256,48 @@ func _validate_catalog() -> void: ), "The beach corner must join two straight beach ocean edges.", ) + _check( + grass_sand_diagonal.must_be_interior + and not grass_sand_diagonal.participates_in_base_solver + and "mixed_surface" in grass_sand_diagonal.tags, + ( + "The grass-sand diagonal must remain an inland post-process " + + "mixed-surface chunk." + ), + ) + _check( + grass_sand_diagonal.surface_tags_for_edge( + TerrainChunkTopology.Edge.NORTH + ) == PackedStringArray(["sand"]) + and grass_sand_diagonal.surface_tags_for_edge( + TerrainChunkTopology.Edge.WEST + ) == PackedStringArray(["sand"]) + and grass_sand_diagonal.surface_tags_for_edge( + TerrainChunkTopology.Edge.EAST + ) == PackedStringArray(["grass"]) + and grass_sand_diagonal.surface_tags_for_edge( + TerrainChunkTopology.Edge.SOUTH + ) == PackedStringArray(["grass"]), + "The diagonal's authored edges must retain two sand and two grass sides.", + ) + var stream := CATALOG.definition_for_id(&"chunk_0003") + _check( + stream != null + and is_equal_approx(stream.selection_weight, 0.3) + and is_equal_approx(stream_variant.selection_weight, 0.3) + and stream_variant.tags == stream.tags + and stream_variant.allowed_neighbor_tags + == stream.allowed_neighbor_tags + and stream_variant.water_inlet_edges == stream.water_inlet_edges + and stream_variant.water_outlet_edges == stream.water_outlet_edges + and stream_variant.water_surface_size == stream.water_surface_size + and stream_variant.water_surface_offset + == stream.water_surface_offset, + ( + "Both stream visuals must share one combined selection weight " + + "and identical terrain/water behavior." + ), + ) for cliff_definition: TerrainChunkDefinition in [ cliff_top, cliff_corner, @@ -248,6 +311,43 @@ func _validate_catalog() -> void: and cliff_definition.base_layer_mesh_name == &"chunk_0000", "Every second-tier piece must overlay level-one grass inland.", ) + for coastal_cliff: TerrainChunkDefinition in [ + cliff_sea_edge, + cliff_sea_corner, + ]: + _check( + coastal_cliff.overlay_only + and coastal_cliff.base_layer_scene == null + and not coastal_cliff.must_be_interior + and coastal_cliff.prefers_map_boundary + and "coast" in coastal_cliff.tags + and "elevation_2_sea_edge" in coastal_cliff.tags, + ( + "Each second-tier sea piece must be a complete coastal " + + "overlay without an inland grass base." + ), + ) + _check( + cliff_sea_edge.ocean_facing_edges + == (1 << int(TerrainChunkTopology.Edge.EAST)), + "The second-tier sea edge must face east before rotation.", + ) + _check( + cliff_sea_corner.ocean_facing_edges + == ( + (1 << int(TerrainChunkTopology.Edge.EAST)) + | (1 << int(TerrainChunkTopology.Edge.SOUTH)) + ), + "The second-tier sea corner must face east and south before rotation.", + ) + _check( + cliff_sea_corner.minimum_required_neighbors == 2 + and ( + "elevation_2_sea_edge" + in cliff_sea_corner.required_neighbor_tags + ), + "The second-tier sea corner must join two straight sea edges.", + ) _check( cliff_top.minimum_required_neighbors == 4 and "elevation_2" in cliff_top.required_neighbor_tags, @@ -293,6 +393,91 @@ func _validate_profiles() -> void: ) +func _validate_second_tier_coastal_layout() -> void: + var generator := TerrainChunkGenerator.new() + generator.catalog = CATALOG + generator.grid_size = Vector2i(4, 4) + generator.generate_on_ready = false + generator.build_collision = false + generator.force_center_chunk_id = &"" + root.add_child(generator) + var keys := PackedStringArray() + for row: int in generator.grid_size.y: + for column: int in generator.grid_size.x: + var at_north := row == 0 + var at_east := column == generator.grid_size.x - 1 + var at_south := row == generator.grid_size.y - 1 + var at_west := column == 0 + if at_north and at_west: + keys.append("chunk_0015@2") + elif at_north and at_east: + keys.append("chunk_0015@1") + elif at_south and at_east: + keys.append("chunk_0015@0") + elif at_south and at_west: + keys.append("chunk_0015@3") + elif at_north: + keys.append("chunk_0014@1") + elif at_east: + keys.append("chunk_0014@0") + elif at_south: + keys.append("chunk_0014@3") + elif at_west: + keys.append("chunk_0014@2") + else: + keys.append("chunk_0009@0") + _check( + generator.generate_from_placement_keys(keys), + ( + "The second-tier sea edge and corner must form a complete " + + "raised coastal perimeter." + ), + ) + if generator.placement_keys().size() == keys.size(): + _check( + _all_neighbor_edges_match(generator), + "Every raised coastal-perimeter seam must match.", + ) + _check( + _count_placements(generator.placement_keys(), "chunk_0014") == 8 + and _count_placements( + generator.placement_keys(), "chunk_0015" + ) == 4, + "The raised coastal perimeter must use its straight and corner pieces.", + ) + for record: Dictionary in generator.placement_records(): + var stable_id: StringName = record.get("stable_id", &"") + if stable_id not in [&"chunk_0014", &"chunk_0015"]: + continue + var coordinate: Vector2i = record.get( + "coordinate", Vector2i.ZERO + ) + var variant := _find_variant( + generator, + stable_id, + int(record.get("rotation_quarters", 0)), + ) + _check( + variant != null + and generator._variant_respects_ocean_boundary( + variant, coordinate + ), + "Every second-tier sea opening must face outside the terrain grid.", + ) + var generated := generator.get_generated_chunks_root() + if generated != null: + for child: Node in generated.get_children(): + var stable_id := StringName( + child.get_meta(&"terrain_chunk_id", &"") + ) + if stable_id in [&"chunk_0014", &"chunk_0015"]: + _check( + child.get_node_or_null("TerrainBaseLayer") == null, + "Second-tier sea pieces must not render inland grass over ocean.", + ) + generator.free() + + func _validate_generation() -> void: var first := _make_generator() root.add_child(first) @@ -308,6 +493,10 @@ func _validate_generation() -> void: _placements_contain(first_keys, stable_id), "Generated diagnostic must contain %s." % stable_id, ) + _check( + _placements_contain(first_keys, "chunk_0013"), + "Generated terrain must replace sharp grass-sand corners with diagonals.", + ) _check( _all_neighbor_edges_match(first), "Every generated neighboring edge must match.", @@ -416,8 +605,12 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ) var stream := CATALOG.definition_for_id(&"chunk_0003") - _check(stream != null, "The stream definition must be available.") - if stream == null: + var alternate_stream := CATALOG.definition_for_id(&"chunk_0016") + _check( + stream != null and alternate_stream != null, + "Both stream visual definitions must be available.", + ) + if stream == null or alternate_stream == null: return for quarter_turns: int in 4: var stream_variant := _find_variant( @@ -429,7 +622,16 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: stream_variant != null, "The stream must support rotation %d." % quarter_turns, ) - if stream_variant == null: + var alternate_variant := _find_variant( + generator, + &"chunk_0016", + quarter_turns, + ) + _check( + alternate_variant != null, + "The alternate stream must support rotation %d." % quarter_turns, + ) + if stream_variant == null or alternate_variant == null: continue var expected_inlet := 1 << int(TerrainChunkTopology.rotated_edge( TerrainChunkTopology.Edge.NORTH, @@ -449,6 +651,25 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: == expected_outlet, "The rotated stream outlet must follow its authored orientation.", ) + _check( + alternate_variant.rotated_edge_mask( + alternate_stream.water_inlet_edges + ) == expected_inlet + and alternate_variant.rotated_edge_mask( + alternate_stream.water_outlet_edges + ) == expected_outlet, + "The alternate stream connectors must rotate exactly like the original.", + ) + for edge_value: int in TerrainChunkTopology.Edge.values(): + var edge := edge_value as TerrainChunkTopology.Edge + _check( + stream_variant.profile(edge).signature( + TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION + ) == alternate_variant.profile(edge).signature( + TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION + ), + "Both stream visuals must expose identical terrain edge profiles.", + ) var beach := CATALOG.definition_for_id(&"chunk_0002") _check(beach != null, "The beach definition must be available.") @@ -611,6 +832,62 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "The beach corner's west side must join a rotated straight beach.", ) + var cliff_top_zero := _find_variant(generator, &"chunk_0009", 0) + var cliff_sea_edge_zero := _find_variant(generator, &"chunk_0014", 0) + var cliff_sea_edge_south := _find_variant(generator, &"chunk_0014", 3) + var cliff_sea_corner_zero := _find_variant(generator, &"chunk_0015", 0) + _check( + cliff_top_zero != null + and cliff_sea_edge_zero != null + and cliff_sea_edge_south != null + and cliff_sea_corner_zero != null, + "The second-tier sea edge variants must be available.", + ) + if ( + cliff_top_zero != null + and cliff_sea_edge_zero != null + and cliff_sea_edge_south != null + and cliff_sea_corner_zero != null + ): + _check( + cliff_sea_edge_zero.profile( + TerrainChunkTopology.Edge.EAST + ).points.is_empty(), + "The second-tier sea edge must remain open on its ocean side.", + ) + _check( + cliff_sea_corner_zero.profile( + TerrainChunkTopology.Edge.EAST + ).points.is_empty() + and cliff_sea_corner_zero.profile( + TerrainChunkTopology.Edge.SOUTH + ).points.is_empty(), + "The second-tier sea corner must retain both ocean openings.", + ) + _check( + generator._edges_are_compatible( + cliff_sea_edge_zero, + TerrainChunkTopology.Edge.WEST, + cliff_top_zero, + TerrainChunkTopology.Edge.EAST, + ), + "The second-tier sea edge must join the raised grass top inland.", + ) + _check( + generator._edges_are_compatible( + cliff_sea_corner_zero, + TerrainChunkTopology.Edge.NORTH, + cliff_sea_edge_zero, + TerrainChunkTopology.Edge.SOUTH, + ) + and generator._edges_are_compatible( + cliff_sea_corner_zero, + TerrainChunkTopology.Edge.WEST, + cliff_sea_edge_south, + TerrainChunkTopology.Edge.EAST, + ), + "The second-tier sea corner must join two straight sea edges.", + ) var flat_grass := _find_variant(generator, &"chunk_0000", 0) var flat_sand := _find_variant(generator, &"chunk_0001", 0) _check( @@ -649,11 +926,57 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "No transition rotation may accept sand behind it.", ) + for quarter_turns: int in 4: + var diagonal := _find_variant( + generator, + &"chunk_0013", + quarter_turns, + ) + _check( + diagonal != null, + "The grass-sand diagonal must support rotation %d." + % quarter_turns, + ) + if diagonal == null: + continue + for source_edge_value: int in TerrainChunkTopology.Edge.values(): + var source_edge := source_edge_value as TerrainChunkTopology.Edge + var edge := TerrainChunkTopology.rotated_edge( + source_edge, + quarter_turns, + ) + var neighbor_edge := TerrainChunkTopology.opposite_edge(edge) + var expects_sand := source_edge in [ + TerrainChunkTopology.Edge.NORTH, + TerrainChunkTopology.Edge.WEST, + ] + var expected := flat_sand if expects_sand else flat_grass + var rejected := flat_grass if expects_sand else flat_sand + _check( + generator._edges_are_compatible( + diagonal, + edge, + expected, + neighbor_edge, + ), + "Each rotated diagonal edge must accept its authored surface.", + ) + _check( + not generator._edges_are_compatible( + diagonal, + edge, + rejected, + neighbor_edge, + ), + "No rotated diagonal edge may accept the opposite surface.", + ) for stable_id: StringName in [ &"chunk_0005", &"chunk_0006", &"chunk_0007", &"chunk_0008", + &"chunk_0014", + &"chunk_0015", ]: for quarter_turns: int in 4: var coast_variant := _find_variant( @@ -674,15 +997,12 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "%s must never place its ocean edge inside the map." % stable_id, ) - if stable_id != &"chunk_0007" and stable_id != &"chunk_0008": + var coast_definition := CATALOG.definition_for_id(stable_id) + if coast_definition == null: continue var boundary_coordinate := Vector2i(3, 3) var ocean_edges := coast_variant.rotated_edge_mask( - ( - grass_ocean_corner.ocean_facing_edges - if stable_id == &"chunk_0007" - else beach_ocean_corner.ocean_facing_edges - ) + coast_definition.ocean_facing_edges ) for edge_value: int in TerrainChunkTopology.Edge.values(): if (ocean_edges & (1 << edge_value)) == 0: @@ -701,15 +1021,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: coast_variant, boundary_coordinate, ), - "Each coast corner rotation must fit its matching map corner.", + "Each coast rotation must fit its matching open map boundary.", ) func _validate_generation_summary(generator: TerrainChunkGenerator) -> void: var summary := generator._build_summary() _check( - int(summary.get("variant_count", 0)) == 53, - "The current catalog must expose all 53 authored rotations.", + int(summary.get("variant_count", 0)) == 69, + "The current catalog must expose all 69 authored rotations.", ) _check( int(summary.get("solver_variant_count", 0)) @@ -762,6 +1082,7 @@ func _make_generator() -> TerrainChunkGenerator: generator.build_collision = false generator.force_center_chunk_id = &"chunk_spawn" generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS) + generator.grass_sand_smoothing_enabled = true generator.maximum_backtracks = 100000 return generator diff --git a/tools/blender/export_terrain_chunks.py b/tools/blender/export_terrain_chunks.py index 654227e..02c7b95 100644 --- a/tools/blender/export_terrain_chunks.py +++ b/tools/blender/export_terrain_chunks.py @@ -282,15 +282,21 @@ 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. - # 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 + # Elevated inland cliff overlays also stop at their downhill edge, + # while second-tier sea pieces carry that edge down to ocean depth. + layered_cliff = "cliff_2" in label + allow_open_east_edge = ( + "ocean_edge" in label + or "sea_edge" in label + or "sea_corner" in label + or layered_cliff + ) # Corner pieces additionally leave canonical negative Y open. Their - # two marked ocean edges rotate together at runtime. + # two marked outer edges rotate together at runtime. allow_open_south_edge = ( - "ocean_edge_corner" in label or layered_inland_cliff + "ocean_edge_corner" in label + or "sea_corner" in label + or (layered_cliff and "corner" in label) ) bound_problems = _validate_bounds( bounds, diff --git a/world/generation/biomes/definitions/biome_coast.tres b/world/generation/biomes/definitions/biome_coast.tres index d1864d3..635181b 100644 --- a/world/generation/biomes/definitions/biome_coast.tres +++ b/world/generation/biomes/definitions/biome_coast.tres @@ -17,4 +17,5 @@ script = ExtResource("1_definition") stable_id = &"biome_coast" label = "coast" required_chunk_tags = PackedStringArray("sand") +forbidden_chunk_tags = PackedStringArray("mixed_surface") prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_sand_tree")]) diff --git a/world/generation/biomes/definitions/biome_forest.tres b/world/generation/biomes/definitions/biome_forest.tres index a660042..21cd724 100644 --- a/world/generation/biomes/definitions/biome_forest.tres +++ b/world/generation/biomes/definitions/biome_forest.tres @@ -27,6 +27,6 @@ script = ExtResource("1_definition") stable_id = &"biome_forest" label = "forest" required_chunk_tags = PackedStringArray("grass") -forbidden_chunk_tags = PackedStringArray("spawn") +forbidden_chunk_tags = PackedStringArray("spawn", "mixed_surface") region_weight = 1.4 prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")]) diff --git a/world/generation/biomes/definitions/biome_pine_forest.tres b/world/generation/biomes/definitions/biome_pine_forest.tres index 6601a46..eec39a4 100644 --- a/world/generation/biomes/definitions/biome_pine_forest.tres +++ b/world/generation/biomes/definitions/biome_pine_forest.tres @@ -27,6 +27,6 @@ script = ExtResource("1_definition") stable_id = &"biome_pine_forest" label = "pine forest" required_chunk_tags = PackedStringArray("grass") -forbidden_chunk_tags = PackedStringArray("spawn") +forbidden_chunk_tags = PackedStringArray("spawn", "mixed_surface") region_weight = 1.2 prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")]) diff --git a/world/generation/biomes/definitions/biome_plains.tres b/world/generation/biomes/definitions/biome_plains.tres index c979cb8..46b8c71 100644 --- a/world/generation/biomes/definitions/biome_plains.tres +++ b/world/generation/biomes/definitions/biome_plains.tres @@ -24,5 +24,6 @@ script = ExtResource("1_definition") stable_id = &"biome_plains" label = "plains" required_chunk_tags = PackedStringArray("grass") +forbidden_chunk_tags = PackedStringArray("mixed_surface") region_weight = 1.0 prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")]) diff --git a/world/generation/chunks/assets/chunk_0001.glb b/world/generation/chunks/assets/chunk_0001.glb index 0b9bc50..6014230 100644 Binary files a/world/generation/chunks/assets/chunk_0001.glb and b/world/generation/chunks/assets/chunk_0001.glb differ diff --git a/world/generation/chunks/assets/chunk_0002.glb b/world/generation/chunks/assets/chunk_0002.glb index db18d8c..2a36b6e 100644 Binary files a/world/generation/chunks/assets/chunk_0002.glb and b/world/generation/chunks/assets/chunk_0002.glb differ diff --git a/world/generation/chunks/assets/chunk_0003.glb b/world/generation/chunks/assets/chunk_0003.glb index 45708a3..a1996eb 100644 Binary files a/world/generation/chunks/assets/chunk_0003.glb and b/world/generation/chunks/assets/chunk_0003.glb differ diff --git a/world/generation/chunks/assets/chunk_0004.glb b/world/generation/chunks/assets/chunk_0004.glb index 62219b9..2839d03 100644 Binary files a/world/generation/chunks/assets/chunk_0004.glb and b/world/generation/chunks/assets/chunk_0004.glb differ diff --git a/world/generation/chunks/assets/chunk_0011.glb b/world/generation/chunks/assets/chunk_0011.glb index fa36cff..85f73ed 100644 Binary files a/world/generation/chunks/assets/chunk_0011.glb and b/world/generation/chunks/assets/chunk_0011.glb differ diff --git a/world/generation/chunks/assets/chunk_0013.glb b/world/generation/chunks/assets/chunk_0013.glb new file mode 100644 index 0000000..b5aa63c Binary files /dev/null and b/world/generation/chunks/assets/chunk_0013.glb differ diff --git a/world/generation/chunks/assets/chunk_0013.glb.import b/world/generation/chunks/assets/chunk_0013.glb.import new file mode 100644 index 0000000..b63b3e4 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0013.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dom2fvsn0s1wa" +path="res://.godot/imported/chunk_0013.glb-64cf2d2bb9c9020fa3d06004b1963f09.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0013.glb" +dest_files=["res://.godot/imported/chunk_0013.glb-64cf2d2bb9c9020fa3d06004b1963f09.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 diff --git a/world/generation/chunks/assets/chunk_0013_grass_lite.png b/world/generation/chunks/assets/chunk_0013_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0013_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0013_grass_lite.png.import b/world/generation/chunks/assets/chunk_0013_grass_lite.png.import new file mode 100644 index 0000000..95218ff --- /dev/null +++ b/world/generation/chunks/assets/chunk_0013_grass_lite.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dct65l02vr54q" +path.s3tc="res://.godot/imported/chunk_0013_grass_lite.png-0cf332d69bdd6f5da588e0ddda3bd437.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0013_grass_lite.png-0cf332d69bdd6f5da588e0ddda3bd437.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0013_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0013_grass_lite.png-0cf332d69bdd6f5da588e0ddda3bd437.s3tc.ctex", "res://.godot/imported/chunk_0013_grass_lite.png-0cf332d69bdd6f5da588e0ddda3bd437.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 diff --git a/world/generation/chunks/assets/chunk_0013_sand.png b/world/generation/chunks/assets/chunk_0013_sand.png new file mode 100644 index 0000000..06f807a Binary files /dev/null and b/world/generation/chunks/assets/chunk_0013_sand.png differ diff --git a/world/generation/chunks/assets/chunk_0013_sand.png.import b/world/generation/chunks/assets/chunk_0013_sand.png.import new file mode 100644 index 0000000..cd4178a --- /dev/null +++ b/world/generation/chunks/assets/chunk_0013_sand.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btjhnb6owbjj2" +path.s3tc="res://.godot/imported/chunk_0013_sand.png-5fd756543742405c3864f1fdfb36632c.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0013_sand.png-5fd756543742405c3864f1fdfb36632c.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_0013_sand.png" +dest_files=["res://.godot/imported/chunk_0013_sand.png-5fd756543742405c3864f1fdfb36632c.s3tc.ctex", "res://.godot/imported/chunk_0013_sand.png-5fd756543742405c3864f1fdfb36632c.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 diff --git a/world/generation/chunks/assets/chunk_0014.glb b/world/generation/chunks/assets/chunk_0014.glb new file mode 100644 index 0000000..13eef5a Binary files /dev/null and b/world/generation/chunks/assets/chunk_0014.glb differ diff --git a/world/generation/chunks/assets/chunk_0014.glb.import b/world/generation/chunks/assets/chunk_0014.glb.import new file mode 100644 index 0000000..3d4bcb8 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0014.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://50aolv5pg8d2" +path="res://.godot/imported/chunk_0014.glb-636ccc776ff9575e58a9db68c3f273af.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0014.glb" +dest_files=["res://.godot/imported/chunk_0014.glb-636ccc776ff9575e58a9db68c3f273af.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 diff --git a/world/generation/chunks/assets/chunk_0014_cliff_wall.png b/world/generation/chunks/assets/chunk_0014_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0014_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0014_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0014_cliff_wall.png.import new file mode 100644 index 0000000..45e466a --- /dev/null +++ b/world/generation/chunks/assets/chunk_0014_cliff_wall.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://6sd1uj06ti8f" +path.s3tc="res://.godot/imported/chunk_0014_cliff_wall.png-9ea694f83659f89e6c17ae1a39cf97cb.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0014_cliff_wall.png-9ea694f83659f89e6c17ae1a39cf97cb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "fc97184f537cc23f83a731327e10c836" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0014_cliff_wall.png" +dest_files=["res://.godot/imported/chunk_0014_cliff_wall.png-9ea694f83659f89e6c17ae1a39cf97cb.s3tc.ctex", "res://.godot/imported/chunk_0014_cliff_wall.png-9ea694f83659f89e6c17ae1a39cf97cb.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 diff --git a/world/generation/chunks/assets/chunk_0014_grass_lite.png b/world/generation/chunks/assets/chunk_0014_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0014_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0014_grass_lite.png.import b/world/generation/chunks/assets/chunk_0014_grass_lite.png.import new file mode 100644 index 0000000..d4c09fe --- /dev/null +++ b/world/generation/chunks/assets/chunk_0014_grass_lite.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b6fftn8x2o1dd" +path.s3tc="res://.godot/imported/chunk_0014_grass_lite.png-90b3bea1cb09cf8dbae98afc39c1f5cd.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0014_grass_lite.png-90b3bea1cb09cf8dbae98afc39c1f5cd.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0014_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0014_grass_lite.png-90b3bea1cb09cf8dbae98afc39c1f5cd.s3tc.ctex", "res://.godot/imported/chunk_0014_grass_lite.png-90b3bea1cb09cf8dbae98afc39c1f5cd.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 diff --git a/world/generation/chunks/assets/chunk_0015.glb b/world/generation/chunks/assets/chunk_0015.glb new file mode 100644 index 0000000..2c42918 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0015.glb differ diff --git a/world/generation/chunks/assets/chunk_0015.glb.import b/world/generation/chunks/assets/chunk_0015.glb.import new file mode 100644 index 0000000..7078d3b --- /dev/null +++ b/world/generation/chunks/assets/chunk_0015.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://d26fd6sjsh50x" +path="res://.godot/imported/chunk_0015.glb-0a28f27956d19c7965f7bc9c838f1b29.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0015.glb" +dest_files=["res://.godot/imported/chunk_0015.glb-0a28f27956d19c7965f7bc9c838f1b29.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 diff --git a/world/generation/chunks/assets/chunk_0015_cliff_wall.png b/world/generation/chunks/assets/chunk_0015_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0015_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0015_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0015_cliff_wall.png.import new file mode 100644 index 0000000..560f5b4 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0015_cliff_wall.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cpce75ti5cwnw" +path.s3tc="res://.godot/imported/chunk_0015_cliff_wall.png-08e30f267a5816ed94aa39dc21ccf05c.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0015_cliff_wall.png-08e30f267a5816ed94aa39dc21ccf05c.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "fc97184f537cc23f83a731327e10c836" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0015_cliff_wall.png" +dest_files=["res://.godot/imported/chunk_0015_cliff_wall.png-08e30f267a5816ed94aa39dc21ccf05c.s3tc.ctex", "res://.godot/imported/chunk_0015_cliff_wall.png-08e30f267a5816ed94aa39dc21ccf05c.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 diff --git a/world/generation/chunks/assets/chunk_0015_grass_lite.png b/world/generation/chunks/assets/chunk_0015_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0015_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0015_grass_lite.png.import b/world/generation/chunks/assets/chunk_0015_grass_lite.png.import new file mode 100644 index 0000000..1ad5aa3 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0015_grass_lite.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pmaf2oetdqln" +path.s3tc="res://.godot/imported/chunk_0015_grass_lite.png-0ae072e9d3b6d68d7fdb493a6acf3bad.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0015_grass_lite.png-0ae072e9d3b6d68d7fdb493a6acf3bad.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0015_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0015_grass_lite.png-0ae072e9d3b6d68d7fdb493a6acf3bad.s3tc.ctex", "res://.godot/imported/chunk_0015_grass_lite.png-0ae072e9d3b6d68d7fdb493a6acf3bad.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 diff --git a/world/generation/chunks/assets/chunk_0016.glb b/world/generation/chunks/assets/chunk_0016.glb new file mode 100644 index 0000000..f4428f1 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0016.glb differ diff --git a/world/generation/chunks/assets/chunk_0016.glb.import b/world/generation/chunks/assets/chunk_0016.glb.import new file mode 100644 index 0000000..bc947f3 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0016.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://te1yrm2uhin1" +path="res://.godot/imported/chunk_0016.glb-6904d8b3583dd301bda1835c501225f3.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0016.glb" +dest_files=["res://.godot/imported/chunk_0016.glb-6904d8b3583dd301bda1835c501225f3.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 diff --git a/world/generation/chunks/assets/chunk_0016_dirt.png b/world/generation/chunks/assets/chunk_0016_dirt.png new file mode 100644 index 0000000..588c8c6 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0016_dirt.png differ diff --git a/world/generation/chunks/assets/chunk_0016_dirt.png.import b/world/generation/chunks/assets/chunk_0016_dirt.png.import new file mode 100644 index 0000000..00b774f --- /dev/null +++ b/world/generation/chunks/assets/chunk_0016_dirt.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dxy8u3vyyu2on" +path.s3tc="res://.godot/imported/chunk_0016_dirt.png-5f2ccbadabdb1e395518abce5c09d66f.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0016_dirt.png-5f2ccbadabdb1e395518abce5c09d66f.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "07d90b3c46a4c3d0c172bed143dc9436" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0016_dirt.png" +dest_files=["res://.godot/imported/chunk_0016_dirt.png-5f2ccbadabdb1e395518abce5c09d66f.s3tc.ctex", "res://.godot/imported/chunk_0016_dirt.png-5f2ccbadabdb1e395518abce5c09d66f.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 diff --git a/world/generation/chunks/assets/chunk_0016_dirt_wall.png b/world/generation/chunks/assets/chunk_0016_dirt_wall.png new file mode 100644 index 0000000..dcc2364 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0016_dirt_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0016_dirt_wall.png.import b/world/generation/chunks/assets/chunk_0016_dirt_wall.png.import new file mode 100644 index 0000000..373e8cf --- /dev/null +++ b/world/generation/chunks/assets/chunk_0016_dirt_wall.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cn8ljtpbsg253" +path.s3tc="res://.godot/imported/chunk_0016_dirt_wall.png-536115f36dddb3063c7b5162468a4876.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0016_dirt_wall.png-536115f36dddb3063c7b5162468a4876.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "b5430dba0b52db87d30bae85ae640b09" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0016_dirt_wall.png" +dest_files=["res://.godot/imported/chunk_0016_dirt_wall.png-536115f36dddb3063c7b5162468a4876.s3tc.ctex", "res://.godot/imported/chunk_0016_dirt_wall.png-536115f36dddb3063c7b5162468a4876.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 diff --git a/world/generation/chunks/assets/chunk_0016_grass_lite.png b/world/generation/chunks/assets/chunk_0016_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0016_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0016_grass_lite.png.import b/world/generation/chunks/assets/chunk_0016_grass_lite.png.import new file mode 100644 index 0000000..6311cde --- /dev/null +++ b/world/generation/chunks/assets/chunk_0016_grass_lite.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dn84haedw08rx" +path.s3tc="res://.godot/imported/chunk_0016_grass_lite.png-df65a0c79eb0a4e70a36096ead0f76f3.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0016_grass_lite.png-df65a0c79eb0a4e70a36096ead0f76f3.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0016_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0016_grass_lite.png-df65a0c79eb0a4e70a36096ead0f76f3.s3tc.ctex", "res://.godot/imported/chunk_0016_grass_lite.png-df65a0c79eb0a4e70a36096ead0f76f3.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 diff --git a/world/generation/chunks/definitions/chunk_0003.tres b/world/generation/chunks/definitions/chunk_0003.tres index 1a1b3dc..4218357 100644 --- a/world/generation/chunks/definitions/chunk_0003.tres +++ b/world/generation/chunks/definitions/chunk_0003.tres @@ -9,7 +9,7 @@ stable_id = &"chunk_0003" label = "stream" packed_scene = ExtResource("2_scene") primary_mesh_name = &"chunk_0003" -selection_weight = 0.6 +selection_weight = 0.3 tags = PackedStringArray("land", "walkable", "fresh_water", "river", "flowing") allowed_neighbor_tags = PackedStringArray("grass") preferred_neighbor_tags = PackedStringArray("grass") diff --git a/world/generation/chunks/definitions/chunk_0013.tres b/world/generation/chunks/definitions/chunk_0013.tres new file mode 100644 index 0000000..22b6061 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0013.tres @@ -0,0 +1,28 @@ +[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_0013.glb" id="2_scene"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0013" +label = "grass-sand diagonal" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0013" +participates_in_base_solver = false +allowed_rotation_mask = 15 +selection_weight = 0.8 +maximum_placements = 16 +tags = PackedStringArray("land", "walkable", "grass", "sand", "flat", "transition", "mixed_surface") +allowed_neighbor_tags = PackedStringArray("grass", "sand") +forbidden_neighbor_tags = PackedStringArray("fresh_water", "spawn") +north_allowed_neighbor_tags = PackedStringArray("sand") +east_allowed_neighbor_tags = PackedStringArray("grass") +south_allowed_neighbor_tags = PackedStringArray("grass") +west_allowed_neighbor_tags = PackedStringArray("sand") +north_surface_tags = PackedStringArray("sand") +east_surface_tags = PackedStringArray("grass") +south_surface_tags = PackedStringArray("grass") +west_surface_tags = PackedStringArray("sand") +preferred_neighbor_tags = PackedStringArray("grass", "sand", "transition") +must_be_interior = true diff --git a/world/generation/chunks/definitions/chunk_0014.tres b/world/generation/chunks/definitions/chunk_0014.tres new file mode 100644 index 0000000..c3a0d87 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0014.tres @@ -0,0 +1,24 @@ +[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_0014.glb" id="2_scene"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0014" +label = "second-tier grass sea edge" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0014" +overlay_only = true +selection_weight = 0.28 +maximum_placements = 32 +tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "edge", "elevation_2", "elevation_2_border", "elevation_2_sea_edge") +allowed_neighbor_tags = PackedStringArray("elevation_2") +north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge") +south_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge") +west_allowed_neighbor_tags = PackedStringArray("elevation_2_top") +required_neighbor_tags = PackedStringArray("elevation_2_top") +minimum_required_neighbors = 1 +preferred_neighbor_tags = PackedStringArray("elevation_2", "coast") +prefers_map_boundary = true +ocean_facing_edges = 2 diff --git a/world/generation/chunks/definitions/chunk_0015.tres b/world/generation/chunks/definitions/chunk_0015.tres new file mode 100644 index 0000000..d7f0a5a --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0015.tres @@ -0,0 +1,23 @@ +[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_0015.glb" id="2_scene"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0015" +label = "second-tier grass sea corner" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0015" +overlay_only = true +selection_weight = 0.12 +maximum_placements = 16 +tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner", "elevation_2", "elevation_2_border", "elevation_2_sea_edge") +allowed_neighbor_tags = PackedStringArray("elevation_2") +north_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge") +west_allowed_neighbor_tags = PackedStringArray("elevation_2_sea_edge") +required_neighbor_tags = PackedStringArray("elevation_2_sea_edge") +minimum_required_neighbors = 2 +preferred_neighbor_tags = PackedStringArray("elevation_2", "coast") +prefers_map_boundary = true +ocean_facing_edges = 6 diff --git a/world/generation/chunks/definitions/chunk_0016.tres b/world/generation/chunks/definitions/chunk_0016.tres new file mode 100644 index 0000000..804bb34 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0016.tres @@ -0,0 +1,19 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" 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_0016.glb" id="2_scene"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0016" +label = "stream variant" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0016" +selection_weight = 0.3 +tags = PackedStringArray("land", "walkable", "fresh_water", "river", "flowing") +allowed_neighbor_tags = PackedStringArray("grass") +preferred_neighbor_tags = PackedStringArray("grass") +water_inlet_edges = 1 +water_outlet_edges = 4 +water_surface_size = Vector2(2.1, 10) +water_surface_offset = Vector2(-0.315, 0) diff --git a/world/generation/chunks/terrain_chunk_catalog.tres b/world/generation/chunks/terrain_chunk_catalog.tres index b262050..7d577a3 100644 --- a/world/generation/chunks/terrain_chunk_catalog.tres +++ b/world/generation/chunks/terrain_chunk_catalog.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=17 format=3] +[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=21 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"] @@ -16,8 +16,12 @@ [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"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0013.tres" id="17_grass_sand_diagonal"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0014.tres" id="18_cliff_sea_edge"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0015.tres" id="19_cliff_sea_corner"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0016.tres" id="20_stream_variant"] [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("12_beach_ocean_corner"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("8_spawn")]) +definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("20_stream_variant"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("17_grass_sand_diagonal"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("18_cliff_sea_edge"), ExtResource("19_cliff_sea_corner"), ExtResource("8_spawn")]) diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index b9d3a0e..7987172 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -37,6 +37,7 @@ generate_on_ready = false build_collision = true force_center_chunk_id = &"chunk_spawn" required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008") +grass_sand_smoothing_enabled = true elevated_cliff_feature_enabled = true required_chunk_weight_multiplier = 64.0 maximum_backtracks = 100000 diff --git a/world/generation/terrain_chunk_catalog.gd b/world/generation/terrain_chunk_catalog.gd index e6741f1..6271e04 100644 --- a/world/generation/terrain_chunk_catalog.gd +++ b/world/generation/terrain_chunk_catalog.gd @@ -38,10 +38,16 @@ func validation_errors() -> PackedStringArray: "%s must define both base-layer scene and mesh name." % definition.stable_id ) - if definition.overlay_only and definition.base_layer_scene == null: + if ( + definition.overlay_only + and definition.base_layer_scene == null + and definition.ocean_facing_edges == 0 + ): errors.append( - "%s is overlay-only but has no base-layer scene." - % definition.stable_id + ( + "%s is an inland overlay but has no base-layer scene." + % definition.stable_id + ) ) if definition.allowed_rotation_mask == 0: errors.append("%s allows no rotations." % definition.stable_id) diff --git a/world/generation/terrain_chunk_definition.gd b/world/generation/terrain_chunk_definition.gd index 2d4c548..ca38dc3 100644 --- a/world/generation/terrain_chunk_definition.gd +++ b/world/generation/terrain_chunk_definition.gd @@ -12,9 +12,14 @@ extends Resource @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. +## ordinary terrain solve. Inland overlays normally provide base_layer_scene; +## complete coastal overlays can instead carry their own descent to sea level. +## They remain in placement manifests and runtime output, but do not inflate +## every cell's global solver domain. @export var overlay_only := false +## Post-process pieces remain available as authored variants and in placement +## manifests, but do not participate in the ordinary base-terrain solve. +@export var participates_in_base_solver := true @export_group("") @export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees") var allowed_rotation_mask := 15 @@ -38,6 +43,16 @@ var allowed_rotation_mask := 15 @export var south_allowed_neighbor_tags := PackedStringArray() @export var west_allowed_neighbor_tags := PackedStringArray() @export_group("") +## Optional canonical descriptions of the visible terrain surface at each +## edge. Empty edges inherit the definition's ordinary tags. Mixed-surface +## chunks use these to prevent a visually grass edge from joining sand (or the +## reverse) even when the chunk itself carries both biome tags. +@export_group("Directional Surface Rules") +@export var north_surface_tags := PackedStringArray() +@export var east_surface_tags := PackedStringArray() +@export var south_surface_tags := PackedStringArray() +@export var west_surface_tags := PackedStringArray() +@export_group("") ## When non-empty, at least this many in-grid cardinal neighbors must carry one ## of the listed tags. This supports statements such as "three sides around the ## spawn must be safe grass" without hard-coding a particular chunk ID. @@ -106,6 +121,28 @@ func directional_allowed_neighbor_tags( return PackedStringArray() +func surface_tags_for_edge( + edge: TerrainChunkTopology.Edge, +) -> PackedStringArray: + var edge_tags := directional_surface_tags(edge) + return tags if edge_tags.is_empty() else edge_tags + + +func directional_surface_tags( + edge: TerrainChunkTopology.Edge, +) -> PackedStringArray: + match edge: + TerrainChunkTopology.Edge.NORTH: + return north_surface_tags + TerrainChunkTopology.Edge.EAST: + return east_surface_tags + TerrainChunkTopology.Edge.SOUTH: + return south_surface_tags + TerrainChunkTopology.Edge.WEST: + return west_surface_tags + return PackedStringArray() + + func _allows_neighbor_with_tags( neighbor: TerrainChunkDefinition, allowed_tags: PackedStringArray, diff --git a/world/generation/terrain_chunk_generator.gd b/world/generation/terrain_chunk_generator.gd index 091179e..8f6235d 100644 --- a/world/generation/terrain_chunk_generator.gd +++ b/world/generation/terrain_chunk_generator.gd @@ -7,6 +7,9 @@ const CONSTRAINT_PROFILE_QUANTIZATION := 0.001 const CANDIDATE_WEIGHT_SCALE := 10000.0 const LARGE_GRID_LIGHTWEIGHT_THRESHOLD := 128 const LARGE_GRID_PROPAGATION_INTERVAL := 8 +# Keep packed domain bits below the signed 64-bit sign bit. Catalogs larger +# than this remain correct through the unpacked solver path. +const MAX_PACKED_SOLVER_VARIANTS := 62 @export var catalog: TerrainChunkCatalog @export var grid_size := Vector2i(7, 7) @@ -16,6 +19,13 @@ const LARGE_GRID_PROPAGATION_INTERVAL := 8 @export var show_chunk_labels := false @export var force_center_chunk_id: StringName = &"chunk_0000" @export var required_chunk_ids := PackedStringArray() +@export_group("Grass-Sand Smoothing") +@export var grass_sand_smoothing_enabled := false +@export var smoothing_grass_chunk_id: StringName = &"chunk_0000" +@export var smoothing_sand_chunk_id: StringName = &"chunk_0001" +@export var smoothing_diagonal_chunk_id: StringName = &"chunk_0013" +@export_range(1, 64, 1) var maximum_smoothing_placements := 16 +@export_group("") @export_group("Elevated Inland Feature") @export var elevated_cliff_feature_enabled := false @export var elevated_cliff_base_chunk_id: StringName = &"chunk_0000" @@ -37,10 +47,22 @@ var _variants: Array[TerrainChunkVariant] = [] # Rotations with identical terrain edges and connector directions share one # solver candidate, then resolve back to an authored rotation after solving. var _solver_variants: Array[TerrainChunkVariant] = [] +var _solver_definitions: Array[TerrainChunkDefinition] = [] +var _solver_variants_by_definition: Dictionary[StringName, Array] = {} +var _definition_variant_masks: Dictionary[StringName, int] = {} +var _required_neighbor_variant_masks: Dictionary[StringName, int] = {} var _equivalent_rotations: Dictionary[String, PackedInt32Array] = {} var _solver_variant_indices: Dictionary = {} var _edge_compatibility := PackedByteArray() +var _edge_compatibility_masks := PackedInt64Array() +var _static_cell_candidate_masks := PackedInt64Array() +var _neighbor_indices: Array[PackedInt32Array] = [] var _placements: Array[TerrainChunkVariant] = [] +var _placement_counts: Dictionary[StringName, int] = {} +var _unfilled_cells := 0 +var _required_stable_ids: Array[StringName] = [] +var _selection_missing_required_count := 0 +var _selection_missing_required_mask := 0 var _random := RandomNumberGenerator.new() var _generated_chunks: Node3D var _backtrack_count := 0 @@ -60,25 +82,27 @@ func generate() -> bool: previous_placements.assign(_placements) _random.seed = generation_seed _backtrack_count = 0 - _placements.clear() - _placements.resize(grid_size.x * grid_size.y) + _reset_placements(grid_size.x * grid_size.y) if not _prepare_elevated_feature_region(): - _placements.assign(previous_placements) + _assign_placements(previous_placements) return false if not _solve_cell(0): - _placements.assign(previous_placements) + _assign_placements(previous_placements) push_error( "Terrain generation could not solve a %dx%d grid with seed %d." % [grid_size.x, grid_size.y, generation_seed] ) return false _resolve_equivalent_rotations() + if not _apply_grass_sand_smoothing(): + _assign_placements(previous_placements) + return false if not _apply_elevated_feature(): - _placements.assign(previous_placements) + _assign_placements(previous_placements) return false var solution_root := _build_solution_root() if solution_root == null: - _placements.assign(previous_placements) + _assign_placements(previous_placements) return false _replace_generated_chunks(solution_root) var summary := _build_summary() @@ -112,11 +136,11 @@ func generate_from_placement_keys(keys: PackedStringArray) -> bool: var previous_placements: Array[TerrainChunkVariant] = [] previous_placements.assign(_placements) - _placements.assign(resolved) + _assign_placements(resolved) _backtrack_count = 0 var solution_root := _build_solution_root() if solution_root == null: - _placements.assign(previous_placements) + _assign_placements(previous_placements) return false _replace_generated_chunks(solution_root) var summary := _build_summary() @@ -199,6 +223,10 @@ func _prepare_catalog() -> bool: _variants.clear() _solver_variants.clear() + _solver_definitions.clear() + _solver_variants_by_definition.clear() + _definition_variant_masks.clear() + _required_neighbor_variant_masks.clear() _equivalent_rotations.clear() _solver_variant_indices.clear() for definition: TerrainChunkDefinition in catalog.definitions: @@ -214,7 +242,10 @@ func _prepare_catalog() -> bool: return false for variant: TerrainChunkVariant in analyzed: _variants.append(variant) - if definition.overlay_only: + if ( + definition.overlay_only + or not definition.participates_in_base_solver + ): continue var constraint_key := _constraint_key(variant) var rotations: PackedInt32Array = _equivalent_rotations.get( @@ -232,11 +263,77 @@ func _prepare_catalog() -> bool: 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 + var variant := _solver_variants[index] + _solver_variant_indices[variant] = index + var definition_variants: Array = _solver_variants_by_definition.get( + variant.definition.stable_id, + [], + ) + definition_variants.append(variant) + if definition_variants.size() == 1: + _solver_definitions.append(variant.definition) + _solver_variants_by_definition[ + variant.definition.stable_id + ] = definition_variants + if index < MAX_PACKED_SOLVER_VARIANTS: + _definition_variant_masks[variant.definition.stable_id] = ( + int( + _definition_variant_masks.get( + variant.definition.stable_id, + 0, + ) + ) + | (1 << index) + ) _build_edge_compatibility_cache() return true +func _reset_placements(cell_count: int) -> void: + _placements.clear() + _placements.resize(cell_count) + _placement_counts.clear() + _unfilled_cells = cell_count + + +func _assign_placements(layout: Array[TerrainChunkVariant]) -> void: + _placements.assign(layout) + _rebuild_placement_tracking() + + +func _rebuild_placement_tracking() -> void: + _placement_counts.clear() + _unfilled_cells = 0 + for placement: TerrainChunkVariant in _placements: + if placement == null: + _unfilled_cells += 1 + continue + _adjust_placement_count(placement.definition.stable_id, 1) + + +func _set_placement(index: int, placement: TerrainChunkVariant) -> void: + var previous := _placements[index] + if previous == placement: + return + if previous == null: + _unfilled_cells -= 1 + else: + _adjust_placement_count(previous.definition.stable_id, -1) + _placements[index] = placement + if placement == null: + _unfilled_cells += 1 + else: + _adjust_placement_count(placement.definition.stable_id, 1) + + +func _adjust_placement_count(stable_id: StringName, difference: int) -> void: + var updated := int(_placement_counts.get(stable_id, 0)) + difference + if updated <= 0: + _placement_counts.erase(stable_id) + return + _placement_counts[stable_id] = updated + + func _constraint_key(variant: TerrainChunkVariant) -> String: return "%s|%s" % [ variant.definition.stable_id, @@ -248,6 +345,10 @@ func _build_edge_compatibility_cache() -> void: var variant_count := _solver_variants.size() _edge_compatibility.resize(variant_count * 4 * variant_count) _edge_compatibility.fill(0) + _edge_compatibility_masks.resize( + variant_count * 4 if _packed_solver_domains_are_available() else 0 + ) + _edge_compatibility_masks.fill(0) for first_index: int in variant_count: var first := _solver_variants[first_index] for first_edge_value: int in TerrainChunkTopology.Edge.values(): @@ -260,14 +361,19 @@ func _build_edge_compatibility_cache() -> void: first_edge, second_index, ) - _edge_compatibility[cache_index] = int( - _calculate_edge_compatibility( + var compatible := _calculate_edge_compatibility( first, first_edge, second, second_edge, - ) ) + _edge_compatibility[cache_index] = int(compatible) + if compatible and _packed_solver_domains_are_available(): + var mask_index := first_index * 4 + int(first_edge) + _edge_compatibility_masks[mask_index] = ( + _edge_compatibility_masks[mask_index] + | (1 << second_index) + ) func _edge_compatibility_index( @@ -284,11 +390,13 @@ func _edge_compatibility_index( func _validate_generation_requirements() -> bool: var seen_required: Dictionary[StringName, bool] = {} + _required_stable_ids.clear() for required_id_value: String in required_chunk_ids: var required_id := StringName(required_id_value) if seen_required.has(required_id): continue seen_required[required_id] = true + _required_stable_ids.append(required_id) var definition := catalog.definition_for_id(required_id) if definition == null: push_error("Required terrain chunk %s is not in the catalog." % required_id) @@ -296,6 +404,15 @@ func _validate_generation_requirements() -> bool: if definition.maximum_placements == 0: push_error("Required terrain chunk %s permits no placements." % required_id) return false + if ( + definition.overlay_only + or not definition.participates_in_base_solver + ): + push_error( + "Required terrain chunk %s does not participate in the base solver." + % required_id + ) + return false if seen_required.size() > grid_size.x * grid_size.y: push_error("The terrain grid has fewer cells than required chunk IDs.") return false @@ -325,36 +442,52 @@ func _solve_cell(placed_count: int) -> bool: ) if _backtrack_count >= maximum_backtracks: return false + _refresh_selection_required_cache() if not _requirements_can_still_be_satisfied(): return false var next_cell := _select_next_cell(placed_count) var index := int(next_cell.get("index", -1)) if index < 0: return false - var candidates := next_cell.get( - "candidates", - [], - ) as Array[TerrainChunkVariant] + var candidates: Array[TerrainChunkVariant] = [] + candidates.assign(next_cell.get("candidates", [])) for candidate: TerrainChunkVariant in _weighted_candidate_order( candidates, index, ): - _placements[index] = candidate + _set_placement(index, candidate) if _solve_cell(placed_count + 1): return true - _placements[index] = null + _set_placement(index, null) _backtrack_count += 1 if _backtrack_count >= maximum_backtracks: break return false +func _refresh_selection_required_cache() -> void: + _selection_missing_required_count = 0 + _selection_missing_required_mask = 0 + for stable_id: StringName in _required_stable_ids: + if int(_placement_counts.get(stable_id, 0)) > 0: + continue + _selection_missing_required_count += 1 + _selection_missing_required_mask |= int( + _definition_variant_masks.get(stable_id, 0) + ) + + 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() + if _packed_solver_domains_are_available(): + if ( + _placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD + and placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0 + ): + return _select_next_large_grid_cell() + return _select_next_large_grid_propagated_cell() + if _placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD: + if placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0: + return _select_next_large_grid_cell_unpacked() # 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. @@ -385,6 +518,221 @@ func _select_next_cell(placed_count: int) -> Dictionary: return {"index": best_index, "candidates": best_candidates} +func _select_next_large_grid_propagated_cell() -> Dictionary: + var domains: Dictionary[int, int] = {} + for index: int in _placements.size(): + if _placements[index] != null: + continue + var candidate_mask := _compatible_candidate_mask(index) + if candidate_mask == 0: + return {"index": index, "candidates": []} + domains[index] = candidate_mask + if not _propagate_domain_masks(domains): + return {"index": -1, "candidates": []} + + var best_index := -1 + var best_mask := 0 + var best_size := _solver_variants.size() + 1 + for index: int in domains: + var candidate_mask := domains[index] + var candidate_count := _mask_bit_count(candidate_mask) + if best_index < 0 or candidate_count < best_size: + best_index = index + best_mask = candidate_mask + best_size = candidate_count + if candidate_count == 1: + break + return { + "index": best_index, + "candidates": _candidates_from_mask(best_mask), + } + + +func _candidates_from_mask(candidate_mask: int) -> Array[TerrainChunkVariant]: + var result: Array[TerrainChunkVariant] = [] + for candidate_index: int in _solver_variants.size(): + if (candidate_mask & (1 << candidate_index)) != 0: + result.append(_solver_variants[candidate_index]) + return result + + +func _mask_bit_count(candidate_mask: int) -> int: + var count := 0 + var remaining := candidate_mask + while remaining != 0: + remaining &= remaining - 1 + count += 1 + return count + + +func _propagate_domain_masks(domains: Dictionary[int, int]) -> bool: + var changed := true + while changed: + changed = false + for index: int in domains.keys(): + var candidates := domains[index] + var supported := 0 + for candidate_index: int in _solver_variants.size(): + var candidate_bit := 1 << candidate_index + if ( + (candidates & candidate_bit) != 0 + and _domain_candidate_has_mask_support( + candidate_index, + index, + domains, + ) + ): + supported |= candidate_bit + if supported == 0: + return false + if supported != candidates: + domains[index] = supported + changed = true + var required_result := _constrain_required_domain_masks(domains) + if required_result < 0: + return false + if required_result > 0: + changed = true + if not _placed_neighbor_requirements_have_domain_mask_support(domains): + return false + return true + + +func _domain_candidate_has_mask_support( + candidate_index: int, + index: int, + domains: Dictionary[int, int], +) -> bool: + var candidate := _solver_variants[candidate_index] + var possible_required_neighbors := 0 + var required_neighbor_mask := _required_neighbor_variant_mask( + candidate.definition + ) + for edge_value: int in TerrainChunkTopology.Edge.values(): + var edge := edge_value as TerrainChunkTopology.Edge + var neighbor_index := _neighbor_indices[index][edge_value] + if neighbor_index < 0: + continue + var placed_neighbor := _placements[neighbor_index] + if placed_neighbor != null: + if _definition_matches_required_neighbor_tags( + candidate.definition, + placed_neighbor.definition, + ): + possible_required_neighbors += 1 + continue + var support_mask := ( + int(domains.get(neighbor_index, 0)) + & _edge_compatibility_masks[candidate_index * 4 + int(edge)] + ) + if ( + candidate.definition.maximum_placements >= 0 + and _definition_placement_count(candidate.definition) + 2 + > candidate.definition.maximum_placements + ): + support_mask &= ~int( + _definition_variant_masks.get( + candidate.definition.stable_id, + 0, + ) + ) + if support_mask == 0: + return false + if (support_mask & required_neighbor_mask) != 0: + possible_required_neighbors += 1 + return ( + possible_required_neighbors + >= candidate.definition.minimum_required_neighbors + ) + + +func _required_neighbor_variant_mask( + definition: TerrainChunkDefinition, +) -> int: + if _required_neighbor_variant_masks.has(definition.stable_id): + return _required_neighbor_variant_masks[definition.stable_id] + var result := 0 + for candidate_index: int in _solver_variants.size(): + if _definition_matches_required_neighbor_tags( + definition, + _solver_variants[candidate_index].definition, + ): + result |= 1 << candidate_index + _required_neighbor_variant_masks[definition.stable_id] = result + return result + + +func _constrain_required_domain_masks( + domains: Dictionary[int, int], +) -> int: + var changed := false + for stable_id: StringName in _missing_required_ids(): + var definition_mask := int(_definition_variant_masks.get(stable_id, 0)) + var supporting_cell := -1 + var supporting_cell_count := 0 + for index: int in domains: + if (domains[index] & definition_mask) == 0: + continue + supporting_cell = index + supporting_cell_count += 1 + if supporting_cell_count > 1: + break + if supporting_cell_count == 0: + return -1 + if supporting_cell_count != 1: + continue + var forced_mask := domains[supporting_cell] & definition_mask + if forced_mask == 0: + return -1 + if forced_mask != domains[supporting_cell]: + domains[supporting_cell] = forced_mask + changed = true + return 1 if changed else 0 + + +func _placed_neighbor_requirements_have_domain_mask_support( + domains: Dictionary[int, int], +) -> bool: + for index: int in _placements.size(): + var placement := _placements[index] + if placement == null or placement.definition.minimum_required_neighbors <= 0: + continue + var placement_variant_index := int( + _solver_variant_indices.get(placement, -1) + ) + if placement_variant_index < 0: + return false + var required_neighbor_mask := _required_neighbor_variant_mask( + placement.definition + ) + var possible_neighbors := 0 + for edge_value: int in TerrainChunkTopology.Edge.values(): + var edge := edge_value as TerrainChunkTopology.Edge + var neighbor_index := _neighbor_indices[index][edge_value] + if neighbor_index < 0: + continue + var neighbor := _placements[neighbor_index] + if neighbor != null: + if _definition_matches_required_neighbor_tags( + placement.definition, + neighbor.definition, + ): + possible_neighbors += 1 + continue + var compatible_required_mask := ( + int(domains.get(neighbor_index, 0)) + & required_neighbor_mask + & _edge_compatibility_masks[ + placement_variant_index * 4 + int(edge) + ] + ) + if compatible_required_mask != 0: + possible_neighbors += 1 + if possible_neighbors < placement.definition.minimum_required_neighbors: + return false + return true + + 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 @@ -395,7 +743,33 @@ func _select_next_large_grid_cell() -> Dictionary: for index: int in _placements.size(): if _placements[index] != null: continue - var candidates := _compatible_candidates(index) + var candidate_mask := _compatible_candidate_mask(index) + if candidate_mask == 0: + return {"index": index, "candidates": []} + var candidates := _candidates_from_mask(candidate_mask) + 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 _select_next_large_grid_cell_unpacked() -> Dictionary: + 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_unpacked(index) if candidates.is_empty(): return {"index": index, "candidates": candidates} var placed_neighbors := _placed_neighbor_count(index) @@ -414,20 +788,10 @@ func _select_next_large_grid_cell() -> Dictionary: 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): + for neighbor_index: int in _neighbor_indices[index]: + if neighbor_index < 0: continue - var neighbor_index := ( - neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x - ) if _placements[neighbor_index] != null: count += 1 return count @@ -622,42 +986,99 @@ func _constrain_required_domains(domains: Dictionary) -> int: func _compatible_candidates(index: int) -> Array[TerrainChunkVariant]: + if not _packed_solver_domains_are_available(): + return _compatible_candidates_unpacked(index) + return _candidates_from_mask(_compatible_candidate_mask(index)) + + +func _compatible_candidates_unpacked( + index: int, +) -> Array[TerrainChunkVariant]: var result: Array[TerrainChunkVariant] = [] var missing_required := _missing_required_ids() - var remaining_cells := _unfilled_cell_count() - var must_place_missing_required := missing_required.size() >= remaining_cells + var must_place_missing_required := ( + missing_required.size() >= _unfilled_cells + ) for candidate: TerrainChunkVariant in _solver_variants: if ( must_place_missing_required and not missing_required.has(candidate.definition.stable_id) ): continue - if not _candidate_can_occupy_cell(candidate, index): - continue - result.append(candidate) + if _candidate_can_occupy_cell(candidate, index): + result.append(candidate) return result -func _candidate_can_occupy_cell( +func _packed_solver_domains_are_available() -> bool: + return _solver_variants.size() <= MAX_PACKED_SOLVER_VARIANTS + + +func _compatible_candidate_mask(index: int) -> int: + if index < 0 or index >= _static_cell_candidate_masks.size(): + return 0 + var result := int(_static_cell_candidate_masks[index]) + if _selection_missing_required_count >= _unfilled_cells: + result &= _selection_missing_required_mask + if result == 0: + return 0 + result = _mask_matching_placed_neighbors(result, index) + if result == 0: + return 0 + + for definition: TerrainChunkDefinition in _solver_definitions: + var definition_mask := int( + _definition_variant_masks.get(definition.stable_id, 0) + ) + if (result & definition_mask) == 0: + continue + var definition_variants: Array = _solver_variants_by_definition.get( + definition.stable_id, + [], + ) + if ( + definition_variants.is_empty() + or not _candidate_dynamic_cell_rules_are_satisfied( + definition_variants[0] as TerrainChunkVariant, + index, + ) + ): + result &= ~definition_mask + return result + + +func _mask_matching_placed_neighbors(candidate_mask: int, index: int) -> int: + var result := candidate_mask + for edge_value: int in TerrainChunkTopology.Edge.values(): + var neighbor_index := _neighbor_indices[index][edge_value] + if neighbor_index < 0: + continue + var neighbor := _placements[neighbor_index] + if neighbor == null: + continue + var neighbor_variant_index := int( + _solver_variant_indices.get(neighbor, -1) + ) + if neighbor_variant_index < 0: + return 0 + var neighbor_edge := TerrainChunkTopology.opposite_edge( + edge_value as TerrainChunkTopology.Edge + ) + result &= _edge_compatibility_masks[ + neighbor_variant_index * 4 + int(neighbor_edge) + ] + if result == 0: + return 0 + return result + + +func _candidate_dynamic_cell_rules_are_satisfied( candidate: TerrainChunkVariant, index: int, ) -> bool: if not _definition_has_capacity(candidate.definition): return false - var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) - 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 ( - coordinate == center - and force_center_chunk_id != &"" - and candidate.definition.stable_id != force_center_chunk_id - ): - return false + var center_index := (grid_size.y / 2) * grid_size.x + grid_size.x / 2 if ( index != center_index and _placements[center_index] == null @@ -667,17 +1088,96 @@ func _candidate_can_occupy_cell( >= candidate.definition.maximum_placements - 1 ): return false + return ( + _candidate_neighbor_requirement_can_still_be_met_at_index( + candidate.definition, + index, + ) + and _candidate_preserves_placed_neighbor_requirements_at_index( + candidate.definition, + index, + ) + ) + + +func _candidate_neighbor_requirement_can_still_be_met_at_index( + definition: TerrainChunkDefinition, + index: int, +) -> bool: + if definition.minimum_required_neighbors <= 0: + return true + var possible_neighbors := 0 + for neighbor_index: int in _neighbor_indices[index]: + if neighbor_index < 0: + continue + var neighbor := _placements[neighbor_index] + if ( + neighbor == null + or _definition_matches_required_neighbor_tags( + definition, + neighbor.definition, + ) + ): + possible_neighbors += 1 + return possible_neighbors >= definition.minimum_required_neighbors + + +func _candidate_preserves_placed_neighbor_requirements_at_index( + candidate_definition: TerrainChunkDefinition, + candidate_index: int, +) -> bool: + for neighbor_index: int in _neighbor_indices[candidate_index]: + if neighbor_index < 0: + continue + var neighbor := _placements[neighbor_index] + if neighbor == null or neighbor.definition.minimum_required_neighbors <= 0: + continue + var possible_neighbors := 0 + for requirement_neighbor_index: int in _neighbor_indices[neighbor_index]: + if requirement_neighbor_index < 0: + continue + if requirement_neighbor_index == candidate_index: + if _definition_matches_required_neighbor_tags( + neighbor.definition, + candidate_definition, + ): + possible_neighbors += 1 + continue + var requirement_neighbor := _placements[requirement_neighbor_index] + if ( + requirement_neighbor == null + or _definition_matches_required_neighbor_tags( + neighbor.definition, + requirement_neighbor.definition, + ) + ): + possible_neighbors += 1 + if possible_neighbors < neighbor.definition.minimum_required_neighbors: + return false + return true + + +func _candidate_can_occupy_cell( + candidate: TerrainChunkVariant, + index: int, +) -> bool: + 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) + if ( + coordinate == center + and force_center_chunk_id != &"" + and candidate.definition.stable_id != force_center_chunk_id + ): + return false return ( _variant_respects_ocean_boundary(candidate, coordinate) and _candidate_matches_placed_neighbors(candidate, coordinate) - and _candidate_neighbor_requirement_can_still_be_met( - candidate.definition, - coordinate, - ) - and _candidate_preserves_placed_neighbor_requirements( - candidate.definition, - coordinate, - ) + and _candidate_dynamic_cell_rules_are_satisfied(candidate, index) ) @@ -685,6 +1185,7 @@ func _prepare_elevated_feature_region() -> bool: _elevated_feature_center = Vector2i(-1, -1) _elevated_feature_reserved_grass_indices.clear() if not elevated_cliff_feature_enabled: + _build_grid_solver_caches() return true for stable_id: StringName in [ elevated_cliff_base_chunk_id, @@ -733,9 +1234,155 @@ func _prepare_elevated_feature_region() -> bool: _elevated_feature_reserved_grass_indices[ coordinate.y * grid_size.x + coordinate.x ] = true + _build_grid_solver_caches() return true +func _build_grid_solver_caches() -> void: + _neighbor_indices.clear() + _neighbor_indices.resize(_placements.size()) + _static_cell_candidate_masks.resize(_placements.size()) + _static_cell_candidate_masks.fill(0) + var center := Vector2i(grid_size.x / 2, grid_size.y / 2) + for index: int in _placements.size(): + var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) + var neighbors := PackedInt32Array([-1, -1, -1, -1]) + for edge_value: int in TerrainChunkTopology.Edge.values(): + var neighbor_coordinate := ( + coordinate + + TerrainChunkTopology.grid_offset( + edge_value as TerrainChunkTopology.Edge + ) + ) + if _coordinate_is_inside_grid(neighbor_coordinate): + neighbors[edge_value] = ( + neighbor_coordinate.y * grid_size.x + + neighbor_coordinate.x + ) + _neighbor_indices[index] = neighbors + + if not _packed_solver_domains_are_available(): + continue + var candidate_mask := 0 + for candidate_index: int in _solver_variants.size(): + var candidate := _solver_variants[candidate_index] + if ( + _elevated_feature_reserved_grass_indices.has(index) + and candidate.definition.stable_id + != elevated_cliff_base_chunk_id + ): + continue + if ( + coordinate == center + and force_center_chunk_id != &"" + and candidate.definition.stable_id != force_center_chunk_id + ): + continue + if not _variant_respects_ocean_boundary(candidate, coordinate): + continue + candidate_mask |= 1 << candidate_index + _static_cell_candidate_masks[index] = candidate_mask + + +func _apply_grass_sand_smoothing() -> bool: + if not grass_sand_smoothing_enabled: + return true + var diagonal_definition := catalog.definition_for_id( + smoothing_diagonal_chunk_id + ) + if diagonal_definition == null: + push_error( + "Grass-sand smoothing references missing chunk %s." + % smoothing_diagonal_chunk_id + ) + return false + if diagonal_definition.participates_in_base_solver: + push_error( + "Grass-sand smoothing chunk %s must be excluded from the base solver." + % smoothing_diagonal_chunk_id + ) + return false + + var candidates: Array[Dictionary] = [] + var center := Vector2i(grid_size.x / 2, grid_size.y / 2) + for index: int in _placements.size(): + var placement := _placements[index] + if placement == null or placement.definition.stable_id not in [ + smoothing_grass_chunk_id, + smoothing_sand_chunk_id, + ]: + continue + var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) + if ( + _coordinate_is_on_boundary(coordinate) + or _elevated_feature_reserved_grass_indices.has(index) + or ( + absi(coordinate.x - center.x) + + absi(coordinate.y - center.y) + <= 1 + ) + ): + continue + for quarter_turns: int in 4: + var diagonal := _authored_variant( + diagonal_definition, + quarter_turns, + ) + if ( + diagonal != null + and _candidate_matches_placed_neighbors(diagonal, coordinate) + ): + candidates.append({"index": index, "variant": diagonal}) + break + + var smoothing_random := RandomNumberGenerator.new() + smoothing_random.seed = generation_seed ^ 0xD1A60A1 + for candidate_index: int in range(candidates.size() - 1, 0, -1): + var swap_index := smoothing_random.randi_range(0, candidate_index) + var temporary := candidates[candidate_index] + candidates[candidate_index] = candidates[swap_index] + candidates[swap_index] = temporary + + var previous_placements: Array[TerrainChunkVariant] = [] + previous_placements.assign(_placements) + var replacement_count := 0 + for candidate: Dictionary in candidates: + if replacement_count >= maximum_smoothing_placements: + break + var index := int(candidate["index"]) + var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) + var diagonal := candidate["variant"] as TerrainChunkVariant + if ( + diagonal == null + or not _candidate_matches_placed_neighbors(diagonal, coordinate) + or not _replacement_preserves_required_chunk(_placements[index]) + ): + continue + _set_placement(index, diagonal) + replacement_count += 1 + + var validation_error := _resolved_layout_validation_error(_placements) + if not validation_error.is_empty(): + _assign_placements(previous_placements) + push_error( + "Grass-sand smoothing produced an invalid layout: " + + validation_error + ) + return false + return true + + +func _replacement_preserves_required_chunk( + original: TerrainChunkVariant, +) -> bool: + if original == null: + return false + var stable_id := original.definition.stable_id + if String(stable_id) not in required_chunk_ids: + return true + return _definition_placement_count(original.definition) > 1 + + func _apply_elevated_feature() -> bool: if not elevated_cliff_feature_enabled: return true @@ -781,7 +1428,10 @@ func _apply_elevated_feature() -> bool: ) return false var coordinate := _elevated_feature_center + (spec["offset"] as Vector2i) - _placements[coordinate.y * grid_size.x + coordinate.x] = variant + _set_placement( + 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) @@ -851,101 +1501,6 @@ func _coordinate_is_inside_grid(coordinate: Vector2i) -> bool: ) -func _candidate_neighbor_requirement_can_still_be_met( - definition: TerrainChunkDefinition, - coordinate: Vector2i, -) -> bool: - if definition.minimum_required_neighbors <= 0: - return true - var possible_neighbors := 0 - for edge_value: int in TerrainChunkTopology.Edge.values(): - var neighbor_coordinate := ( - coordinate - + TerrainChunkTopology.grid_offset( - edge_value as TerrainChunkTopology.Edge - ) - ) - if not _coordinate_is_inside_grid(neighbor_coordinate): - continue - var neighbor := _placements[ - neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x - ] - if ( - neighbor == null - or _definition_matches_required_neighbor_tags( - definition, - neighbor.definition, - ) - ): - possible_neighbors += 1 - return possible_neighbors >= definition.minimum_required_neighbors - - -func _candidate_preserves_placed_neighbor_requirements( - definition: TerrainChunkDefinition, - coordinate: Vector2i, -) -> bool: - for edge_value: int in TerrainChunkTopology.Edge.values(): - var neighbor_coordinate := ( - coordinate - + TerrainChunkTopology.grid_offset( - edge_value as TerrainChunkTopology.Edge - ) - ) - if not _coordinate_is_inside_grid(neighbor_coordinate): - continue - var neighbor := _placements[ - neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x - ] - if neighbor == null or neighbor.definition.minimum_required_neighbors <= 0: - continue - if not _placed_requirement_can_still_be_met_with_candidate( - neighbor.definition, - neighbor_coordinate, - definition, - coordinate, - ): - return false - return true - - -func _placed_requirement_can_still_be_met_with_candidate( - definition: TerrainChunkDefinition, - coordinate: Vector2i, - candidate_definition: TerrainChunkDefinition, - candidate_coordinate: Vector2i, -) -> bool: - var possible_neighbors := 0 - for edge_value: int in TerrainChunkTopology.Edge.values(): - var neighbor_coordinate := ( - coordinate - + TerrainChunkTopology.grid_offset( - edge_value as TerrainChunkTopology.Edge - ) - ) - if not _coordinate_is_inside_grid(neighbor_coordinate): - continue - if neighbor_coordinate == candidate_coordinate: - if _definition_matches_required_neighbor_tags( - definition, - candidate_definition, - ): - possible_neighbors += 1 - continue - var neighbor := _placements[ - neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x - ] - if ( - neighbor == null - or _definition_matches_required_neighbor_tags( - definition, - neighbor.definition, - ) - ): - possible_neighbors += 1 - return possible_neighbors >= definition.minimum_required_neighbors - - func _definition_has_capacity(definition: TerrainChunkDefinition) -> bool: if definition.maximum_placements < 0: return true @@ -953,11 +1508,9 @@ func _definition_has_capacity(definition: TerrainChunkDefinition) -> bool: func _definition_placement_count(definition: TerrainChunkDefinition) -> int: - var count := 0 - for placement: TerrainChunkVariant in _placements: - if placement != null and placement.definition == definition: - count += 1 - return count + if definition == null: + return 0 + return int(_placement_counts.get(definition.stable_id, 0)) func _edges_are_compatible( @@ -1024,11 +1577,30 @@ func _calculate_edge_compatibility( or (first_inlet and second_outlet) ) return ( - first.allows_non_water_neighbor_on_edge(second, first_edge) + _edge_surfaces_are_compatible( + first, + first_edge, + second, + second_edge, + ) + and first.allows_non_water_neighbor_on_edge(second, first_edge) and second.allows_non_water_neighbor_on_edge(first, second_edge) ) +func _edge_surfaces_are_compatible( + first: TerrainChunkVariant, + first_edge: TerrainChunkTopology.Edge, + second: TerrainChunkVariant, + second_edge: TerrainChunkTopology.Edge, +) -> bool: + var first_tags := first.surface_tags(first_edge) + var second_tags := second.surface_tags(second_edge) + if first_tags.is_empty() or second_tags.is_empty(): + return true + return TerrainChunkDefinition.has_any_tag(first_tags, second_tags) + + func _variant_edge_has_connector( variant: TerrainChunkVariant, source_mask: int, @@ -1057,35 +1629,57 @@ func _requirements_can_still_be_satisfied() -> bool: func _definition_has_available_cell( definition: TerrainChunkDefinition, ) -> bool: + var definition_variants: Array = _solver_variants_by_definition.get( + definition.stable_id, + [], + ) + if ( + _packed_solver_domains_are_available() + and not definition_variants.is_empty() + ): + var definition_mask := int( + _definition_variant_masks.get(definition.stable_id, 0) + ) + for index: int in _placements.size(): + if _placements[index] != null: + continue + var candidate_mask := ( + int(_static_cell_candidate_masks[index]) & definition_mask + ) + if candidate_mask == 0: + continue + candidate_mask = _mask_matching_placed_neighbors( + candidate_mask, + index, + ) + if ( + candidate_mask != 0 + and _candidate_dynamic_cell_rules_are_satisfied( + definition_variants[0] as TerrainChunkVariant, + index, + ) + ): + return true + return false for index: int in _placements.size(): if _placements[index] != null: continue - for candidate: TerrainChunkVariant in _solver_variants: - if ( - candidate.definition == definition - and _candidate_can_occupy_cell(candidate, index) - ): + for candidate: TerrainChunkVariant in definition_variants: + if _candidate_can_occupy_cell(candidate, index): return true return false func _unfilled_cell_count() -> int: - var count := 0 - for placement: TerrainChunkVariant in _placements: - if placement == null: - count += 1 - return count + return _unfilled_cells func _missing_required_ids() -> Array[StringName]: - var found: Dictionary[StringName, bool] = {} - for placement: TerrainChunkVariant in _placements: - if placement != null: - found[placement.definition.stable_id] = true var result: Array[StringName] = [] - for required_id_value: String in required_chunk_ids: - var required_id := StringName(required_id_value) - if not found.has(required_id) and not result.has(required_id): + for required_id: StringName in _required_stable_ids: + if ( + int(_placement_counts.get(required_id, 0)) <= 0 + ): result.append(required_id) return result @@ -1335,13 +1929,7 @@ func _walkable_connectivity_validation_error( func _required_chunk_is_missing(stable_id: StringName) -> bool: if not required_chunk_ids.has(String(stable_id)): return false - for placement: TerrainChunkVariant in _placements: - if ( - placement != null - and placement.definition.stable_id == stable_id - ): - return false - return true + return int(_placement_counts.get(stable_id, 0)) <= 0 func _resolve_equivalent_rotations() -> void: diff --git a/world/generation/terrain_chunk_variant.gd b/world/generation/terrain_chunk_variant.gd index efb590a..ca45812 100644 --- a/world/generation/terrain_chunk_variant.gd +++ b/world/generation/terrain_chunk_variant.gd @@ -35,6 +35,10 @@ func allows_non_water_neighbor_on_edge( ) +func surface_tags(edge: TerrainChunkTopology.Edge) -> PackedStringArray: + return definition.surface_tags_for_edge(source_edge(edge)) + + func topology_signature(quantization: float) -> String: var tokens := PackedStringArray() for edge_value: int in TerrainChunkTopology.Edge.values(): @@ -44,9 +48,10 @@ func topology_signature(quantization: float) -> String: func constraint_signature(quantization: float) -> String: - return "%s|neighbors:%s|ocean:%d|in:%d|out:%d" % [ + return "%s|neighbors:%s|surfaces:%s|ocean:%d|in:%d|out:%d" % [ topology_signature(quantization), _neighbor_rule_signature(), + _surface_rule_signature(), rotated_edge_mask(definition.ocean_facing_edges), rotated_edge_mask(definition.water_inlet_edges), rotated_edge_mask(definition.water_outlet_edges), @@ -65,6 +70,16 @@ func _neighbor_rule_signature() -> String: return "/".join(edge_tokens) +func _surface_rule_signature() -> String: + var edge_tokens := PackedStringArray() + for edge_value: int in TerrainChunkTopology.Edge.values(): + var edge := edge_value as TerrainChunkTopology.Edge + var edge_tags: PackedStringArray = surface_tags(edge).duplicate() + edge_tags.sort() + edge_tokens.append(",".join(edge_tags)) + return "/".join(edge_tokens) + + func rotated_edge_mask(source_mask: int) -> int: var result := 0 for edge_value: int in TerrainChunkTopology.Edge.values():