diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index c6ac945..3a7a297 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -82,8 +82,14 @@ func _validate_generated_region( generator: TerrainChunkGenerator, ) -> void: var placements := generator.placement_keys() - assert(placements.size() == 49) - assert(placements[24].begins_with("chunk_spawn@")) + var expected_chunk_count := generator.grid_size.x * generator.grid_size.y + var center := Vector2i( + generator.grid_size.x / 2, + generator.grid_size.y / 2, + ) + var center_index := center.y * generator.grid_size.x + center.x + assert(placements.size() == expected_chunk_count) + assert(placements[center_index].begins_with("chunk_spawn@")) assert(_placement_count(placements, "chunk_spawn") == 1) assert(_placement_count(placements, "chunk_0001") >= 1) assert(_placement_count(placements, "chunk_0002") >= 1) @@ -92,8 +98,24 @@ func _validate_generated_region( assert(_placement_count(placements, "chunk_0005") >= 1) assert(_placement_count(placements, "chunk_0006") >= 1) assert(_placement_count(placements, "chunk_0007") >= 1) - assert(generator.get_generated_chunks_root().get_child_count() == 49) - assert(generator.get_primary_terrain_meshes().size() == 49) + assert(_placement_count(placements, "chunk_0008") >= 1) + assert(_placement_count(placements, "chunk_0009") == 1) + assert(_placement_count(placements, "chunk_0010") == 4) + assert( + _placement_count(placements, "chunk_0011") + + _placement_count(placements, "chunk_0012") + == 4 + ) + assert(_placement_count(placements, "chunk_0012") <= 1) + assert( + generator.get_generated_chunks_root().get_child_count() + == expected_chunk_count + ) + assert( + generator.get_primary_terrain_meshes().size() + == expected_chunk_count + ) + _validate_elevated_cliff_feature(generator) var shop := region.get_node("Interactables/FishingShopWorld") as Node3D var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D @@ -111,6 +133,7 @@ func _validate_generated_region( var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D assert(ocean != null and fresh_root != null) assert(ocean.water_type == WaterType.Type.SALT_WATER) + assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT)) var fresh_placement_count := 0 for record: Dictionary in generator.placement_records(): var tags: PackedStringArray = record.get("tags", PackedStringArray()) @@ -123,6 +146,7 @@ func _validate_generated_region( var fresh := child as WaterBodyAuthoring assert(fresh != null) assert(fresh.water_type == WaterType.Type.FRESH_WATER) + assert(is_equal_approx(fresh.position.y, GeneratedWorldRegion.WATER_HEIGHT)) assert(fresh.visible) assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0) assert(not fresh.visual_surface_enabled) @@ -155,13 +179,13 @@ func _validate_generated_region( &"terrain_chunk_coordinate", Vector2i.ZERO, ) - var center := Vector2i( + var spawn_coordinate := Vector2i( generator.grid_size.x / 2, generator.grid_size.y / 2, ) var spawn_distance := ( - absi(coordinate.x - center.x) - + absi(coordinate.y - center.y) + absi(coordinate.x - spawn_coordinate.x) + + absi(coordinate.y - spawn_coordinate.y) ) assert(spawn_distance >= definition.minimum_spawn_chunk_distance) _validate_decoration_transform( @@ -169,6 +193,13 @@ func _validate_generated_region( child as Node3D, definition, ) + if prop_id in [&"prop_tree_1", &"prop_tree_2", &"prop_tree_3"]: + assert( + _has_material_variant_override( + child.get_node_or_null("Visual"), + definition.material_variants, + ) + ) assert(_decoration_group_count(region, &"grass_tree") > 0) assert(_decoration_group_count(region, &"sand_tree") > 0) assert( @@ -187,6 +218,69 @@ func _validate_generated_region( ) +func _validate_elevated_cliff_feature( + generator: TerrainChunkGenerator, +) -> void: + var elevated_coordinates: Dictionary[Vector2i, StringName] = {} + var top_coordinate := Vector2i(-1, -1) + for record: Dictionary in generator.placement_records(): + var stable_id: StringName = record.get("stable_id", &"") + if stable_id not in [ + &"chunk_0009", + &"chunk_0010", + &"chunk_0011", + &"chunk_0012", + ]: + continue + var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO) + assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1) + assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1) + elevated_coordinates[coordinate] = stable_id + if stable_id == &"chunk_0009": + top_coordinate = coordinate + assert(elevated_coordinates.size() == 9) + assert(top_coordinate != Vector2i(-1, -1)) + for row_offset: int in range(-1, 2): + for column_offset: int in range(-1, 2): + assert( + elevated_coordinates.has( + top_coordinate + Vector2i(column_offset, row_offset) + ) + ) + + var generated := generator.get_generated_chunks_root() + assert(generated != null) + var layered_count := 0 + for chunk_root: Node in generated.get_children(): + var stable_id := StringName( + chunk_root.get_meta(&"terrain_chunk_id", &"") + ) + if stable_id not in [ + &"chunk_0009", + &"chunk_0010", + &"chunk_0011", + &"chunk_0012", + ]: + continue + layered_count += 1 + var base_layer := chunk_root.get_node_or_null("TerrainBaseLayer") + var overlay := chunk_root.get_node_or_null("TerrainOverlay") + assert(base_layer != null and overlay != null) + var base_mesh := TerrainChunkAnalyzer.find_primary_mesh( + base_layer, + &"chunk_0000", + ) + var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh( + overlay, + stable_id, + ) + assert(base_mesh != null and base_mesh.mesh != null) + assert(overlay_mesh != null and overlay_mesh.mesh != null) + assert(base_mesh.has_node("TerrainBaseLayerCollision")) + assert(overlay_mesh.has_node("TerrainCollision")) + assert(layered_count == 9) + + func _validate_ocean_facing_record( record: Dictionary, grid_size: Vector2i, @@ -224,6 +318,23 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void: assert(instance.position.is_zero_approx()) assert(_find_mesh_instance(instance) != null) instance.free() + var mushroom := catalog.definition_for_id(&"prop_mushroom") + assert(mushroom != null and mushroom.has_collision()) + assert(mushroom.visual_offset.y < 0.0) + for tree_id: StringName in [ + &"prop_tree_1", + &"prop_tree_2", + &"prop_tree_3", + ]: + var tree := catalog.definition_for_id(tree_id) + assert(tree != null) + assert(tree.minimum_visual_scale < tree.maximum_visual_scale) + assert(tree.material_variants.size() >= 3) + assert(not tree.variant_material_slot_names.is_empty()) + var pine := catalog.definition_for_id(&"prop_pine") + assert(pine != null) + assert(pine.minimum_visual_scale < pine.maximum_visual_scale) + assert(pine.material_variants.is_empty()) func _validate_biome_catalog( @@ -276,7 +387,16 @@ func _validate_decoration_transform( var collision := prop.get_node_or_null( "TrunkCollision/CollisionShape" ) as CollisionShape3D - assert(visual_root != null and visual_root.position.is_zero_approx()) + assert(visual_root != null) + assert(visual_root.position.is_equal_approx(definition.visual_offset)) + var visual_scale := float( + prop.get_meta(&"terrain_prop_visual_scale", 1.0) + ) + assert(visual_scale >= definition.minimum_visual_scale - 0.001) + assert(visual_scale <= definition.maximum_visual_scale + 0.001) + assert( + visual_root.scale.is_equal_approx(Vector3.ONE * visual_scale) + ) assert(visual != null and visual.mesh != null) if definition.has_collision(): assert(collision != null) @@ -319,6 +439,26 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D: return null +func _has_material_variant_override( + root_node: Node, + variants: Array[Material], +) -> bool: + if root_node == null: + return false + var mesh_instance := root_node as MeshInstance3D + if mesh_instance != null and mesh_instance.mesh != null: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var override := mesh_instance.get_surface_override_material( + surface_index + ) + if override != null and override in variants: + return true + for child: Node in root_node.get_children(): + if _has_material_variant_override(child, variants): + return true + return false + + func _validate_authored_chunk_surfaces( region: GeneratedWorldRegion, generator: TerrainChunkGenerator, @@ -329,6 +469,7 @@ func _validate_authored_chunk_surfaces( var found_grass_ocean_edge := false var found_grass_beach_transition := false var found_grass_ocean_corner := false + var found_beach_ocean_corner := false for chunk_root: Node in generated.get_children(): if chunk_root.name.begins_with("chunk_0002r"): var beach := chunk_root.find_child( @@ -383,11 +524,20 @@ func _validate_authored_chunk_surfaces( assert("grass_lite" in corner_materials) assert("cliff_wall" in corner_materials) found_grass_ocean_corner = true + elif chunk_root.name.begins_with("chunk_0008r"): + var beach_ocean_corner := chunk_root.find_child( + "chunk_0008", true, false + ) as MeshInstance3D + assert(beach_ocean_corner != null and beach_ocean_corner.mesh != null) + var beach_corner_materials := _material_names(beach_ocean_corner) + assert("sand" in beach_corner_materials) + found_beach_ocean_corner = true assert(found_beach) assert(found_pond) assert(found_grass_ocean_edge) assert(found_grass_beach_transition) assert(found_grass_ocean_corner) + assert(found_beach_ocean_corner) func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray: 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 64d5f1f..04976a9 100644 --- a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd +++ b/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd @@ -119,6 +119,7 @@ func _make_generator() -> TerrainChunkGenerator: "chunk_0005", "chunk_0006", "chunk_0007", + "chunk_0008", ] ) 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 964da9a..7e12544 100644 --- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn +++ b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn @@ -21,12 +21,13 @@ shadow_enabled = true [node name="TerrainChunkGenerator" type="Node3D" parent="."] script = ExtResource("2_generator") catalog = ExtResource("3_catalog") -grid_size = Vector2i(7, 7) +grid_size = Vector2i(11, 9) generation_seed = 13001 build_collision = true show_chunk_labels = false force_center_chunk_id = &"chunk_spawn" -required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007") +required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008") +elevated_cliff_feature_enabled = true [node name="Player" parent="." instance=ExtResource("4_player")] position = Vector3(0, 0.3, 0) diff --git a/tests/terrain_chunk_generator_validation.gd b/tests/terrain_chunk_generator_validation.gd index 0d70816..467c154 100644 --- a/tests/terrain_chunk_generator_validation.gd +++ b/tests/terrain_chunk_generator_validation.gd @@ -12,6 +12,11 @@ const EXPECTED_IDS: Array[String] = [ "chunk_0005", "chunk_0006", "chunk_0007", + "chunk_0008", + "chunk_0009", + "chunk_0010", + "chunk_0011", + "chunk_0012", "chunk_spawn", ] const REQUIRED_IDS: Array[String] = [ @@ -23,10 +28,11 @@ const REQUIRED_IDS: Array[String] = [ "chunk_0005", "chunk_0006", "chunk_0007", + "chunk_0008", ] const TEST_SEED := 13001 const CONNECTOR_STRESS_SEED := 287 -const EXPECTED_SOLVER_VARIANT_COUNT := 27 +const EXPECTED_SOLVER_VARIANT_COUNT := 31 const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = { &"chunk_0000": 4, &"chunk_0001": 4, @@ -36,6 +42,11 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = { &"chunk_0005": 4, &"chunk_0006": 4, &"chunk_0007": 4, + &"chunk_0008": 4, + &"chunk_0009": 4, + &"chunk_0010": 4, + &"chunk_0011": 4, + &"chunk_0012": 4, &"chunk_spawn": 1, } @@ -75,6 +86,11 @@ func _validate_catalog() -> void: var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005") var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006") var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007") + var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008") + var cliff_top := CATALOG.definition_for_id(&"chunk_0009") + var cliff_corner := CATALOG.definition_for_id(&"chunk_0010") + var cliff_edge := CATALOG.definition_for_id(&"chunk_0011") + var cliff_ramp := CATALOG.definition_for_id(&"chunk_0012") var spawn := CATALOG.definition_for_id(&"chunk_spawn") _check( ( @@ -84,6 +100,11 @@ func _validate_catalog() -> void: and grass_ocean_edge != null and grass_beach_transition != null and grass_ocean_corner != null + and beach_ocean_corner != null + and cliff_top != null + and cliff_corner != null + and cliff_edge != null + and cliff_ramp != null and spawn != null ), "The authored biome-rule definitions must be available.", @@ -95,6 +116,11 @@ func _validate_catalog() -> void: and grass_ocean_edge != null and grass_beach_transition != null and grass_ocean_corner != null + and beach_ocean_corner != null + and cliff_top != null + and cliff_corner != null + and cliff_edge != null + and cliff_ramp != null and spawn != null ): _check( @@ -193,6 +219,45 @@ func _validate_catalog() -> void: ), "Both corner land seams must accept straight grass ocean edges.", ) + _check( + beach_ocean_corner.ocean_facing_edges + == ( + (1 << int(TerrainChunkTopology.Edge.EAST)) + | (1 << int(TerrainChunkTopology.Edge.SOUTH)) + ), + "The authored beach corner must expose east and south to ocean.", + ) + _check( + beach_ocean_corner.minimum_required_neighbors == 2 + and ( + "beach_ocean_edge" + in beach_ocean_corner.required_neighbor_tags + ), + "The beach corner must join two straight beach ocean edges.", + ) + for cliff_definition: TerrainChunkDefinition in [ + cliff_top, + cliff_corner, + cliff_edge, + cliff_ramp, + ]: + _check( + cliff_definition.overlay_only + and cliff_definition.must_be_interior + and cliff_definition.base_layer_scene != null + and cliff_definition.base_layer_mesh_name == &"chunk_0000", + "Every second-tier piece must overlay level-one grass inland.", + ) + _check( + cliff_top.minimum_required_neighbors == 4 + and "elevation_2" in cliff_top.required_neighbor_tags, + "The second-tier top must be enclosed on all four sides.", + ) + _check( + "ramp" in cliff_ramp.tags + and cliff_ramp.maximum_placements > 0, + "The second-tier ramp must remain available without being required.", + ) func _validate_profiles() -> void: @@ -443,11 +508,13 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005") var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006") var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007") + var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008") _check( ( grass_ocean_edge != null and grass_beach_transition != null and grass_ocean_corner != null + and beach_ocean_corner != null ), "The authored coastline additions must be available.", ) @@ -455,6 +522,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: grass_ocean_edge == null or grass_beach_transition == null or grass_ocean_corner == null + or beach_ocean_corner == null ): return var beach_zero := _find_variant(generator, &"chunk_0002", 0) @@ -462,12 +530,14 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: var grass_edge_south := _find_variant(generator, &"chunk_0005", 3) var transition_zero := _find_variant(generator, &"chunk_0006", 0) var corner_zero := _find_variant(generator, &"chunk_0007", 0) + var beach_corner_zero := _find_variant(generator, &"chunk_0008", 0) _check( beach_zero != null and grass_edge_zero != null and grass_edge_south != null and transition_zero != null - and corner_zero != null, + and corner_zero != null + and beach_corner_zero != null, "The unrotated coastline variants must be available.", ) if ( @@ -476,6 +546,7 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: and grass_edge_south != null and transition_zero != null and corner_zero != null + and beach_corner_zero != null ): _check( grass_edge_zero.profile( @@ -522,6 +593,24 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "The corner's west side must join a straight grass ocean edge.", ) + _check( + beach_corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches( + beach_zero.profile(TerrainChunkTopology.Edge.SOUTH), + generator.edge_match_tolerance, + ), + "The beach corner's north side must join a straight beach.", + ) + var beach_south := _find_variant(generator, &"chunk_0002", 3) + _check( + beach_south != null + and beach_corner_zero.profile( + TerrainChunkTopology.Edge.WEST + ).matches( + beach_south.profile(TerrainChunkTopology.Edge.EAST), + generator.edge_match_tolerance, + ), + "The beach corner's west side must join a rotated straight beach.", + ) var flat_grass := _find_variant(generator, &"chunk_0000", 0) var flat_sand := _find_variant(generator, &"chunk_0001", 0) _check( @@ -560,7 +649,12 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "No transition rotation may accept sand behind it.", ) - for stable_id: StringName in [&"chunk_0005", &"chunk_0006", &"chunk_0007"]: + for stable_id: StringName in [ + &"chunk_0005", + &"chunk_0006", + &"chunk_0007", + &"chunk_0008", + ]: for quarter_turns: int in 4: var coast_variant := _find_variant( generator, @@ -580,11 +674,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: ), "%s must never place its ocean edge inside the map." % stable_id, ) - if stable_id != &"chunk_0007": + if stable_id != &"chunk_0007" and stable_id != &"chunk_0008": continue var boundary_coordinate := Vector2i(3, 3) var ocean_edges := coast_variant.rotated_edge_mask( - grass_ocean_corner.ocean_facing_edges + ( + grass_ocean_corner.ocean_facing_edges + if stable_id == &"chunk_0007" + else beach_ocean_corner.ocean_facing_edges + ) ) for edge_value: int in TerrainChunkTopology.Edge.values(): if (ocean_edges & (1 << edge_value)) == 0: @@ -603,15 +701,15 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void: coast_variant, boundary_coordinate, ), - "Each grass corner rotation must fit its matching map corner.", + "Each coast corner rotation must fit its matching map corner.", ) func _validate_generation_summary(generator: TerrainChunkGenerator) -> void: var summary := generator._build_summary() _check( - int(summary.get("variant_count", 0)) == 33, - "The current catalog must expose all 33 authored rotations.", + int(summary.get("variant_count", 0)) == 53, + "The current catalog must expose all 53 authored rotations.", ) _check( int(summary.get("solver_variant_count", 0)) @@ -811,6 +909,27 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void: straight_edge_neighbors == 2, "Every grass corner must join two straight grass ocean edges.", ) + if placement.definition.stable_id == &"chunk_0008": + var straight_beach_neighbors := 0 + for edge_value: int in TerrainChunkTopology.Edge.values(): + var neighbor_coordinate := ( + coordinate + + TerrainChunkTopology.grid_offset( + edge_value as TerrainChunkTopology.Edge + ) + ) + if not generator._coordinate_is_inside_grid(neighbor_coordinate): + continue + var neighbor := generator._placements[ + neighbor_coordinate.y * generator.grid_size.x + + neighbor_coordinate.x + ] + if neighbor.definition.stable_id == &"chunk_0002": + straight_beach_neighbors += 1 + _check( + straight_beach_neighbors == 2, + "Every beach corner must join two straight beach ocean edges.", + ) if placement.definition.stable_id != &"chunk_0001": continue var touches_coast := false diff --git a/tools/blender/export_terrain_chunks.py b/tools/blender/export_terrain_chunks.py index 27b775d..654227e 100644 --- a/tools/blender/export_terrain_chunks.py +++ b/tools/blender/export_terrain_chunks.py @@ -282,10 +282,16 @@ def _discover_chunks(tolerance: float) -> list[ChunkSource]: # Coastline meshes may intentionally stop before the positive-X edge, # leaving the surrounding ocean visible. Positive X is the canonical # authored ocean direction; Godot supplies the quarter-turn variants. - allow_open_east_edge = "ocean_edge" in label + # Elevated inland cliff overlays intentionally stop at their downhill + # edge. The runtime places them over the existing flat grass chunk, so + # these omissions are not coastline openings or holes in the final cell. + layered_inland_cliff = "cliff_2" in label + allow_open_east_edge = "ocean_edge" in label or layered_inland_cliff # Corner pieces additionally leave canonical negative Y open. Their # two marked ocean edges rotate together at runtime. - allow_open_south_edge = "ocean_edge_corner" in label + allow_open_south_edge = ( + "ocean_edge_corner" in label or layered_inland_cliff + ) bound_problems = _validate_bounds( bounds, tolerance, diff --git a/world/generation/biomes/definitions/biome_forest.tres b/world/generation/biomes/definitions/biome_forest.tres index 0d11be2..a660042 100644 --- a/world/generation/biomes/definitions/biome_forest.tres +++ b/world/generation/biomes/definitions/biome_forest.tres @@ -6,10 +6,11 @@ [sub_resource type="Resource" id="Rule_grass_tree"] script = ExtResource("2_rule") procedural_group = &"grass_tree" -placement_chance = 9000 -adjacency_bonus = 1000 -maximum_density = 10000 +placement_chance = 8200 +adjacency_bonus = 1200 +maximum_density = 8500 minimum_placements = 2 +placement_attempts_per_chunk = 3 allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3") [sub_resource type="Resource" id="Rule_grass_detail"] diff --git a/world/generation/biomes/definitions/biome_pine_forest.tres b/world/generation/biomes/definitions/biome_pine_forest.tres index 293acbc..6601a46 100644 --- a/world/generation/biomes/definitions/biome_pine_forest.tres +++ b/world/generation/biomes/definitions/biome_pine_forest.tres @@ -6,10 +6,11 @@ [sub_resource type="Resource" id="Rule_grass_tree"] script = ExtResource("2_rule") procedural_group = &"grass_tree" -placement_chance = 9000 -adjacency_bonus = 1000 -maximum_density = 10000 +placement_chance = 8200 +adjacency_bonus = 1200 +maximum_density = 8500 minimum_placements = 2 +placement_attempts_per_chunk = 3 allowed_prop_ids = PackedStringArray("prop_pine") [sub_resource type="Resource" id="Rule_grass_detail"] diff --git a/world/generation/chunks/assets/chunk_0005_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0005_cliff_wall.png.import index 0796279..51d8e11 100644 --- a/world/generation/chunks/assets/chunk_0005_cliff_wall.png.import +++ b/world/generation/chunks/assets/chunk_0005_cliff_wall.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0005_grass_lite.png.import b/world/generation/chunks/assets/chunk_0005_grass_lite.png.import index c1b1dd6..01e84d4 100644 --- a/world/generation/chunks/assets/chunk_0005_grass_lite.png.import +++ b/world/generation/chunks/assets/chunk_0005_grass_lite.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0006_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0006_cliff_wall.png.import index 238e929..411a5b1 100644 --- a/world/generation/chunks/assets/chunk_0006_cliff_wall.png.import +++ b/world/generation/chunks/assets/chunk_0006_cliff_wall.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0006_grass_lite.png.import b/world/generation/chunks/assets/chunk_0006_grass_lite.png.import index fb37b89..2d01a58 100644 --- a/world/generation/chunks/assets/chunk_0006_grass_lite.png.import +++ b/world/generation/chunks/assets/chunk_0006_grass_lite.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0006_sand.png.import b/world/generation/chunks/assets/chunk_0006_sand.png.import index bc9f61a..d829b05 100644 --- a/world/generation/chunks/assets/chunk_0006_sand.png.import +++ b/world/generation/chunks/assets/chunk_0006_sand.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0007_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0007_cliff_wall.png.import index 007f60d..d78acc6 100644 --- a/world/generation/chunks/assets/chunk_0007_cliff_wall.png.import +++ b/world/generation/chunks/assets/chunk_0007_cliff_wall.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0007_grass_lite.png.import b/world/generation/chunks/assets/chunk_0007_grass_lite.png.import index abe6b52..03e4dc5 100644 --- a/world/generation/chunks/assets/chunk_0007_grass_lite.png.import +++ b/world/generation/chunks/assets/chunk_0007_grass_lite.png.import @@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" diff --git a/world/generation/chunks/assets/chunk_0008.glb b/world/generation/chunks/assets/chunk_0008.glb new file mode 100644 index 0000000..d785e65 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0008.glb differ diff --git a/world/generation/chunks/assets/chunk_0008.glb.import b/world/generation/chunks/assets/chunk_0008.glb.import new file mode 100644 index 0000000..a090711 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0008.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bm1teitx5ijxl" +path="res://.godot/imported/chunk_0008.glb-0b0a6712e5384c3f72c65e6ef1fc12c4.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0008.glb" +dest_files=["res://.godot/imported/chunk_0008.glb-0b0a6712e5384c3f72c65e6ef1fc12c4.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/world/generation/chunks/assets/chunk_0008_sand.png b/world/generation/chunks/assets/chunk_0008_sand.png new file mode 100644 index 0000000..06f807a Binary files /dev/null and b/world/generation/chunks/assets/chunk_0008_sand.png differ diff --git a/world/generation/chunks/assets/chunk_0008_sand.png.import b/world/generation/chunks/assets/chunk_0008_sand.png.import new file mode 100644 index 0000000..9c135d8 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0008_sand.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dj24j2d1u8712" +path.s3tc="res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.s3tc.ctex" +path.etc2="res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "adfa66b8fe9da7f3450d3ddfd432b86a" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0008_sand.png" +dest_files=["res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.s3tc.ctex", "res://.godot/imported/chunk_0008_sand.png-55d7cf4f540d9cfb7d4ba59e9f9f38da.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/world/generation/chunks/assets/chunk_0009.glb b/world/generation/chunks/assets/chunk_0009.glb new file mode 100644 index 0000000..8c09d3a Binary files /dev/null and b/world/generation/chunks/assets/chunk_0009.glb differ diff --git a/world/generation/chunks/assets/chunk_0009.glb.import b/world/generation/chunks/assets/chunk_0009.glb.import new file mode 100644 index 0000000..72c9778 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0009.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://hasempwso5gi" +path="res://.godot/imported/chunk_0009.glb-397f642bee3da14146c56fddf8e21369.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0009.glb" +dest_files=["res://.godot/imported/chunk_0009.glb-397f642bee3da14146c56fddf8e21369.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/world/generation/chunks/assets/chunk_0009_grass_lite.png b/world/generation/chunks/assets/chunk_0009_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0009_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0009_grass_lite.png.import b/world/generation/chunks/assets/chunk_0009_grass_lite.png.import new file mode 100644 index 0000000..5f3beed --- /dev/null +++ b/world/generation/chunks/assets/chunk_0009_grass_lite.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djjyaoih5vcnd" +path="res://.godot/imported/chunk_0009_grass_lite.png-5d49eb465b8a8f8739796ce13c646f7d.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0009_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0009_grass_lite.png-5d49eb465b8a8f8739796ce13c646f7d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0010.glb b/world/generation/chunks/assets/chunk_0010.glb new file mode 100644 index 0000000..302fb11 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0010.glb differ diff --git a/world/generation/chunks/assets/chunk_0010.glb.import b/world/generation/chunks/assets/chunk_0010.glb.import new file mode 100644 index 0000000..a8039a0 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0010.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b5tf5e1g5auu7" +path="res://.godot/imported/chunk_0010.glb-6ffffa0064f5930acc433afe5e4334e4.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0010.glb" +dest_files=["res://.godot/imported/chunk_0010.glb-6ffffa0064f5930acc433afe5e4334e4.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/world/generation/chunks/assets/chunk_0010_cliff_wall.png b/world/generation/chunks/assets/chunk_0010_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0010_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0010_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0010_cliff_wall.png.import new file mode 100644 index 0000000..68aa6c1 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0010_cliff_wall.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cwwraho3pj5aa" +path="res://.godot/imported/chunk_0010_cliff_wall.png-78830d69b22e3c32cc295e574d596c3a.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "fc97184f537cc23f83a731327e10c836" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0010_cliff_wall.png" +dest_files=["res://.godot/imported/chunk_0010_cliff_wall.png-78830d69b22e3c32cc295e574d596c3a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0010_grass_lite.png b/world/generation/chunks/assets/chunk_0010_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0010_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0010_grass_lite.png.import b/world/generation/chunks/assets/chunk_0010_grass_lite.png.import new file mode 100644 index 0000000..994f591 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0010_grass_lite.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://q7k2r4mb3bsr" +path="res://.godot/imported/chunk_0010_grass_lite.png-694b30e6e36cc16c1cf98b2f3823a0c2.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0010_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0010_grass_lite.png-694b30e6e36cc16c1cf98b2f3823a0c2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0011.glb b/world/generation/chunks/assets/chunk_0011.glb new file mode 100644 index 0000000..fa36cff Binary files /dev/null and b/world/generation/chunks/assets/chunk_0011.glb differ diff --git a/world/generation/chunks/assets/chunk_0011.glb.import b/world/generation/chunks/assets/chunk_0011.glb.import new file mode 100644 index 0000000..86ee993 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0011.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cym6ilrdikjcj" +path="res://.godot/imported/chunk_0011.glb-cda3ab701cf5e469b53437c043314d05.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0011.glb" +dest_files=["res://.godot/imported/chunk_0011.glb-cda3ab701cf5e469b53437c043314d05.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/world/generation/chunks/assets/chunk_0011_cliff_wall.png b/world/generation/chunks/assets/chunk_0011_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0011_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0011_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0011_cliff_wall.png.import new file mode 100644 index 0000000..c8ac4eb --- /dev/null +++ b/world/generation/chunks/assets/chunk_0011_cliff_wall.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dggc1jbgenl11" +path="res://.godot/imported/chunk_0011_cliff_wall.png-9a4d64a03d75f23c2ef3614386484a4e.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "fc97184f537cc23f83a731327e10c836" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0011_cliff_wall.png" +dest_files=["res://.godot/imported/chunk_0011_cliff_wall.png-9a4d64a03d75f23c2ef3614386484a4e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0011_grass_lite.png b/world/generation/chunks/assets/chunk_0011_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0011_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0011_grass_lite.png.import b/world/generation/chunks/assets/chunk_0011_grass_lite.png.import new file mode 100644 index 0000000..a99277b --- /dev/null +++ b/world/generation/chunks/assets/chunk_0011_grass_lite.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bhuhcaowvcyrk" +path="res://.godot/imported/chunk_0011_grass_lite.png-3a0a1a7ad2d55bd853b238730af91a7c.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0011_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0011_grass_lite.png-3a0a1a7ad2d55bd853b238730af91a7c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0012.glb b/world/generation/chunks/assets/chunk_0012.glb new file mode 100644 index 0000000..bbde6a3 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0012.glb differ diff --git a/world/generation/chunks/assets/chunk_0012.glb.import b/world/generation/chunks/assets/chunk_0012.glb.import new file mode 100644 index 0000000..65fad11 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0012.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://c4i32u0bdtix5" +path="res://.godot/imported/chunk_0012.glb-1c818ae539bef19d5ca4fbe3ace96887.scn" + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0012.glb" +dest_files=["res://.godot/imported/chunk_0012.glb-1c818ae539bef19d5ca4fbe3ace96887.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/world/generation/chunks/assets/chunk_0012_cliff_wall.png b/world/generation/chunks/assets/chunk_0012_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0012_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0012_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0012_cliff_wall.png.import new file mode 100644 index 0000000..b2039d0 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0012_cliff_wall.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cf27tcolkmfog" +path="res://.godot/imported/chunk_0012_cliff_wall.png-3d27d4041790b1c3dbc838d6a0c94d40.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "fc97184f537cc23f83a731327e10c836" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0012_cliff_wall.png" +dest_files=["res://.godot/imported/chunk_0012_cliff_wall.png-3d27d4041790b1c3dbc838d6a0c94d40.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0012_dirt.png b/world/generation/chunks/assets/chunk_0012_dirt.png new file mode 100644 index 0000000..588c8c6 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0012_dirt.png differ diff --git a/world/generation/chunks/assets/chunk_0012_dirt.png.import b/world/generation/chunks/assets/chunk_0012_dirt.png.import new file mode 100644 index 0000000..bdc481f --- /dev/null +++ b/world/generation/chunks/assets/chunk_0012_dirt.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3f1w276lmg7i" +path="res://.godot/imported/chunk_0012_dirt.png-5c3a2a10642f9d078ad1d1c6333d8264.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "07d90b3c46a4c3d0c172bed143dc9436" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0012_dirt.png" +dest_files=["res://.godot/imported/chunk_0012_dirt.png-5c3a2a10642f9d078ad1d1c6333d8264.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/assets/chunk_0012_grass_lite.png b/world/generation/chunks/assets/chunk_0012_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0012_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0012_grass_lite.png.import b/world/generation/chunks/assets/chunk_0012_grass_lite.png.import new file mode 100644 index 0000000..ea56bd8 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0012_grass_lite.png.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://lv8xn2moas2s" +path="res://.godot/imported/chunk_0012_grass_lite.png-ccfdc53acb9a967eb5d00e669258fc3c.ctex" +metadata={ +"vram_texture": false +} +generator_parameters={ +"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4" +} + +[deps] + +source_file="res://world/generation/chunks/assets/chunk_0012_grass_lite.png" +dest_files=["res://.godot/imported/chunk_0012_grass_lite.png-ccfdc53acb9a967eb5d00e669258fc3c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/world/generation/chunks/definitions/chunk_0002.tres b/world/generation/chunks/definitions/chunk_0002.tres index 1e51d12..77545a7 100644 --- a/world/generation/chunks/definitions/chunk_0002.tres +++ b/world/generation/chunks/definitions/chunk_0002.tres @@ -11,7 +11,7 @@ packed_scene = ExtResource("2_scene") primary_mesh_name = &"chunk_0002" selection_weight = 0.75 maximum_placements = 20 -tags = PackedStringArray("land", "walkable", "sand", "coast", "slope") +tags = PackedStringArray("land", "walkable", "sand", "coast", "slope", "beach_ocean_edge") allowed_neighbor_tags = PackedStringArray("sand", "grass") required_neighbor_tags = PackedStringArray("sand") minimum_required_neighbors = 1 diff --git a/world/generation/chunks/definitions/chunk_0008.tres b/world/generation/chunks/definitions/chunk_0008.tres new file mode 100644 index 0000000..29854f7 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0008.tres @@ -0,0 +1,22 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0008.glb" id="2_scene"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0008" +label = "beach corner" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0008" +selection_weight = 0.2 +maximum_placements = 4 +tags = PackedStringArray("land", "walkable", "sand", "coast", "slope", "corner") +allowed_neighbor_tags = PackedStringArray("sand", "coast") +north_allowed_neighbor_tags = PackedStringArray("beach_ocean_edge") +west_allowed_neighbor_tags = PackedStringArray("beach_ocean_edge") +required_neighbor_tags = PackedStringArray("beach_ocean_edge") +minimum_required_neighbors = 2 +preferred_neighbor_tags = PackedStringArray("coast") +prefers_map_boundary = true +ocean_facing_edges = 6 diff --git a/world/generation/chunks/definitions/chunk_0009.tres b/world/generation/chunks/definitions/chunk_0009.tres new file mode 100644 index 0000000..4be6d07 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0009.tres @@ -0,0 +1,23 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] + +[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0009.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0009" +label = "second-tier grass top" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0009" +base_layer_scene = ExtResource("3_base_grass") +base_layer_mesh_name = &"chunk_0000" +overlay_only = true +selection_weight = 0.22 +maximum_placements = 8 +tags = PackedStringArray("land", "walkable", "grass", "elevation_2", "elevation_2_top") +allowed_neighbor_tags = PackedStringArray("elevation_2") +required_neighbor_tags = PackedStringArray("elevation_2") +minimum_required_neighbors = 4 +preferred_neighbor_tags = PackedStringArray("elevation_2_top") +must_be_interior = true diff --git a/world/generation/chunks/definitions/chunk_0010.tres b/world/generation/chunks/definitions/chunk_0010.tres new file mode 100644 index 0000000..d9120b1 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0010.tres @@ -0,0 +1,27 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] + +[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0010.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0010" +label = "second-tier grass corner" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0010" +base_layer_scene = ExtResource("3_base_grass") +base_layer_mesh_name = &"chunk_0000" +overlay_only = true +selection_weight = 0.12 +maximum_placements = 16 +tags = PackedStringArray("land", "walkable", "grass", "cliff", "corner", "elevation_2", "elevation_2_border") +allowed_neighbor_tags = PackedStringArray("grass", "elevation_2") +north_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +east_allowed_neighbor_tags = PackedStringArray("grass") +south_allowed_neighbor_tags = PackedStringArray("grass") +west_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +required_neighbor_tags = PackedStringArray("elevation_2_border") +minimum_required_neighbors = 2 +preferred_neighbor_tags = PackedStringArray("elevation_2") +must_be_interior = true diff --git a/world/generation/chunks/definitions/chunk_0011.tres b/world/generation/chunks/definitions/chunk_0011.tres new file mode 100644 index 0000000..e7ce943 --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0011.tres @@ -0,0 +1,27 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] + +[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0011.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0011" +label = "second-tier grass edge" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0011" +base_layer_scene = ExtResource("3_base_grass") +base_layer_mesh_name = &"chunk_0000" +overlay_only = true +selection_weight = 0.28 +maximum_placements = 32 +tags = PackedStringArray("land", "walkable", "grass", "cliff", "edge", "elevation_2", "elevation_2_border") +allowed_neighbor_tags = PackedStringArray("grass", "elevation_2") +north_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +east_allowed_neighbor_tags = PackedStringArray("grass") +south_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +west_allowed_neighbor_tags = PackedStringArray("elevation_2_top") +required_neighbor_tags = PackedStringArray("elevation_2_top") +minimum_required_neighbors = 1 +preferred_neighbor_tags = PackedStringArray("elevation_2") +must_be_interior = true diff --git a/world/generation/chunks/definitions/chunk_0012.tres b/world/generation/chunks/definitions/chunk_0012.tres new file mode 100644 index 0000000..06caccb --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0012.tres @@ -0,0 +1,27 @@ +[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] + +[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0012.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="3_base_grass"] + +[resource] +script = ExtResource("1_definition") +stable_id = &"chunk_0012" +label = "second-tier grass ramp" +packed_scene = ExtResource("2_scene") +primary_mesh_name = &"chunk_0012" +base_layer_scene = ExtResource("3_base_grass") +base_layer_mesh_name = &"chunk_0000" +overlay_only = true +selection_weight = 0.12 +maximum_placements = 8 +tags = PackedStringArray("land", "walkable", "grass", "cliff", "edge", "ramp", "elevation_2", "elevation_2_border") +allowed_neighbor_tags = PackedStringArray("grass", "elevation_2") +north_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +east_allowed_neighbor_tags = PackedStringArray("grass") +south_allowed_neighbor_tags = PackedStringArray("elevation_2_border") +west_allowed_neighbor_tags = PackedStringArray("elevation_2_top") +required_neighbor_tags = PackedStringArray("elevation_2_top") +minimum_required_neighbors = 1 +preferred_neighbor_tags = PackedStringArray("elevation_2") +must_be_interior = true diff --git a/world/generation/chunks/terrain_chunk_catalog.tres b/world/generation/chunks/terrain_chunk_catalog.tres index 6240098..b262050 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=12 format=3] +[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=17 format=3] [ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"] @@ -11,8 +11,13 @@ [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0005.tres" id="9_grass_ocean_edge"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0006.tres" id="10_grass_beach_transition"] [ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0007.tres" id="11_grass_ocean_corner"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0008.tres" id="12_beach_ocean_corner"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0009.tres" id="13_cliff_top"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0010.tres" id="14_cliff_corner"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0011.tres" id="15_cliff_edge"] +[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0012.tres" id="16_cliff_ramp"] [resource] script = ExtResource("1_catalog") chunk_size = 10.0 -definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("8_spawn")]) +definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("8_spawn")]) diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index f313b51..59876b3 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -78,6 +78,11 @@ func _ready() -> void: func generate_world(seed: int) -> bool: if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED: return false + if ( + seed == _current_seed + and _generator.get_generated_chunks_root() != null + ): + return true _current_seed = seed _generator.generation_seed = seed return _generator.generate() @@ -237,29 +242,34 @@ func _on_generation_completed(summary: Dictionary) -> void: _prop_definitions_for(rule, tags), coordinate, ) - if ( - definitions.is_empty() - or not _prop_group_roll_succeeds( - rule, - group_key, + for _placement_attempt: int in ( + rule.placement_attempts_per_chunk + ): + if group_counts.get(group_key, 0) >= maximum: + break + if ( + definitions.is_empty() + or not _prop_group_roll_succeeds( + rule, + group_key, + coordinate, + random, + ) + ): + continue + if _maybe_add_prop( + definitions, + record.get("position", Vector3.ZERO), coordinate, + biome.stable_id, random, - ) - ): - continue - if _maybe_add_prop( - definitions, - record.get("position", Vector3.ZERO), - coordinate, - biome.stable_id, - random, - terrain_triangles, - ): - group_counts[group_key] += 1 - _record_prop_group_coordinate( - group_key, - coordinate, - ) + terrain_triangles, + ): + group_counts[group_key] += 1 + _record_prop_group_coordinate( + group_key, + coordinate, + ) _ensure_minimum_props( eligible_records, group_counts, @@ -322,7 +332,7 @@ func _configure_static_water() -> void: _ocean.fish_pool = OCEAN_POOL _ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"] _ocean.surface_size = Vector2(10000.0, 10000.0) - _ocean.position.y = WATER_HEIGHT - 0.025 + _ocean.position.y = WATER_HEIGHT _apply_water_materials() @@ -451,6 +461,11 @@ func _add_prop( ) -> bool: if definition == null or definition.packed_scene == null: return false + var visual_scale := random.randf_range( + definition.minimum_visual_scale, + definition.maximum_visual_scale, + ) + var scaled_clearance := definition.clearance_radius * visual_scale var placement := Vector3.ZERO var found_surface := false for attempt: int in PROP_PLACEMENT_ATTEMPTS: @@ -470,7 +485,7 @@ func _add_prop( > WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE and _has_prop_clearance( placement, - definition.clearance_radius, + scaled_clearance, ) ): placement.y = surface_height @@ -492,22 +507,26 @@ func _add_prop( prop.set_meta(&"terrain_prop_group", definition.procedural_group) prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate) prop.set_meta(&"terrain_biome_id", biome_id) + prop.set_meta(&"terrain_prop_visual_scale", visual_scale) prop.position = placement prop.rotation.y = random.randf_range(-PI, PI) _decorations.add_child(prop) visual_root.name = "Visual" prop.add_child(visual_root) + visual_root.position = definition.visual_offset + visual_root.scale = Vector3.ONE * visual_scale _configure_prop_visuals(visual_root) - _add_prop_collision(prop, definition) + _apply_prop_material_variant(visual_root, definition, random) + _add_prop_collision(prop, definition, visual_scale) _placed_prop_positions.append(placement) - _placed_prop_clearance_radii.append(definition.clearance_radius) + _placed_prop_clearance_radii.append(scaled_clearance) _placed_prop_groups.append(definition.procedural_group) if definition.gatherable_anchor_height > 0.0: var anchor := Marker3D.new() anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count() anchor.position = prop.position + Vector3( 0.0, - definition.gatherable_anchor_height, + definition.gatherable_anchor_height * visual_scale, 0.0, ) _tree_anchors.add_child(anchor) @@ -624,7 +643,11 @@ func _prop_group_maximum( if eligible_count <= 0: return 0 var maximum := ceili( - float(eligible_count * rule.maximum_density) + float( + eligible_count + * rule.placement_attempts_per_chunk + * rule.maximum_density + ) / float(PROP_CHANCE_SCALE) ) return maxi(maximum, rule.minimum_placements) @@ -777,6 +800,51 @@ func _configure_prop_visuals(root_node: Node) -> void: _configure_prop_visuals(child) +func _apply_prop_material_variant( + root_node: Node, + definition: TerrainPropDefinition, + random: RandomNumberGenerator, +) -> void: + if ( + definition.material_variants.is_empty() + or definition.variant_material_slot_names.is_empty() + ): + return + var variant := definition.material_variants[ + random.randi_range(0, definition.material_variants.size() - 1) + ] + _apply_material_variant_to_meshes( + root_node, + definition.variant_material_slot_names, + variant, + ) + + +func _apply_material_variant_to_meshes( + root_node: Node, + target_material_names: PackedStringArray, + variant: Material, +) -> void: + var mesh_instance := root_node as MeshInstance3D + if mesh_instance != null and mesh_instance.mesh != null: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var active_material := mesh_instance.get_active_material(surface_index) + if ( + active_material != null + and active_material.resource_name in target_material_names + ): + mesh_instance.set_surface_override_material( + surface_index, + variant, + ) + for child: Node in root_node.get_children(): + _apply_material_variant_to_meshes( + child, + target_material_names, + variant, + ) + + func _terrain_surface_triangles() -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes(): @@ -818,6 +886,7 @@ func _surface_height_at( func _add_prop_collision( prop: Node3D, definition: TerrainPropDefinition, + visual_scale: float, ) -> void: if not definition.has_collision(): return @@ -830,16 +899,16 @@ func _add_prop_collision( collision.name = "CollisionShape" if definition.has_box_collision(): var box := BoxShape3D.new() - box.size = definition.collision_box_size + box.size = definition.collision_box_size * visual_scale collision.shape = box - collision.position = definition.collision_offset + collision.position = definition.collision_offset * visual_scale else: var cylinder := CylinderShape3D.new() - cylinder.radius = definition.collision_radius - cylinder.height = definition.collision_height + cylinder.radius = definition.collision_radius * visual_scale + cylinder.height = definition.collision_height * visual_scale collision.shape = cylinder collision.position = ( - definition.collision_offset + definition.collision_offset * visual_scale + Vector3.UP * cylinder.height * 0.5 ) body.add_child(collision) diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index e46796f..b9d3a0e 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -31,12 +31,13 @@ gatherable_anchor_root = NodePath("GatherableAnchors") unique_name_in_owner = true script = ExtResource("2_generator") catalog = ExtResource("3_catalog") -grid_size = Vector2i(7, 7) +grid_size = Vector2i(15, 13) generation_seed = 13001 generate_on_ready = false build_collision = true force_center_chunk_id = &"chunk_spawn" -required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007") +required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007", "chunk_0008") +elevated_cliff_feature_enabled = true required_chunk_weight_multiplier = 64.0 maximum_backtracks = 100000 @@ -60,7 +61,7 @@ script = ExtResource("8_diggable") area_id = &"starter_beach" terrain_source = NodePath("../../Terrain/TerrainChunkGenerator") surface_materials = Array[StringName]([&"sand"]) -generation_bounds = Rect2(-35, -35, 70, 70) +generation_bounds = Rect2(-55, -45, 110, 90) minimum_global_y = -0.24 maximum_global_y = 0.5 minimum_up_dot = 0.72 diff --git a/world/generation/props/definitions/prop_mushroom.tres b/world/generation/props/definitions/prop_mushroom.tres index cf63eff..e4bff54 100644 --- a/world/generation/props/definitions/prop_mushroom.tres +++ b/world/generation/props/definitions/prop_mushroom.tres @@ -16,3 +16,7 @@ preferred_nearby_prop_groups = PackedStringArray("grass_tree") preferred_nearby_radius = 12.0 nearby_preference_weight_multiplier = 3.0 clearance_radius = 0.8 +visual_offset = Vector3(0, -0.08, 0) +collision_radius = 0.65 +collision_height = 1.0 +collision_offset = Vector3(0, -0.08, 0) diff --git a/world/generation/props/definitions/prop_pine.tres b/world/generation/props/definitions/prop_pine.tres index 19361c8..0a569cf 100644 --- a/world/generation/props/definitions/prop_pine.tres +++ b/world/generation/props/definitions/prop_pine.tres @@ -13,6 +13,8 @@ required_chunk_tags = PackedStringArray("grass") selection_weight = 0.7 minimum_spawn_chunk_distance = 2 clearance_radius = 1.55 +minimum_visual_scale = 0.75 +maximum_visual_scale = 1.22 collision_radius = 0.5 collision_height = 4.0 gatherable_anchor_height = 2.15 diff --git a/world/generation/props/definitions/prop_tree_1.tres b/world/generation/props/definitions/prop_tree_1.tres index 4b942db..41ab406 100644 --- a/world/generation/props/definitions/prop_tree_1.tres +++ b/world/generation/props/definitions/prop_tree_1.tres @@ -1,7 +1,10 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"] [resource] script = ExtResource("1_definition") @@ -12,6 +15,10 @@ procedural_group = &"grass_tree" required_chunk_tags = PackedStringArray("grass") minimum_spawn_chunk_distance = 2 clearance_radius = 1.3 +minimum_visual_scale = 0.75 +maximum_visual_scale = 1.15 +variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) collision_radius = 0.4 collision_height = 3.2 gatherable_anchor_height = 2.15 diff --git a/world/generation/props/definitions/prop_tree_2.tres b/world/generation/props/definitions/prop_tree_2.tres index 3d1ab62..c4d09fa 100644 --- a/world/generation/props/definitions/prop_tree_2.tres +++ b/world/generation/props/definitions/prop_tree_2.tres @@ -1,7 +1,10 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"] [resource] script = ExtResource("1_definition") @@ -12,6 +15,10 @@ procedural_group = &"grass_tree" required_chunk_tags = PackedStringArray("grass") minimum_spawn_chunk_distance = 2 clearance_radius = 1.5 +minimum_visual_scale = 0.8 +maximum_visual_scale = 1.18 +variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) collision_radius = 0.5 collision_height = 3.8 gatherable_anchor_height = 2.15 diff --git a/world/generation/props/definitions/prop_tree_3.tres b/world/generation/props/definitions/prop_tree_3.tres index 637a281..642a8de 100644 --- a/world/generation/props/definitions/prop_tree_3.tres +++ b/world/generation/props/definitions/prop_tree_3.tres @@ -1,7 +1,10 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"] [resource] script = ExtResource("1_definition") @@ -12,6 +15,10 @@ procedural_group = &"grass_tree" required_chunk_tags = PackedStringArray("grass") minimum_spawn_chunk_distance = 2 clearance_radius = 1.8 +minimum_visual_scale = 0.85 +maximum_visual_scale = 1.15 +variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) collision_radius = 0.55 collision_height = 4.2 gatherable_anchor_height = 2.15 diff --git a/world/generation/props/materials/leaf_dark.tres b/world/generation/props/materials/leaf_dark.tres new file mode 100644 index 0000000..d75f991 --- /dev/null +++ b/world/generation/props/materials/leaf_dark.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_variant_dark" +albedo_color = Color(0.1712, 0.252, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_light.tres b/world/generation/props/materials/leaf_light.tres new file mode 100644 index 0000000..d49f786 --- /dev/null +++ b/world/generation/props/materials/leaf_light.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_variant_light" +albedo_color = Color(0.3388, 0.7187, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_mid.tres b/world/generation/props/materials/leaf_mid.tres new file mode 100644 index 0000000..d0ee646 --- /dev/null +++ b/world/generation/props/materials/leaf_mid.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_variant_mid" +albedo_color = Color(0.3147, 0.4631, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/terrain_biome_prop_rule.gd b/world/generation/terrain_biome_prop_rule.gd index 96da0db..7b02e98 100644 --- a/world/generation/terrain_biome_prop_rule.gd +++ b/world/generation/terrain_biome_prop_rule.gd @@ -6,6 +6,9 @@ extends Resource @export_range(0, 10000, 1) var adjacency_bonus := 0 @export_range(0, 10000, 1) var maximum_density := 10000 @export_range(0, 64, 1) var minimum_placements := 0 +## More than one attempt permits intentionally dense biomes without changing +## the density of every biome that uses the same procedural prop group. +@export_range(1, 8, 1) var placement_attempts_per_chunk := 1 ## Empty permits every compatible prop in the procedural group. @export var allowed_prop_ids := PackedStringArray() diff --git a/world/generation/terrain_chunk_analyzer.gd b/world/generation/terrain_chunk_analyzer.gd index 9f1557c..84895b1 100644 --- a/world/generation/terrain_chunk_analyzer.gd +++ b/world/generation/terrain_chunk_analyzer.gd @@ -34,6 +34,29 @@ static func create_variants( chunk_size, edge_epsilon, ) + if definition.base_layer_scene != null: + var base_layer_root := definition.base_layer_scene.instantiate() + var base_layer_mesh := find_primary_mesh( + base_layer_root, + definition.base_layer_mesh_name, + ) + if base_layer_mesh == null or base_layer_mesh.mesh == null: + push_error( + "Terrain chunk %s has no base-layer MeshInstance3D named %s." + % [definition.stable_id, definition.base_layer_mesh_name] + ) + base_layer_root.free() + chunk_root.free() + return variants + boundary_points.append_array( + _collect_boundary_points( + base_layer_mesh.mesh, + _transform_relative_to(base_layer_mesh, base_layer_root), + chunk_size, + edge_epsilon, + ) + ) + base_layer_root.free() for quarter_turns: int in 4: if not definition.allows_quarter_turn(quarter_turns): continue diff --git a/world/generation/terrain_chunk_catalog.gd b/world/generation/terrain_chunk_catalog.gd index 3c73822..e6741f1 100644 --- a/world/generation/terrain_chunk_catalog.gd +++ b/world/generation/terrain_chunk_catalog.gd @@ -30,6 +30,19 @@ func validation_errors() -> PackedStringArray: errors.append("%s has no PackedScene." % definition.stable_id) if definition.primary_mesh_name == &"": errors.append("%s has no primary mesh name." % definition.stable_id) + if ( + (definition.base_layer_scene == null) + != (definition.base_layer_mesh_name == &"") + ): + errors.append( + "%s must define both base-layer scene and mesh name." + % definition.stable_id + ) + if definition.overlay_only and definition.base_layer_scene == null: + errors.append( + "%s is overlay-only but has no base-layer scene." + % definition.stable_id + ) if definition.allowed_rotation_mask == 0: errors.append("%s allows no rotations." % definition.stable_id) var has_coast := "coast" in definition.tags @@ -44,6 +57,11 @@ func validation_errors() -> PackedStringArray: "%s has an ocean-facing edge without the coast tag." % definition.stable_id ) + if definition.must_be_interior and definition.prefers_map_boundary: + errors.append( + "%s cannot require the interior and prefer the boundary." + % definition.stable_id + ) var has_fresh_water := "fresh_water" in definition.tags var has_water_footprint := ( definition.water_surface_size.x > 0.0 diff --git a/world/generation/terrain_chunk_definition.gd b/world/generation/terrain_chunk_definition.gd index 54f27cb..2d4c548 100644 --- a/world/generation/terrain_chunk_definition.gd +++ b/world/generation/terrain_chunk_definition.gd @@ -5,6 +5,17 @@ extends Resource @export var label := "" @export var packed_scene: PackedScene @export var primary_mesh_name: StringName +## Optional existing level-one chunk placed beneath an authored elevated +## overlay. This lets cliff edges leave their downhill side open while the +## ordinary base terrain remains present in the same generated cell. +@export_group("Layered Terrain") +@export var base_layer_scene: PackedScene +@export var base_layer_mesh_name: StringName +## Overlay-only chunks replace a reserved base-terrain placement after the +## ordinary terrain solve. They remain in placement manifests and runtime +## output, but do not inflate every cell's global solver domain. +@export var overlay_only := false +@export_group("") @export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees") var allowed_rotation_mask := 15 @export_range(0.01, 100.0, 0.01) var selection_weight := 1.0 @@ -36,6 +47,9 @@ var allowed_rotation_mask := 15 ## weight. This shapes regions without turning a visual preference into a hard ## generation constraint. @export var preferred_neighbor_tags := PackedStringArray() +## Interior-only pieces may not occupy any outer grid cell. Coastal elevated +## pieces use separate authored definitions rather than weakening this rule. +@export var must_be_interior := false ## Coastal transition pieces should normally migrate toward the generated ## region's perimeter while remaining legal in the interior. @export var prefers_map_boundary := false diff --git a/world/generation/terrain_chunk_generator.gd b/world/generation/terrain_chunk_generator.gd index df3afc6..091179e 100644 --- a/world/generation/terrain_chunk_generator.gd +++ b/world/generation/terrain_chunk_generator.gd @@ -5,6 +5,8 @@ signal generation_completed(summary: Dictionary) const CONSTRAINT_PROFILE_QUANTIZATION := 0.001 const CANDIDATE_WEIGHT_SCALE := 10000.0 +const LARGE_GRID_LIGHTWEIGHT_THRESHOLD := 128 +const LARGE_GRID_PROPAGATION_INTERVAL := 8 @export var catalog: TerrainChunkCatalog @export var grid_size := Vector2i(7, 7) @@ -14,6 +16,15 @@ const CANDIDATE_WEIGHT_SCALE := 10000.0 @export var show_chunk_labels := false @export var force_center_chunk_id: StringName = &"chunk_0000" @export var required_chunk_ids := PackedStringArray() +@export_group("Elevated Inland Feature") +@export var elevated_cliff_feature_enabled := false +@export var elevated_cliff_base_chunk_id: StringName = &"chunk_0000" +@export var elevated_cliff_top_chunk_id: StringName = &"chunk_0009" +@export var elevated_cliff_corner_chunk_id: StringName = &"chunk_0010" +@export var elevated_cliff_edge_chunk_id: StringName = &"chunk_0011" +@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012" +@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 0.75 +@export_group("") @export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0 @export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6 @export_range(0.01, 1.0, 0.01) var long_repeat_weight_multiplier := 0.3 @@ -33,6 +44,8 @@ var _placements: Array[TerrainChunkVariant] = [] var _random := RandomNumberGenerator.new() var _generated_chunks: Node3D var _backtrack_count := 0 +var _elevated_feature_center := Vector2i(-1, -1) +var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {} func _ready() -> void: @@ -49,6 +62,9 @@ func generate() -> bool: _backtrack_count = 0 _placements.clear() _placements.resize(grid_size.x * grid_size.y) + if not _prepare_elevated_feature_region(): + _placements.assign(previous_placements) + return false if not _solve_cell(0): _placements.assign(previous_placements) push_error( @@ -57,6 +73,9 @@ func generate() -> bool: ) return false _resolve_equivalent_rotations() + if not _apply_elevated_feature(): + _placements.assign(previous_placements) + return false var solution_root := _build_solution_root() if solution_root == null: _placements.assign(previous_placements) @@ -195,6 +214,8 @@ func _prepare_catalog() -> bool: return false for variant: TerrainChunkVariant in analyzed: _variants.append(variant) + if definition.overlay_only: + continue var constraint_key := _constraint_key(variant) var rotations: PackedInt32Array = _equivalent_rotations.get( constraint_key, @@ -207,6 +228,9 @@ func _prepare_catalog() -> bool: if _variants.is_empty(): push_error("Terrain chunk catalog produced no usable variants.") return false + if _solver_variants.is_empty(): + push_error("Terrain chunk catalog produced no base-terrain solver variants.") + return false for index: int in _solver_variants.size(): _solver_variant_indices[_solver_variants[index]] = index _build_edge_compatibility_cache() @@ -303,7 +327,7 @@ func _solve_cell(placed_count: int) -> bool: return false if not _requirements_can_still_be_satisfied(): return false - var next_cell := _select_next_cell() + var next_cell := _select_next_cell(placed_count) var index := int(next_cell.get("index", -1)) if index < 0: return false @@ -325,7 +349,12 @@ func _solve_cell(placed_count: int) -> bool: return false -func _select_next_cell() -> Dictionary: +func _select_next_cell(placed_count: int) -> Dictionary: + if ( + _placements.size() >= LARGE_GRID_LIGHTWEIGHT_THRESHOLD + and placed_count % LARGE_GRID_PROPAGATION_INTERVAL != 0 + ): + return _select_next_large_grid_cell() # Rebuild and propagate the small domain table on every branch. This catches # unsupported connector chains before they turn into a deep recursive dead # end, while keeping placement state simple and deterministic. @@ -356,6 +385,54 @@ func _select_next_cell() -> Dictionary: return {"index": best_index, "candidates": best_candidates} +func _select_next_large_grid_cell() -> Dictionary: + # Full all-cell arc propagation scales cubically as the map grows. Large + # worlds instead use the same compatibility checks with a frontier-aware MRV + # pass. Exact placement validation and backtracking remain unchanged. + var best_index := -1 + var best_candidates: Array[TerrainChunkVariant] = [] + var best_placed_neighbors := -1 + for index: int in _placements.size(): + if _placements[index] != null: + continue + var candidates := _compatible_candidates(index) + if candidates.is_empty(): + return {"index": index, "candidates": candidates} + var placed_neighbors := _placed_neighbor_count(index) + if ( + best_index < 0 + or candidates.size() < best_candidates.size() + or ( + candidates.size() == best_candidates.size() + and placed_neighbors > best_placed_neighbors + ) + ): + best_index = index + best_candidates = candidates + best_placed_neighbors = placed_neighbors + return {"index": best_index, "candidates": best_candidates} + + +func _placed_neighbor_count(index: int) -> int: + var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) + var count := 0 + for edge_value: int in TerrainChunkTopology.Edge.values(): + var neighbor_coordinate := ( + coordinate + + TerrainChunkTopology.grid_offset( + edge_value as TerrainChunkTopology.Edge + ) + ) + if not _coordinate_is_inside_grid(neighbor_coordinate): + continue + var neighbor_index := ( + neighbor_coordinate.y * grid_size.x + neighbor_coordinate.x + ) + if _placements[neighbor_index] != null: + count += 1 + return count + + func _propagate_domains(domains: Dictionary) -> bool: var changed := true while changed: @@ -568,6 +645,11 @@ func _candidate_can_occupy_cell( if not _definition_has_capacity(candidate.definition): return false var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) + if ( + _elevated_feature_reserved_grass_indices.has(index) + and candidate.definition.stable_id != elevated_cliff_base_chunk_id + ): + return false var center := Vector2i(grid_size.x / 2, grid_size.y / 2) var center_index := center.y * grid_size.x + center.x if ( @@ -599,10 +681,123 @@ func _candidate_can_occupy_cell( ) +func _prepare_elevated_feature_region() -> bool: + _elevated_feature_center = Vector2i(-1, -1) + _elevated_feature_reserved_grass_indices.clear() + if not elevated_cliff_feature_enabled: + return true + for stable_id: StringName in [ + elevated_cliff_base_chunk_id, + elevated_cliff_top_chunk_id, + elevated_cliff_corner_chunk_id, + elevated_cliff_edge_chunk_id, + elevated_cliff_ramp_chunk_id, + ]: + if catalog.definition_for_id(stable_id) == null: + push_error("Elevated cliff feature references missing chunk %s." % stable_id) + return false + if grid_size.x < 11 or grid_size.y < 9: + push_error( + "Elevated cliff feature requires at least an 11x9 terrain grid." + ) + return false + var forced_center := Vector2i(grid_size.x / 2, grid_size.y / 2) + var candidates: Array[Vector2i] = [] + for row: int in range(2, grid_size.y - 2): + for column: int in range(2, grid_size.x - 2): + var candidate := Vector2i(column, row) + if ( + absi(candidate.x - forced_center.x) <= 2 + and absi(candidate.y - forced_center.y) <= 2 + ): + continue + candidates.append(candidate) + if candidates.is_empty(): + push_error("Terrain grid has no inland 3x3 cliff feature location.") + return false + var feature_random := RandomNumberGenerator.new() + feature_random.seed = generation_seed ^ 0x2E1E7A7ED + _elevated_feature_center = candidates[ + feature_random.randi_range(0, candidates.size() - 1) + ] + # Solve the authored 3x3 cliff assembly into a one-cell ring of ordinary + # level-one flat grass. Every raised piece still occupies one of the inner + # nine grass cells; the outer ring prevents a stream, pond, or coast profile + # from being exposed directly against its downhill edge. + for row_offset: int in range(-2, 3): + for column_offset: int in range(-2, 3): + var coordinate := ( + _elevated_feature_center + + Vector2i(column_offset, row_offset) + ) + _elevated_feature_reserved_grass_indices[ + coordinate.y * grid_size.x + coordinate.x + ] = true + return true + + +func _apply_elevated_feature() -> bool: + if not elevated_cliff_feature_enabled: + return true + if _elevated_feature_center.x < 0 or _elevated_feature_center.y < 0: + push_error("Elevated cliff feature has no reserved base region.") + return false + var edge_specs: Array[Dictionary] = [ + {"offset": Vector2i(0, -1), "turns": 1}, + {"offset": Vector2i(1, 0), "turns": 0}, + {"offset": Vector2i(0, 1), "turns": 3}, + {"offset": Vector2i(-1, 0), "turns": 2}, + ] + var feature_random := RandomNumberGenerator.new() + feature_random.seed = generation_seed ^ 0x51A7C11FF + var ramp_edge := -1 + if feature_random.randf() < elevated_cliff_ramp_chance: + ramp_edge = feature_random.randi_range(0, edge_specs.size() - 1) + var placements: Array[Dictionary] = [ + {"offset": Vector2i(-1, -1), "id": elevated_cliff_corner_chunk_id, "turns": 2}, + {"offset": Vector2i(1, -1), "id": elevated_cliff_corner_chunk_id, "turns": 1}, + {"offset": Vector2i(0, 0), "id": elevated_cliff_top_chunk_id, "turns": 0}, + {"offset": Vector2i(-1, 1), "id": elevated_cliff_corner_chunk_id, "turns": 3}, + {"offset": Vector2i(1, 1), "id": elevated_cliff_corner_chunk_id, "turns": 0}, + ] + for edge_index: int in edge_specs.size(): + var edge_spec := edge_specs[edge_index] + placements.append({ + "offset": edge_spec["offset"], + "id": ( + elevated_cliff_ramp_chunk_id + if edge_index == ramp_edge + else elevated_cliff_edge_chunk_id + ), + "turns": edge_spec["turns"], + }) + for spec: Dictionary in placements: + var definition := catalog.definition_for_id(spec["id"] as StringName) + var variant := _authored_variant(definition, int(spec["turns"])) + if variant == null: + push_error( + "Elevated cliff feature cannot resolve %s rotation %d." + % [spec["id"], spec["turns"]] + ) + return false + var coordinate := _elevated_feature_center + (spec["offset"] as Vector2i) + _placements[coordinate.y * grid_size.x + coordinate.x] = variant + var validation_error := _resolved_layout_validation_error(_placements) + if not validation_error.is_empty(): + push_error("Elevated cliff feature is invalid: " + validation_error) + return false + return true + + func _variant_respects_ocean_boundary( variant: TerrainChunkVariant, coordinate: Vector2i, ) -> bool: + if ( + variant.definition.must_be_interior + and _coordinate_is_on_boundary(coordinate) + ): + return false var ocean_edges := variant.rotated_edge_mask( variant.definition.ocean_facing_edges ) @@ -1231,7 +1426,7 @@ func _build_solution_root() -> Node3D: if variant == null: continue var coordinate := Vector2i(index % grid_size.x, index / grid_size.x) - var chunk_root := variant.definition.packed_scene.instantiate() as Node3D + var chunk_root := _instantiate_chunk_root(variant.definition) if chunk_root == null: push_error("%s does not instantiate as Node3D." % variant.stable_key()) solution_root.free() @@ -1260,6 +1455,27 @@ func _build_solution_root() -> Node3D: return solution_root +func _instantiate_chunk_root( + definition: TerrainChunkDefinition, +) -> Node3D: + var terrain_visual := definition.packed_scene.instantiate() as Node3D + if terrain_visual == null: + return null + if definition.base_layer_scene == null: + return terrain_visual + var base_layer_visual := definition.base_layer_scene.instantiate() as Node3D + if base_layer_visual == null: + terrain_visual.free() + return null + var layered_root := Node3D.new() + layered_root.name = "LayeredTerrainChunk" + base_layer_visual.name = "TerrainBaseLayer" + terrain_visual.name = "TerrainOverlay" + layered_root.add_child(base_layer_visual) + layered_root.add_child(terrain_visual) + return layered_root + + func _replace_generated_chunks(solution_root: Node3D) -> void: _clear_generated_chunks() _generated_chunks = solution_root @@ -1274,11 +1490,34 @@ func _add_collision( chunk_root, definition.primary_mesh_name, ) + _add_collision_to_mesh( + primary_mesh, + definition.stable_id, + "TerrainCollision", + ) + if definition.base_layer_scene == null: + return + var base_layer_mesh := TerrainChunkAnalyzer.find_primary_mesh( + chunk_root, + definition.base_layer_mesh_name, + ) + _add_collision_to_mesh( + base_layer_mesh, + definition.stable_id, + "TerrainBaseLayerCollision", + ) + + +func _add_collision_to_mesh( + primary_mesh: MeshInstance3D, + stable_id: StringName, + collision_name: String, +) -> void: if primary_mesh == null or primary_mesh.mesh == null: return var terrain_shape := primary_mesh.mesh.create_trimesh_shape() if terrain_shape == null or terrain_shape.get_faces().is_empty(): - push_warning("%s produced no terrain collision." % definition.stable_id) + push_warning("%s produced no terrain collision." % stable_id) return var concave_shape := terrain_shape as ConcavePolygonShape3D if concave_shape != null: @@ -1287,7 +1526,7 @@ func _add_collision( # can never become an invisible collision gap. concave_shape.backface_collision = true var collision_body := StaticBody3D.new() - collision_body.name = "TerrainCollision" + collision_body.name = collision_name collision_body.collision_layer = 1 collision_body.collision_mask = 0 primary_mesh.add_child(collision_body) diff --git a/world/generation/terrain_prop_catalog.gd b/world/generation/terrain_prop_catalog.gd index f3375b1..e73fbe0 100644 --- a/world/generation/terrain_prop_catalog.gd +++ b/world/generation/terrain_prop_catalog.gd @@ -82,4 +82,23 @@ func validation_errors() -> PackedStringArray: "%s has a negative box collision dimension." % definition.stable_id ) + if definition.minimum_visual_scale > definition.maximum_visual_scale: + errors.append( + "%s has an inverted visual scale range." + % definition.stable_id + ) + if ( + not definition.material_variants.is_empty() + and definition.variant_material_slot_names.is_empty() + ): + errors.append( + "%s has material variants but no target material slots." + % definition.stable_id + ) + for material: Material in definition.material_variants: + if material == null: + errors.append( + "%s contains an empty material variant." + % definition.stable_id + ) return errors diff --git a/world/generation/terrain_prop_definition.gd b/world/generation/terrain_prop_definition.gd index ca04b21..f6e73ae 100644 --- a/world/generation/terrain_prop_definition.gd +++ b/world/generation/terrain_prop_definition.gd @@ -18,6 +18,15 @@ extends Resource @export_range(0.0, 100.0, 0.5) var preferred_nearby_radius := 0.0 @export_range(1.0, 10.0, 0.1) var nearby_preference_weight_multiplier := 1.0 @export_range(0.0, 10.0, 0.05) var clearance_radius := 0.5 +## Procedural instances use a deterministic uniform scale in this range. +@export_range(0.1, 4.0, 0.05) var minimum_visual_scale := 1.0 +@export_range(0.1, 4.0, 0.05) var maximum_visual_scale := 1.0 +## Presentation-only offset. The prop root remains exactly terrain-aligned. +@export var visual_offset := Vector3.ZERO +## A single material is chosen per prop and applied only to matching imported +## material slots. This keeps trunks untouched while varying foliage. +@export var variant_material_slot_names := PackedStringArray() +@export var material_variants: Array[Material] = [] @export_range(0.0, 5.0, 0.05) var collision_radius := 0.0 @export_range(0.0, 20.0, 0.05) var collision_height := 0.0 @export var collision_box_size := Vector3.ZERO