Expand generated terrain with biome-driven props
|
|
@ -43,6 +43,7 @@ readonly -a QUICK_TESTS=(
|
||||||
"tests/shoreline_ambience_validation.gd"
|
"tests/shoreline_ambience_validation.gd"
|
||||||
"tests/surface_drawing_validation.gd"
|
"tests/surface_drawing_validation.gd"
|
||||||
"tests/tackle_order_validation.gd"
|
"tests/tackle_order_validation.gd"
|
||||||
|
"tests/terrain_biome_validation.gd"
|
||||||
"tests/terrain_blender_material_validation.gd"
|
"tests/terrain_blender_material_validation.gd"
|
||||||
"tests/terrain_chunk_generator_validation.gd"
|
"tests/terrain_chunk_generator_validation.gd"
|
||||||
"tests/texture_sampling_validation.gd"
|
"tests/texture_sampling_validation.gd"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,37 @@ const RegionScene: PackedScene = preload(
|
||||||
"res://world/generation/generated_world_region.tscn"
|
"res://world/generation/generated_world_region.tscn"
|
||||||
)
|
)
|
||||||
const FIRST_SEED := 13001
|
const FIRST_SEED := 13001
|
||||||
const SECOND_SEED := 13002
|
const SECOND_SEED := 13004
|
||||||
|
const EXPECTED_PROP_IDS: Array[StringName] = [
|
||||||
|
&"prop_bridge",
|
||||||
|
&"prop_dock",
|
||||||
|
&"prop_log",
|
||||||
|
&"prop_log_post_1",
|
||||||
|
&"prop_log_post_2",
|
||||||
|
&"prop_mushroom",
|
||||||
|
&"prop_palm",
|
||||||
|
&"prop_pine",
|
||||||
|
&"prop_timber_1",
|
||||||
|
&"prop_timber_2",
|
||||||
|
&"prop_tree_1",
|
||||||
|
&"prop_tree_2",
|
||||||
|
&"prop_tree_3",
|
||||||
|
]
|
||||||
|
const EXPECTED_BIOME_IDS: Array[StringName] = [
|
||||||
|
&"biome_plains",
|
||||||
|
&"biome_forest",
|
||||||
|
&"biome_pine_forest",
|
||||||
|
&"biome_coast",
|
||||||
|
]
|
||||||
|
const PROCEDURAL_PROP_IDS: Array[StringName] = [
|
||||||
|
&"prop_log",
|
||||||
|
&"prop_mushroom",
|
||||||
|
&"prop_palm",
|
||||||
|
&"prop_pine",
|
||||||
|
&"prop_tree_1",
|
||||||
|
&"prop_tree_2",
|
||||||
|
&"prop_tree_3",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -26,15 +56,20 @@ func _run() -> void:
|
||||||
_validate_generated_region(region, generator)
|
_validate_generated_region(region, generator)
|
||||||
|
|
||||||
var first_layout := generator.placement_keys()
|
var first_layout := generator.placement_keys()
|
||||||
|
var first_biomes := _biome_signature(region, generator)
|
||||||
var first_decorations := _decoration_signature(region)
|
var first_decorations := _decoration_signature(region)
|
||||||
assert(region.generate_world(FIRST_SEED))
|
assert(region.generate_world(FIRST_SEED))
|
||||||
assert(generator.placement_keys() == first_layout)
|
assert(generator.placement_keys() == first_layout)
|
||||||
|
assert(_biome_signature(region, generator) == first_biomes)
|
||||||
assert(_decoration_signature(region) == first_decorations)
|
assert(_decoration_signature(region) == first_decorations)
|
||||||
|
|
||||||
assert(region.generate_world(SECOND_SEED))
|
assert(region.generate_world(SECOND_SEED))
|
||||||
|
await process_frame
|
||||||
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
_validate_generated_region(region, generator)
|
_validate_generated_region(region, generator)
|
||||||
assert(generator.placement_keys() != first_layout)
|
assert(generator.placement_keys() != first_layout)
|
||||||
|
assert(_biome_signature(region, generator) != first_biomes)
|
||||||
|
|
||||||
region.queue_free()
|
region.queue_free()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
@ -47,14 +82,18 @@ func _validate_generated_region(
|
||||||
generator: TerrainChunkGenerator,
|
generator: TerrainChunkGenerator,
|
||||||
) -> void:
|
) -> void:
|
||||||
var placements := generator.placement_keys()
|
var placements := generator.placement_keys()
|
||||||
assert(placements.size() == 25)
|
assert(placements.size() == 49)
|
||||||
assert(placements[12].begins_with("chunk_spawn@"))
|
assert(placements[24].begins_with("chunk_spawn@"))
|
||||||
assert(_placement_count(placements, "chunk_spawn") == 1)
|
assert(_placement_count(placements, "chunk_spawn") == 1)
|
||||||
assert(_placement_count(placements, "chunk_0001") >= 1)
|
assert(_placement_count(placements, "chunk_0001") >= 1)
|
||||||
assert(_placement_count(placements, "chunk_0002") >= 1)
|
assert(_placement_count(placements, "chunk_0002") >= 1)
|
||||||
assert(_placement_count(placements, "chunk_0003") >= 1)
|
assert(_placement_count(placements, "chunk_0003") >= 1)
|
||||||
assert(_placement_count(placements, "chunk_0004") >= 1)
|
assert(_placement_count(placements, "chunk_0004") >= 1)
|
||||||
assert(generator.get_generated_chunks_root().get_child_count() == 25)
|
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)
|
||||||
|
|
||||||
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
|
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
|
||||||
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
|
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
|
||||||
|
|
@ -65,6 +104,8 @@ func _validate_generated_region(
|
||||||
assert(region.get_fishing_shop() != null)
|
assert(region.get_fishing_shop() != null)
|
||||||
assert(region.get_player_storage() != null)
|
assert(region.get_player_storage() != null)
|
||||||
assert(region.get_player_spawn_transform().origin.y > 0.0)
|
assert(region.get_player_spawn_transform().origin.y > 0.0)
|
||||||
|
_validate_prop_catalog(region)
|
||||||
|
_validate_biome_catalog(region, generator)
|
||||||
|
|
||||||
var ocean := region.get_node("WaterBodies/OceanWater") as WaterBodyAuthoring
|
var ocean := region.get_node("WaterBodies/OceanWater") as WaterBodyAuthoring
|
||||||
var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
|
var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
|
||||||
|
|
@ -73,6 +114,8 @@ func _validate_generated_region(
|
||||||
var fresh_placement_count := 0
|
var fresh_placement_count := 0
|
||||||
for record: Dictionary in generator.placement_records():
|
for record: Dictionary in generator.placement_records():
|
||||||
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
||||||
|
if "coast" in tags:
|
||||||
|
_validate_ocean_facing_record(record, generator.grid_size)
|
||||||
if "fresh_water" in tags:
|
if "fresh_water" in tags:
|
||||||
fresh_placement_count += 1
|
fresh_placement_count += 1
|
||||||
assert(fresh_root.get_child_count() == fresh_placement_count)
|
assert(fresh_root.get_child_count() == fresh_placement_count)
|
||||||
|
|
@ -95,38 +138,185 @@ func _validate_generated_region(
|
||||||
assert(anchors != null)
|
assert(anchors != null)
|
||||||
assert(anchors.get_spawn_positions().size() > 0)
|
assert(anchors.get_spawn_positions().size() > 0)
|
||||||
for child: Node in decorations.get_children():
|
for child: Node in decorations.get_children():
|
||||||
assert(
|
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
||||||
child.name.begins_with("regular_tree_")
|
assert(PROCEDURAL_PROP_IDS.has(prop_id))
|
||||||
or child.name.begins_with("palm_tree_")
|
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||||
|
assert(definition != null)
|
||||||
|
var biome_id := StringName(
|
||||||
|
child.get_meta(&"terrain_biome_id", &"")
|
||||||
)
|
)
|
||||||
_validate_decoration_transform(child as Node3D)
|
var biome := region.get_biome_catalog().definition_for_id(biome_id)
|
||||||
assert(_decoration_count(decorations, "regular_tree_") > 0)
|
assert(biome != null)
|
||||||
assert(_decoration_count(decorations, "palm_tree_") > 0)
|
var biome_rule := biome.prop_rule_for_group(
|
||||||
|
definition.procedural_group
|
||||||
|
)
|
||||||
|
assert(biome_rule != null and biome_rule.allows_prop(definition))
|
||||||
|
var coordinate: Vector2i = child.get_meta(
|
||||||
|
&"terrain_chunk_coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
var center := Vector2i(
|
||||||
|
generator.grid_size.x / 2,
|
||||||
|
generator.grid_size.y / 2,
|
||||||
|
)
|
||||||
|
var spawn_distance := (
|
||||||
|
absi(coordinate.x - center.x)
|
||||||
|
+ absi(coordinate.y - center.y)
|
||||||
|
)
|
||||||
|
assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
|
||||||
|
_validate_decoration_transform(
|
||||||
|
region,
|
||||||
|
child as Node3D,
|
||||||
|
definition,
|
||||||
|
)
|
||||||
|
assert(_decoration_group_count(region, &"grass_tree") > 0)
|
||||||
|
assert(_decoration_group_count(region, &"sand_tree") > 0)
|
||||||
|
assert(
|
||||||
|
_decoration_biome_group_count(
|
||||||
|
region,
|
||||||
|
&"biome_forest",
|
||||||
|
&"grass_tree",
|
||||||
|
) >= 2
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
_decoration_biome_group_count(
|
||||||
|
region,
|
||||||
|
&"biome_pine_forest",
|
||||||
|
&"grass_tree",
|
||||||
|
) >= 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _validate_decoration_transform(prop: Node3D) -> void:
|
func _validate_ocean_facing_record(
|
||||||
|
record: Dictionary,
|
||||||
|
grid_size: Vector2i,
|
||||||
|
) -> void:
|
||||||
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||||
|
var ocean_edges := int(record.get("ocean_facing_edges", 0))
|
||||||
|
assert(ocean_edges != 0)
|
||||||
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
if (ocean_edges & (1 << edge_value)) == 0:
|
||||||
|
continue
|
||||||
|
var ocean_coordinate := (
|
||||||
|
coordinate
|
||||||
|
+ TerrainChunkTopology.grid_offset(
|
||||||
|
edge_value as TerrainChunkTopology.Edge
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
ocean_coordinate.x < 0
|
||||||
|
or ocean_coordinate.y < 0
|
||||||
|
or ocean_coordinate.x >= grid_size.x
|
||||||
|
or ocean_coordinate.y >= grid_size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
||||||
|
var catalog := region.get_prop_catalog()
|
||||||
|
assert(catalog != null)
|
||||||
|
assert(catalog.validation_errors().is_empty())
|
||||||
|
assert(catalog.definitions.size() == EXPECTED_PROP_IDS.size())
|
||||||
|
for stable_id: StringName in EXPECTED_PROP_IDS:
|
||||||
|
var definition := catalog.definition_for_id(stable_id)
|
||||||
|
assert(definition != null and definition.packed_scene != null)
|
||||||
|
var instance := definition.packed_scene.instantiate() as Node3D
|
||||||
|
assert(instance != null)
|
||||||
|
assert(instance.position.is_zero_approx())
|
||||||
|
assert(_find_mesh_instance(instance) != null)
|
||||||
|
instance.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_biome_catalog(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
generator: TerrainChunkGenerator,
|
||||||
|
) -> void:
|
||||||
|
var catalog := region.get_biome_catalog()
|
||||||
|
assert(catalog != null)
|
||||||
|
assert(catalog.validation_errors().is_empty())
|
||||||
|
assert(catalog.definitions.size() == EXPECTED_BIOME_IDS.size())
|
||||||
|
var counts: Dictionary[StringName, int] = {}
|
||||||
|
for record: Dictionary in generator.placement_records():
|
||||||
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||||
|
var biome_id := region.get_biome_at(coordinate)
|
||||||
|
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
||||||
|
if "grass" in tags or "sand" in tags:
|
||||||
|
assert(biome_id != &"")
|
||||||
|
var biome := catalog.definition_for_id(biome_id)
|
||||||
|
assert(biome != null and biome.supports_chunk_tags(tags))
|
||||||
|
counts[biome_id] = counts.get(biome_id, 0) + 1
|
||||||
|
else:
|
||||||
|
assert(biome_id == &"")
|
||||||
|
for biome_id: StringName in EXPECTED_BIOME_IDS:
|
||||||
|
assert(counts.get(biome_id, 0) > 0)
|
||||||
|
var center := Vector2i(
|
||||||
|
generator.grid_size.x / 2,
|
||||||
|
generator.grid_size.y / 2,
|
||||||
|
)
|
||||||
|
assert(region.get_biome_at(center) == &"biome_plains")
|
||||||
|
for child: Node in generator.get_generated_chunks_root().get_children():
|
||||||
|
var coordinate: Vector2i = child.get_meta(
|
||||||
|
&"terrain_chunk_coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
StringName(child.get_meta(&"terrain_biome_id", &""))
|
||||||
|
== region.get_biome_at(coordinate)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_decoration_transform(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
prop: Node3D,
|
||||||
|
definition: TerrainPropDefinition,
|
||||||
|
) -> void:
|
||||||
assert(prop != null)
|
assert(prop != null)
|
||||||
assert(prop.scale.is_equal_approx(Vector3.ONE))
|
assert(prop.scale.is_equal_approx(Vector3.ONE))
|
||||||
var visual := prop.get_node_or_null("Visual") as MeshInstance3D
|
var visual_root := prop.get_node_or_null("Visual") as Node3D
|
||||||
|
var visual := _find_mesh_instance(visual_root)
|
||||||
var collision := prop.get_node_or_null(
|
var collision := prop.get_node_or_null(
|
||||||
"TrunkCollision/CollisionShape"
|
"TrunkCollision/CollisionShape"
|
||||||
) as CollisionShape3D
|
) as CollisionShape3D
|
||||||
|
assert(visual_root != null and visual_root.position.is_zero_approx())
|
||||||
assert(visual != null and visual.mesh != null)
|
assert(visual != null and visual.mesh != null)
|
||||||
assert(collision != null and collision.shape is CylinderShape3D)
|
if definition.has_collision():
|
||||||
assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
|
assert(collision != null)
|
||||||
var bounds := visual.mesh.get_aabb()
|
assert(
|
||||||
var minimum_y := INF
|
collision.shape is CylinderShape3D
|
||||||
for corner_index: int in 8:
|
or collision.shape is BoxShape3D
|
||||||
var corner := bounds.position + Vector3(
|
|
||||||
bounds.size.x if (corner_index & 1) != 0 else 0.0,
|
|
||||||
bounds.size.y if (corner_index & 2) != 0 else 0.0,
|
|
||||||
bounds.size.z if (corner_index & 4) != 0 else 0.0,
|
|
||||||
)
|
)
|
||||||
minimum_y = minf(
|
assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
|
||||||
minimum_y,
|
else:
|
||||||
(visual.global_transform * corner).y,
|
assert(collision == null)
|
||||||
)
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
assert(absf(minimum_y - prop.global_position.y) <= 0.01)
|
prop.global_position + Vector3.UP * 8.0,
|
||||||
|
prop.global_position + Vector3.DOWN * 8.0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
var prop_body := prop.get_node_or_null(
|
||||||
|
"TrunkCollision",
|
||||||
|
) as CollisionObject3D
|
||||||
|
if prop_body != null:
|
||||||
|
query.exclude = [prop_body.get_rid()]
|
||||||
|
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
|
||||||
|
assert(
|
||||||
|
not hit.is_empty(),
|
||||||
|
"No terrain collision below %s (%s) at %s."
|
||||||
|
% [prop.name, definition.stable_id, prop.global_position],
|
||||||
|
)
|
||||||
|
var ground_position: Vector3 = hit["position"]
|
||||||
|
assert(absf(ground_position.y - prop.global_position.y) <= 0.01)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
|
||||||
|
if root_node is MeshInstance3D:
|
||||||
|
return root_node as MeshInstance3D
|
||||||
|
if root_node == null:
|
||||||
|
return null
|
||||||
|
for child: Node in root_node.get_children():
|
||||||
|
var mesh_instance := _find_mesh_instance(child)
|
||||||
|
if mesh_instance != null:
|
||||||
|
return mesh_instance
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func _validate_authored_chunk_surfaces(
|
func _validate_authored_chunk_surfaces(
|
||||||
|
|
@ -136,6 +326,9 @@ func _validate_authored_chunk_surfaces(
|
||||||
var generated := generator.get_generated_chunks_root()
|
var generated := generator.get_generated_chunks_root()
|
||||||
var found_beach := false
|
var found_beach := false
|
||||||
var found_pond := false
|
var found_pond := false
|
||||||
|
var found_grass_ocean_edge := false
|
||||||
|
var found_grass_beach_transition := false
|
||||||
|
var found_grass_ocean_corner := false
|
||||||
for chunk_root: Node in generated.get_children():
|
for chunk_root: Node in generated.get_children():
|
||||||
if chunk_root.name.begins_with("chunk_0002r"):
|
if chunk_root.name.begins_with("chunk_0002r"):
|
||||||
var beach := chunk_root.find_child(
|
var beach := chunk_root.find_child(
|
||||||
|
|
@ -157,8 +350,53 @@ func _validate_authored_chunk_surfaces(
|
||||||
assert(normal.y >= -0.001)
|
assert(normal.y >= -0.001)
|
||||||
_validate_pond_collision(region, pond)
|
_validate_pond_collision(region, pond)
|
||||||
found_pond = true
|
found_pond = true
|
||||||
|
elif chunk_root.name.begins_with("chunk_0005r"):
|
||||||
|
var grass_ocean_edge := chunk_root.find_child(
|
||||||
|
"chunk_0005", true, false
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(grass_ocean_edge != null and grass_ocean_edge.mesh != null)
|
||||||
|
var edge_materials := _material_names(grass_ocean_edge)
|
||||||
|
assert("grass_lite" in edge_materials)
|
||||||
|
assert("cliff_wall" in edge_materials)
|
||||||
|
found_grass_ocean_edge = true
|
||||||
|
elif chunk_root.name.begins_with("chunk_0006r"):
|
||||||
|
var grass_beach_transition := chunk_root.find_child(
|
||||||
|
"chunk_0006", true, false
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(
|
||||||
|
grass_beach_transition != null
|
||||||
|
and grass_beach_transition.mesh != null
|
||||||
|
)
|
||||||
|
var transition_materials := _material_names(
|
||||||
|
grass_beach_transition
|
||||||
|
)
|
||||||
|
assert("sand" in transition_materials)
|
||||||
|
assert("grass_lite" in transition_materials)
|
||||||
|
assert("cliff_wall" in transition_materials)
|
||||||
|
found_grass_beach_transition = true
|
||||||
|
elif chunk_root.name.begins_with("chunk_0007r"):
|
||||||
|
var grass_ocean_corner := chunk_root.find_child(
|
||||||
|
"chunk_0007", true, false
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(grass_ocean_corner != null and grass_ocean_corner.mesh != null)
|
||||||
|
var corner_materials := _material_names(grass_ocean_corner)
|
||||||
|
assert("grass_lite" in corner_materials)
|
||||||
|
assert("cliff_wall" in corner_materials)
|
||||||
|
found_grass_ocean_corner = true
|
||||||
assert(found_beach)
|
assert(found_beach)
|
||||||
assert(found_pond)
|
assert(found_pond)
|
||||||
|
assert(found_grass_ocean_edge)
|
||||||
|
assert(found_grass_beach_transition)
|
||||||
|
assert(found_grass_ocean_corner)
|
||||||
|
|
||||||
|
|
||||||
|
func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
|
||||||
|
var result := PackedStringArray()
|
||||||
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||||
|
var material := mesh_instance.get_active_material(surface_index)
|
||||||
|
if material != null:
|
||||||
|
result.append(material.resource_name)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
func _validate_pond_collision(
|
func _validate_pond_collision(
|
||||||
|
|
@ -207,14 +445,50 @@ func _validate_pond_collision(
|
||||||
assert(collider != null and collider.get_parent() == pond)
|
assert(collider != null and collider.get_parent() == pond)
|
||||||
|
|
||||||
|
|
||||||
func _decoration_count(decorations: Node3D, prefix: String) -> int:
|
func _decoration_group_count(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
group: StringName,
|
||||||
|
) -> int:
|
||||||
var count := 0
|
var count := 0
|
||||||
|
var decorations := region.get_node("Decorations") as Node3D
|
||||||
for child: Node in decorations.get_children():
|
for child: Node in decorations.get_children():
|
||||||
if child.name.begins_with(prefix):
|
var stable_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
||||||
|
var definition := region.get_prop_catalog().definition_for_id(stable_id)
|
||||||
|
if definition != null and definition.procedural_group == group:
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func _decoration_biome_group_count(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
biome_id: StringName,
|
||||||
|
group: StringName,
|
||||||
|
) -> int:
|
||||||
|
var count := 0
|
||||||
|
var decorations := region.get_node("Decorations") as Node3D
|
||||||
|
for child: Node in decorations.get_children():
|
||||||
|
if StringName(child.get_meta(&"terrain_biome_id", &"")) != biome_id:
|
||||||
|
continue
|
||||||
|
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
||||||
|
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
||||||
|
if definition != null and definition.procedural_group == group:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func _biome_signature(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
generator: TerrainChunkGenerator,
|
||||||
|
) -> PackedStringArray:
|
||||||
|
var result := PackedStringArray()
|
||||||
|
for row: int in generator.grid_size.y:
|
||||||
|
for column: int in generator.grid_size.x:
|
||||||
|
result.append(
|
||||||
|
String(region.get_biome_at(Vector2i(column, row)))
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
func _placement_count(keys: PackedStringArray, stable_id: String) -> int:
|
func _placement_count(keys: PackedStringArray, stable_id: String) -> int:
|
||||||
var count := 0
|
var count := 0
|
||||||
for key: String in keys:
|
for key: String in keys:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ extends SceneTree
|
||||||
const CATALOG: TerrainChunkCatalog = preload(
|
const CATALOG: TerrainChunkCatalog = preload(
|
||||||
"res://world/generation/chunks/terrain_chunk_catalog.tres"
|
"res://world/generation/chunks/terrain_chunk_catalog.tres"
|
||||||
)
|
)
|
||||||
const MATCH_TOLERANCE := 0.01
|
const BIOME_CATALOG: TerrainBiomeCatalog = preload(
|
||||||
|
"res://world/generation/biomes/terrain_biome_catalog.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -11,30 +13,34 @@ func _initialize() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
|
var generator := _make_generator()
|
||||||
|
root.add_child(generator)
|
||||||
|
if not generator._prepare_catalog():
|
||||||
|
generator.free()
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
var variants: Array[TerrainChunkVariant] = []
|
var variants: Array[TerrainChunkVariant] = []
|
||||||
for definition: TerrainChunkDefinition in CATALOG.definitions:
|
variants.assign(generator._solver_variants)
|
||||||
var seen: Dictionary[String, bool] = {}
|
|
||||||
for variant: TerrainChunkVariant in TerrainChunkAnalyzer.create_variants(
|
|
||||||
definition,
|
|
||||||
CATALOG.chunk_size,
|
|
||||||
):
|
|
||||||
var signature := variant.topology_signature(0.001)
|
|
||||||
if seen.has(signature):
|
|
||||||
continue
|
|
||||||
seen[signature] = true
|
|
||||||
variants.append(variant)
|
|
||||||
|
|
||||||
print("Terrain chunk variants: %d" % variants.size())
|
print(
|
||||||
|
"Terrain chunk rotations: %d authored, %d distinct constraints"
|
||||||
|
% [generator._variants.size(), variants.size()]
|
||||||
|
)
|
||||||
for variant: TerrainChunkVariant in variants:
|
for variant: TerrainChunkVariant in variants:
|
||||||
print("\n", variant.stable_key())
|
print("\n", variant.stable_key())
|
||||||
|
var ocean_edges := variant.rotated_edge_mask(
|
||||||
|
variant.definition.ocean_facing_edges
|
||||||
|
)
|
||||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
var edge := edge_value as TerrainChunkTopology.Edge
|
var edge := edge_value as TerrainChunkTopology.Edge
|
||||||
var matches := PackedStringArray()
|
var matches := PackedStringArray()
|
||||||
for other: TerrainChunkVariant in variants:
|
for other: TerrainChunkVariant in variants:
|
||||||
var opposite := TerrainChunkTopology.opposite_edge(edge)
|
var opposite := TerrainChunkTopology.opposite_edge(edge)
|
||||||
if variant.profile(edge).matches(
|
if generator._edges_are_compatible(
|
||||||
other.profile(opposite),
|
variant,
|
||||||
MATCH_TOLERANCE,
|
edge,
|
||||||
|
other,
|
||||||
|
opposite,
|
||||||
):
|
):
|
||||||
matches.append(
|
matches.append(
|
||||||
"%s.%s"
|
"%s.%s"
|
||||||
|
|
@ -44,17 +50,61 @@ func _run() -> void:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
" %s [%d points]: %s"
|
" %s%s [%d points]: %s"
|
||||||
% [
|
% [
|
||||||
TerrainChunkTopology.edge_name(edge),
|
TerrainChunkTopology.edge_name(edge),
|
||||||
|
(
|
||||||
|
" [ocean]"
|
||||||
|
if (ocean_edges & (1 << edge_value)) != 0
|
||||||
|
else ""
|
||||||
|
),
|
||||||
variant.profile(edge).points.size(),
|
variant.profile(edge).points.size(),
|
||||||
", ".join(matches) if not matches.is_empty() else "NONE",
|
", ".join(matches) if not matches.is_empty() else "NONE",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if generator.generate():
|
||||||
|
var summary := generator._build_summary()
|
||||||
|
print(
|
||||||
|
(
|
||||||
|
"\nSeeded diagnostic layout %s "
|
||||||
|
+ "(%d backtracks, %d repeated edges):"
|
||||||
|
)
|
||||||
|
% [
|
||||||
|
str(summary["layout_fingerprint"]).substr(0, 12),
|
||||||
|
int(summary["backtracks"]),
|
||||||
|
int(summary["adjacent_repeat_edges"]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var keys := generator.placement_keys()
|
||||||
|
for row: int in generator.grid_size.y:
|
||||||
|
var cells := PackedStringArray()
|
||||||
|
for column: int in generator.grid_size.x:
|
||||||
|
cells.append(keys[row * generator.grid_size.x + column])
|
||||||
|
print(" ", " ".join(cells))
|
||||||
|
var biomes := TerrainBiomeAssigner.assign(
|
||||||
|
BIOME_CATALOG,
|
||||||
|
generator.placement_records(),
|
||||||
|
generator.generation_seed,
|
||||||
|
)
|
||||||
|
print("\nBiome layout:")
|
||||||
|
for row: int in generator.grid_size.y:
|
||||||
|
var cells := PackedStringArray()
|
||||||
|
for column: int in generator.grid_size.x:
|
||||||
|
var biome_id: StringName = biomes.get(
|
||||||
|
Vector2i(column, row),
|
||||||
|
&"",
|
||||||
|
)
|
||||||
|
cells.append(String(biome_id).trim_prefix("biome_"))
|
||||||
|
print(" ", " ".join(cells))
|
||||||
|
generator.free()
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_generator() -> TerrainChunkGenerator:
|
||||||
var generator := TerrainChunkGenerator.new()
|
var generator := TerrainChunkGenerator.new()
|
||||||
generator.catalog = CATALOG
|
generator.catalog = CATALOG
|
||||||
generator.grid_size = Vector2i(5, 5)
|
generator.grid_size = Vector2i(7, 7)
|
||||||
generator.generation_seed = 13001
|
generator.generation_seed = 13001
|
||||||
generator.generate_on_ready = false
|
generator.generate_on_ready = false
|
||||||
generator.build_collision = false
|
generator.build_collision = false
|
||||||
|
|
@ -66,16 +116,9 @@ func _run() -> void:
|
||||||
"chunk_0002",
|
"chunk_0002",
|
||||||
"chunk_0003",
|
"chunk_0003",
|
||||||
"chunk_0004",
|
"chunk_0004",
|
||||||
|
"chunk_0005",
|
||||||
|
"chunk_0006",
|
||||||
|
"chunk_0007",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
root.add_child(generator)
|
return generator
|
||||||
if generator.generate():
|
|
||||||
print("\nSeeded diagnostic layout:")
|
|
||||||
var keys := generator.placement_keys()
|
|
||||||
for row: int in generator.grid_size.y:
|
|
||||||
var cells := PackedStringArray()
|
|
||||||
for column: int in generator.grid_size.x:
|
|
||||||
cells.append(keys[row * generator.grid_size.x + column])
|
|
||||||
print(" ", " ".join(cells))
|
|
||||||
generator.free()
|
|
||||||
quit(0)
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,14 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
func _on_generation_completed(summary: Dictionary) -> void:
|
func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
_status.text = (
|
_status.text = (
|
||||||
"Terrain generator diagnostic • seed %d • %d chunks • "
|
"Terrain generator diagnostic • seed %d • %d chunks • "
|
||||||
+ "%d variants • %d backtracks\nR: regenerate • Esc/B: close"
|
+ "%d rotations / %d constraints • %d backtracks • "
|
||||||
|
+ "%d repeated edges\nlayout %s • R: regenerate • Esc/B: close"
|
||||||
) % [
|
) % [
|
||||||
int(summary["seed"]),
|
int(summary["seed"]),
|
||||||
int(summary["chunk_count"]),
|
int(summary["chunk_count"]),
|
||||||
int(summary["variant_count"]),
|
int(summary["variant_count"]),
|
||||||
|
int(summary["solver_variant_count"]),
|
||||||
int(summary["backtracks"]),
|
int(summary["backtracks"]),
|
||||||
|
int(summary["adjacent_repeat_edges"]),
|
||||||
|
str(summary["layout_fingerprint"]).substr(0, 12),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,12 @@ shadow_enabled = true
|
||||||
[node name="TerrainChunkGenerator" type="Node3D" parent="."]
|
[node name="TerrainChunkGenerator" type="Node3D" parent="."]
|
||||||
script = ExtResource("2_generator")
|
script = ExtResource("2_generator")
|
||||||
catalog = ExtResource("3_catalog")
|
catalog = ExtResource("3_catalog")
|
||||||
grid_size = Vector2i(5, 5)
|
grid_size = Vector2i(7, 7)
|
||||||
generation_seed = 13001
|
generation_seed = 13001
|
||||||
build_collision = true
|
build_collision = true
|
||||||
show_chunk_labels = false
|
show_chunk_labels = false
|
||||||
force_center_chunk_id = &"chunk_spawn"
|
force_center_chunk_id = &"chunk_spawn"
|
||||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
|
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007")
|
||||||
|
|
||||||
[node name="Player" parent="." instance=ExtResource("4_player")]
|
[node name="Player" parent="." instance=ExtResource("4_player")]
|
||||||
position = Vector3(0, 0.3, 0)
|
position = Vector3(0, 0.3, 0)
|
||||||
|
|
|
||||||
135
tests/terrain_biome_validation.gd
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const BIOME_CATALOG: TerrainBiomeCatalog = preload(
|
||||||
|
"res://world/generation/biomes/terrain_biome_catalog.tres"
|
||||||
|
)
|
||||||
|
const PROP_CATALOG: TerrainPropCatalog = preload(
|
||||||
|
"res://world/generation/props/terrain_prop_catalog.tres"
|
||||||
|
)
|
||||||
|
const EXPECTED_BIOMES: Array[StringName] = [
|
||||||
|
&"biome_plains",
|
||||||
|
&"biome_forest",
|
||||||
|
&"biome_pine_forest",
|
||||||
|
&"biome_coast",
|
||||||
|
]
|
||||||
|
const TEST_SEED := 13001
|
||||||
|
|
||||||
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred(&"_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_validate_catalog()
|
||||||
|
_validate_assignment()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_catalog() -> void:
|
||||||
|
for error: String in BIOME_CATALOG.validation_errors():
|
||||||
|
_check(false, error)
|
||||||
|
_check(
|
||||||
|
BIOME_CATALOG.definitions.size() == EXPECTED_BIOMES.size(),
|
||||||
|
"The biome catalog must contain all initial authored profiles.",
|
||||||
|
)
|
||||||
|
for biome_id: StringName in EXPECTED_BIOMES:
|
||||||
|
var biome := BIOME_CATALOG.definition_for_id(biome_id)
|
||||||
|
_check(biome != null, "The biome catalog is missing %s." % biome_id)
|
||||||
|
if biome == null:
|
||||||
|
continue
|
||||||
|
for rule: TerrainBiomePropRule in biome.prop_rules:
|
||||||
|
for prop_id_value: String in rule.allowed_prop_ids:
|
||||||
|
var prop := PROP_CATALOG.definition_for_id(
|
||||||
|
StringName(prop_id_value)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
prop != null and rule.allows_prop(prop),
|
||||||
|
"%s has an invalid %s prop rule."
|
||||||
|
% [biome_id, prop_id_value],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_assignment() -> void:
|
||||||
|
var records := _synthetic_records()
|
||||||
|
var first := TerrainBiomeAssigner.assign(
|
||||||
|
BIOME_CATALOG,
|
||||||
|
records,
|
||||||
|
TEST_SEED,
|
||||||
|
)
|
||||||
|
var second := TerrainBiomeAssigner.assign(
|
||||||
|
BIOME_CATALOG,
|
||||||
|
records,
|
||||||
|
TEST_SEED,
|
||||||
|
)
|
||||||
|
_check(first == second, "Biome assignment must be deterministic by seed.")
|
||||||
|
_check(
|
||||||
|
TerrainBiomeAssigner.fingerprint(first).length() == 64,
|
||||||
|
"Biome assignments must expose a SHA-256 fingerprint.",
|
||||||
|
)
|
||||||
|
var counts := TerrainBiomeAssigner.counts(first)
|
||||||
|
for biome_id: StringName in EXPECTED_BIOMES:
|
||||||
|
_check(
|
||||||
|
counts.get(biome_id, 0) > 0,
|
||||||
|
"The synthetic map must contain %s." % biome_id,
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.get(Vector2i(3, 3), &"") == &"biome_plains",
|
||||||
|
"The spawn-tagged center must remain plains.",
|
||||||
|
)
|
||||||
|
for record: Dictionary in records:
|
||||||
|
var coordinate: Vector2i = record["coordinate"]
|
||||||
|
var tags: PackedStringArray = record["tags"]
|
||||||
|
var biome_id: StringName = first.get(coordinate, &"")
|
||||||
|
if "fresh_water" in tags:
|
||||||
|
_check(
|
||||||
|
biome_id == &"",
|
||||||
|
"Freshwater-only chunks must not inherit a land-prop biome.",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
var biome := BIOME_CATALOG.definition_for_id(biome_id)
|
||||||
|
_check(
|
||||||
|
biome != null and biome.supports_chunk_tags(tags),
|
||||||
|
"Biome %s does not support chunk %s."
|
||||||
|
% [biome_id, coordinate],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _synthetic_records() -> Array[Dictionary]:
|
||||||
|
var result: Array[Dictionary] = []
|
||||||
|
for row: int in 7:
|
||||||
|
for column: int in 7:
|
||||||
|
var coordinate := Vector2i(column, row)
|
||||||
|
var tags := PackedStringArray(["land", "walkable", "grass"])
|
||||||
|
if row == 0:
|
||||||
|
tags = PackedStringArray(["land", "walkable", "sand"])
|
||||||
|
elif coordinate == Vector2i(0, 6):
|
||||||
|
tags = PackedStringArray([
|
||||||
|
"land",
|
||||||
|
"walkable",
|
||||||
|
"fresh_water",
|
||||||
|
"pond",
|
||||||
|
])
|
||||||
|
elif coordinate == Vector2i(3, 3):
|
||||||
|
tags.append("spawn")
|
||||||
|
result.append({
|
||||||
|
"coordinate": coordinate,
|
||||||
|
"tags": tags,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
_failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if _failures.is_empty():
|
||||||
|
print("Terrain biome validation: PASS")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure: String in _failures:
|
||||||
|
printerr("Terrain biome validation: ", failure)
|
||||||
|
quit(1)
|
||||||
1
tests/terrain_biome_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cxioi62f6oap0
|
||||||
|
|
@ -9,6 +9,9 @@ const EXPECTED_IDS: Array[String] = [
|
||||||
"chunk_0002",
|
"chunk_0002",
|
||||||
"chunk_0003",
|
"chunk_0003",
|
||||||
"chunk_0004",
|
"chunk_0004",
|
||||||
|
"chunk_0005",
|
||||||
|
"chunk_0006",
|
||||||
|
"chunk_0007",
|
||||||
"chunk_spawn",
|
"chunk_spawn",
|
||||||
]
|
]
|
||||||
const REQUIRED_IDS: Array[String] = [
|
const REQUIRED_IDS: Array[String] = [
|
||||||
|
|
@ -17,8 +20,24 @@ const REQUIRED_IDS: Array[String] = [
|
||||||
"chunk_0002",
|
"chunk_0002",
|
||||||
"chunk_0003",
|
"chunk_0003",
|
||||||
"chunk_0004",
|
"chunk_0004",
|
||||||
|
"chunk_0005",
|
||||||
|
"chunk_0006",
|
||||||
|
"chunk_0007",
|
||||||
]
|
]
|
||||||
const TEST_SEED := 13001
|
const TEST_SEED := 13001
|
||||||
|
const CONNECTOR_STRESS_SEED := 287
|
||||||
|
const EXPECTED_SOLVER_VARIANT_COUNT := 27
|
||||||
|
const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
|
||||||
|
&"chunk_0000": 4,
|
||||||
|
&"chunk_0001": 4,
|
||||||
|
&"chunk_0002": 4,
|
||||||
|
&"chunk_0003": 4,
|
||||||
|
&"chunk_0004": 4,
|
||||||
|
&"chunk_0005": 4,
|
||||||
|
&"chunk_0006": 4,
|
||||||
|
&"chunk_0007": 4,
|
||||||
|
&"chunk_spawn": 1,
|
||||||
|
}
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
|
|
@ -50,6 +69,130 @@ func _validate_catalog() -> void:
|
||||||
definition.packed_scene != null,
|
definition.packed_scene != null,
|
||||||
"%s must reference an imported GLB." % stable_id,
|
"%s must reference an imported GLB." % stable_id,
|
||||||
)
|
)
|
||||||
|
var grass := CATALOG.definition_for_id(&"chunk_0000")
|
||||||
|
var sand := CATALOG.definition_for_id(&"chunk_0001")
|
||||||
|
var beach := CATALOG.definition_for_id(&"chunk_0002")
|
||||||
|
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 spawn := CATALOG.definition_for_id(&"chunk_spawn")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
grass != null
|
||||||
|
and sand != null
|
||||||
|
and beach != null
|
||||||
|
and grass_ocean_edge != null
|
||||||
|
and grass_beach_transition != null
|
||||||
|
and grass_ocean_corner != null
|
||||||
|
and spawn != null
|
||||||
|
),
|
||||||
|
"The authored biome-rule definitions must be available.",
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
grass != null
|
||||||
|
and sand != null
|
||||||
|
and beach != null
|
||||||
|
and grass_ocean_edge != null
|
||||||
|
and grass_beach_transition != null
|
||||||
|
and grass_ocean_corner != null
|
||||||
|
and spawn != null
|
||||||
|
):
|
||||||
|
_check(
|
||||||
|
sand.allows_non_water_neighbor(beach),
|
||||||
|
"Flat sand must permit an adjacent authored beach.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
sand.allows_non_water_neighbor(grass),
|
||||||
|
"Flat sand must permit the inland transition to grass.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass.allows_non_water_neighbor(sand),
|
||||||
|
"Grass must permit the inland side of a flat-sand transition.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
"coast" in sand.required_neighbor_tags,
|
||||||
|
"Every flat-sand chunk must remain attached to an authored beach.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
spawn.minimum_required_neighbors == 3,
|
||||||
|
"Spawn must retain its three-sided safe-grass requirement.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
beach.ocean_facing_edges
|
||||||
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
||||||
|
"The authored beach must descend eastward into ocean before rotation.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_ocean_edge.ocean_facing_edges
|
||||||
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
||||||
|
"The authored grass cliff must face eastward ocean before rotation.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_beach_transition.ocean_facing_edges
|
||||||
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
||||||
|
"The authored grass/beach join must face eastward ocean before rotation.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_beach_transition.minimum_required_neighbors == 2
|
||||||
|
and "coast" in grass_beach_transition.required_neighbor_tags,
|
||||||
|
"The grass/beach join must remain between two coastline chunks.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_beach_transition.allows_non_water_neighbor_on_edge(
|
||||||
|
grass,
|
||||||
|
TerrainChunkTopology.Edge.WEST,
|
||||||
|
)
|
||||||
|
and not grass_beach_transition.allows_non_water_neighbor_on_edge(
|
||||||
|
sand,
|
||||||
|
TerrainChunkTopology.Edge.WEST,
|
||||||
|
),
|
||||||
|
"The grass-backed transition edge must accept grass and reject sand.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_beach_transition.allows_non_water_neighbor_on_edge(
|
||||||
|
beach,
|
||||||
|
TerrainChunkTopology.Edge.NORTH,
|
||||||
|
)
|
||||||
|
and grass_beach_transition.allows_non_water_neighbor_on_edge(
|
||||||
|
grass_ocean_edge,
|
||||||
|
TerrainChunkTopology.Edge.SOUTH,
|
||||||
|
),
|
||||||
|
"The transition's coastline seams must retain their authored roles.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not grass_ocean_edge.allows_non_water_neighbor_on_edge(
|
||||||
|
sand,
|
||||||
|
TerrainChunkTopology.Edge.WEST,
|
||||||
|
),
|
||||||
|
"The straight grass coast's inland edge must reject sand.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_ocean_corner.ocean_facing_edges
|
||||||
|
== (
|
||||||
|
(1 << int(TerrainChunkTopology.Edge.EAST))
|
||||||
|
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
|
||||||
|
),
|
||||||
|
"The authored grass corner must expose east and south to ocean.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_ocean_corner.minimum_required_neighbors == 2
|
||||||
|
and (
|
||||||
|
"grass_ocean_edge"
|
||||||
|
in grass_ocean_corner.required_neighbor_tags
|
||||||
|
),
|
||||||
|
"The grass corner must join two straight grass ocean edges.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
grass_ocean_corner.allows_non_water_neighbor_on_edge(
|
||||||
|
grass_ocean_edge,
|
||||||
|
TerrainChunkTopology.Edge.NORTH,
|
||||||
|
)
|
||||||
|
and grass_ocean_corner.allows_non_water_neighbor_on_edge(
|
||||||
|
grass_ocean_edge,
|
||||||
|
TerrainChunkTopology.Edge.WEST,
|
||||||
|
),
|
||||||
|
"Both corner land seams must accept straight grass ocean edges.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _validate_profiles() -> void:
|
func _validate_profiles() -> void:
|
||||||
|
|
@ -68,10 +211,20 @@ func _validate_profiles() -> void:
|
||||||
variant.edge_profiles.size() == 4,
|
variant.edge_profiles.size() == 4,
|
||||||
"%s must expose four edge profiles." % variant.stable_key(),
|
"%s must expose four edge profiles." % variant.stable_key(),
|
||||||
)
|
)
|
||||||
for profile: TerrainChunkEdgeProfile in variant.edge_profiles:
|
var ocean_edges := variant.rotated_edge_mask(
|
||||||
|
definition.ocean_facing_edges
|
||||||
|
)
|
||||||
|
for edge_index: int in variant.edge_profiles.size():
|
||||||
|
var profile := variant.edge_profiles[edge_index]
|
||||||
_check(
|
_check(
|
||||||
not profile.points.is_empty(),
|
(
|
||||||
"%s has an empty edge profile." % variant.stable_key(),
|
not profile.points.is_empty()
|
||||||
|
or (ocean_edges & (1 << edge_index)) != 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"%s has an empty non-ocean edge profile."
|
||||||
|
% variant.stable_key()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,12 +232,12 @@ func _validate_generation() -> void:
|
||||||
var first := _make_generator()
|
var first := _make_generator()
|
||||||
root.add_child(first)
|
root.add_child(first)
|
||||||
var first_solved := first.generate()
|
var first_solved := first.generate()
|
||||||
_check(first_solved, "Generator must solve the prototype 5x5 grid.")
|
_check(first_solved, "Generator must solve the prototype 7x7 grid.")
|
||||||
if not first_solved:
|
if not first_solved:
|
||||||
first.free()
|
first.free()
|
||||||
return
|
return
|
||||||
var first_keys := first.placement_keys()
|
var first_keys := first.placement_keys()
|
||||||
_check(first_keys.size() == 25, "Generator must place exactly 25 chunks.")
|
_check(first_keys.size() == 49, "Generator must place exactly 49 chunks.")
|
||||||
for stable_id: String in REQUIRED_IDS:
|
for stable_id: String in REQUIRED_IDS:
|
||||||
_check(
|
_check(
|
||||||
_placements_contain(first_keys, stable_id),
|
_placements_contain(first_keys, stable_id),
|
||||||
|
|
@ -99,9 +252,25 @@ func _validate_generation() -> void:
|
||||||
"Generated layouts must contain exactly one spawn chunk.",
|
"Generated layouts must contain exactly one spawn chunk.",
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
first_keys[12].begins_with("chunk_spawn@"),
|
first_keys[24].begins_with("chunk_spawn@"),
|
||||||
"The spawn chunk must occupy the center cell.",
|
"The spawn chunk must occupy the center cell.",
|
||||||
)
|
)
|
||||||
|
_validate_layout_rules(first)
|
||||||
|
_validate_authored_rotations(first)
|
||||||
|
_validate_generation_summary(first)
|
||||||
|
var first_generated_root := first.get_generated_chunks_root()
|
||||||
|
_check(
|
||||||
|
first.generate(),
|
||||||
|
"Repeated generation on one generator must solve.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.placement_keys() == first_keys,
|
||||||
|
"Repeated generation on one generator must remain deterministic.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.get_generated_chunks_root() != first_generated_root,
|
||||||
|
"Successful regeneration must replace the generated scene atomically.",
|
||||||
|
)
|
||||||
|
|
||||||
var second := _make_generator()
|
var second := _make_generator()
|
||||||
root.add_child(second)
|
root.add_child(second)
|
||||||
|
|
@ -115,8 +284,20 @@ func _validate_generation() -> void:
|
||||||
second.placement_keys() == first_keys,
|
second.placement_keys() == first_keys,
|
||||||
"The same seed and catalog must produce the same layout.",
|
"The same seed and catalog must produce the same layout.",
|
||||||
)
|
)
|
||||||
|
var restored := _make_generator()
|
||||||
|
restored.generation_seed = TEST_SEED + 999
|
||||||
|
root.add_child(restored)
|
||||||
|
_check(
|
||||||
|
restored.generate_from_placement_keys(first_keys),
|
||||||
|
"A resolved placement manifest must rebuild successfully.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
restored.placement_keys() == first_keys,
|
||||||
|
"A resolved placement manifest must preserve exact rotations.",
|
||||||
|
)
|
||||||
first.free()
|
first.free()
|
||||||
second.free()
|
second.free()
|
||||||
|
restored.free()
|
||||||
|
|
||||||
for seed_offset: int in range(1, 6):
|
for seed_offset: int in range(1, 6):
|
||||||
var generator := _make_generator()
|
var generator := _make_generator()
|
||||||
|
|
@ -135,18 +316,349 @@ func _validate_generation() -> void:
|
||||||
"Seed %d must contain %s."
|
"Seed %d must contain %s."
|
||||||
% [generator.generation_seed, stable_id],
|
% [generator.generation_seed, stable_id],
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
_all_neighbor_edges_match(generator),
|
_all_neighbor_edges_match(generator),
|
||||||
"Seed %d has a mismatched neighboring edge."
|
"Seed %d has a mismatched neighboring edge."
|
||||||
% generator.generation_seed,
|
% generator.generation_seed,
|
||||||
)
|
)
|
||||||
|
_validate_layout_rules(generator)
|
||||||
generator.free()
|
generator.free()
|
||||||
|
|
||||||
|
var connector_stress := _make_generator()
|
||||||
|
connector_stress.generation_seed = CONNECTOR_STRESS_SEED
|
||||||
|
connector_stress.maximum_backtracks = 100
|
||||||
|
root.add_child(connector_stress)
|
||||||
|
_check(
|
||||||
|
connector_stress.generate(),
|
||||||
|
"Connector propagation must solve the known stress seed.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
connector_stress._backtrack_count < 100,
|
||||||
|
"Connector propagation must reject unsupported branches early.",
|
||||||
|
)
|
||||||
|
connector_stress.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
||||||
|
var counts: Dictionary[StringName, int] = {}
|
||||||
|
for variant: TerrainChunkVariant in generator._variants:
|
||||||
|
var stable_id := variant.definition.stable_id
|
||||||
|
counts[stable_id] = counts.get(stable_id, 0) + 1
|
||||||
|
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
|
||||||
|
_check(
|
||||||
|
counts.get(stable_id, 0) == EXPECTED_VARIANT_COUNTS[stable_id],
|
||||||
|
"%s must preserve every explicitly allowed rotation." % stable_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
var stream := CATALOG.definition_for_id(&"chunk_0003")
|
||||||
|
_check(stream != null, "The stream definition must be available.")
|
||||||
|
if stream == null:
|
||||||
|
return
|
||||||
|
for quarter_turns: int in 4:
|
||||||
|
var stream_variant := _find_variant(
|
||||||
|
generator,
|
||||||
|
&"chunk_0003",
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
stream_variant != null,
|
||||||
|
"The stream must support rotation %d." % quarter_turns,
|
||||||
|
)
|
||||||
|
if stream_variant == null:
|
||||||
|
continue
|
||||||
|
var expected_inlet := 1 << int(TerrainChunkTopology.rotated_edge(
|
||||||
|
TerrainChunkTopology.Edge.NORTH,
|
||||||
|
quarter_turns,
|
||||||
|
))
|
||||||
|
var expected_outlet := 1 << int(TerrainChunkTopology.rotated_edge(
|
||||||
|
TerrainChunkTopology.Edge.SOUTH,
|
||||||
|
quarter_turns,
|
||||||
|
))
|
||||||
|
_check(
|
||||||
|
stream_variant.rotated_edge_mask(stream.water_inlet_edges)
|
||||||
|
== expected_inlet,
|
||||||
|
"The rotated stream inlet must follow its authored orientation.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
stream_variant.rotated_edge_mask(stream.water_outlet_edges)
|
||||||
|
== expected_outlet,
|
||||||
|
"The rotated stream outlet must follow its authored orientation.",
|
||||||
|
)
|
||||||
|
|
||||||
|
var beach := CATALOG.definition_for_id(&"chunk_0002")
|
||||||
|
_check(beach != null, "The beach definition must be available.")
|
||||||
|
if beach == null:
|
||||||
|
return
|
||||||
|
for quarter_turns: int in 4:
|
||||||
|
var beach_variant := _find_variant(
|
||||||
|
generator,
|
||||||
|
&"chunk_0002",
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
beach_variant != null,
|
||||||
|
"The beach must support rotation %d." % quarter_turns,
|
||||||
|
)
|
||||||
|
if beach_variant == null:
|
||||||
|
continue
|
||||||
|
var expected_ocean_edge := 1 << int(
|
||||||
|
TerrainChunkTopology.rotated_edge(
|
||||||
|
TerrainChunkTopology.Edge.EAST,
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
beach_variant.rotated_edge_mask(beach.ocean_facing_edges)
|
||||||
|
== expected_ocean_edge,
|
||||||
|
"The rotated beach must keep its low edge facing the ocean.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not generator._variant_respects_ocean_boundary(
|
||||||
|
beach_variant,
|
||||||
|
Vector2i(3, 3),
|
||||||
|
),
|
||||||
|
"A beach slope must never direct its low edge into the map.",
|
||||||
|
)
|
||||||
|
var boundary_coordinate := Vector2i(3, 3)
|
||||||
|
match TerrainChunkTopology.rotated_edge(
|
||||||
|
TerrainChunkTopology.Edge.EAST,
|
||||||
|
quarter_turns,
|
||||||
|
):
|
||||||
|
TerrainChunkTopology.Edge.NORTH:
|
||||||
|
boundary_coordinate.y = 0
|
||||||
|
TerrainChunkTopology.Edge.EAST:
|
||||||
|
boundary_coordinate.x = generator.grid_size.x - 1
|
||||||
|
TerrainChunkTopology.Edge.SOUTH:
|
||||||
|
boundary_coordinate.y = generator.grid_size.y - 1
|
||||||
|
TerrainChunkTopology.Edge.WEST:
|
||||||
|
boundary_coordinate.x = 0
|
||||||
|
_check(
|
||||||
|
generator._variant_respects_ocean_boundary(
|
||||||
|
beach_variant,
|
||||||
|
boundary_coordinate,
|
||||||
|
),
|
||||||
|
"A beach slope must be legal when its low edge faces open ocean.",
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
grass_ocean_edge != null
|
||||||
|
and grass_beach_transition != null
|
||||||
|
and grass_ocean_corner != null
|
||||||
|
),
|
||||||
|
"The authored coastline additions must be available.",
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
grass_ocean_edge == null
|
||||||
|
or grass_beach_transition == null
|
||||||
|
or grass_ocean_corner == null
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var beach_zero := _find_variant(generator, &"chunk_0002", 0)
|
||||||
|
var grass_edge_zero := _find_variant(generator, &"chunk_0005", 0)
|
||||||
|
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)
|
||||||
|
_check(
|
||||||
|
beach_zero != null
|
||||||
|
and grass_edge_zero != null
|
||||||
|
and grass_edge_south != null
|
||||||
|
and transition_zero != null
|
||||||
|
and corner_zero != null,
|
||||||
|
"The unrotated coastline variants must be available.",
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
beach_zero != null
|
||||||
|
and grass_edge_zero != null
|
||||||
|
and grass_edge_south != null
|
||||||
|
and transition_zero != null
|
||||||
|
and corner_zero != null
|
||||||
|
):
|
||||||
|
_check(
|
||||||
|
grass_edge_zero.profile(
|
||||||
|
TerrainChunkTopology.Edge.EAST
|
||||||
|
).points.is_empty(),
|
||||||
|
"The grass cliff's authored east edge must remain open to ocean.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
transition_zero.profile(
|
||||||
|
TerrainChunkTopology.Edge.NORTH
|
||||||
|
).matches(
|
||||||
|
beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
|
||||||
|
generator.edge_match_tolerance,
|
||||||
|
),
|
||||||
|
"The transition's north side must match the authored beach.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
transition_zero.profile(
|
||||||
|
TerrainChunkTopology.Edge.SOUTH
|
||||||
|
).matches(
|
||||||
|
grass_edge_zero.profile(TerrainChunkTopology.Edge.NORTH),
|
||||||
|
generator.edge_match_tolerance,
|
||||||
|
),
|
||||||
|
"The transition's south side must match the grass cliff.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
corner_zero.profile(TerrainChunkTopology.Edge.EAST).points.is_empty()
|
||||||
|
and corner_zero.profile(
|
||||||
|
TerrainChunkTopology.Edge.SOUTH
|
||||||
|
).points.is_empty(),
|
||||||
|
"The corner's authored east and south edges must remain open to ocean.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
|
||||||
|
grass_edge_zero.profile(TerrainChunkTopology.Edge.SOUTH),
|
||||||
|
generator.edge_match_tolerance,
|
||||||
|
),
|
||||||
|
"The corner's north side must join a straight grass ocean edge.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
corner_zero.profile(TerrainChunkTopology.Edge.WEST).matches(
|
||||||
|
grass_edge_south.profile(TerrainChunkTopology.Edge.EAST),
|
||||||
|
generator.edge_match_tolerance,
|
||||||
|
),
|
||||||
|
"The corner's west side must join a straight grass ocean edge.",
|
||||||
|
)
|
||||||
|
var flat_grass := _find_variant(generator, &"chunk_0000", 0)
|
||||||
|
var flat_sand := _find_variant(generator, &"chunk_0001", 0)
|
||||||
|
_check(
|
||||||
|
flat_grass != null and flat_sand != null,
|
||||||
|
"Flat grass and sand variants must be available for seam validation.",
|
||||||
|
)
|
||||||
|
if flat_grass != null and flat_sand != null:
|
||||||
|
for quarter_turns: int in 4:
|
||||||
|
var transition := _find_variant(
|
||||||
|
generator,
|
||||||
|
&"chunk_0006",
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
if transition == null:
|
||||||
|
continue
|
||||||
|
var inland_edge := TerrainChunkTopology.rotated_edge(
|
||||||
|
TerrainChunkTopology.Edge.WEST,
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
var neighbor_edge := TerrainChunkTopology.opposite_edge(inland_edge)
|
||||||
|
_check(
|
||||||
|
generator._edges_are_compatible(
|
||||||
|
transition,
|
||||||
|
inland_edge,
|
||||||
|
flat_grass,
|
||||||
|
neighbor_edge,
|
||||||
|
),
|
||||||
|
"Every transition rotation must accept grass behind it.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not generator._edges_are_compatible(
|
||||||
|
transition,
|
||||||
|
inland_edge,
|
||||||
|
flat_sand,
|
||||||
|
neighbor_edge,
|
||||||
|
),
|
||||||
|
"No transition rotation may accept sand behind it.",
|
||||||
|
)
|
||||||
|
for stable_id: StringName in [&"chunk_0005", &"chunk_0006", &"chunk_0007"]:
|
||||||
|
for quarter_turns: int in 4:
|
||||||
|
var coast_variant := _find_variant(
|
||||||
|
generator,
|
||||||
|
stable_id,
|
||||||
|
quarter_turns,
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
coast_variant != null,
|
||||||
|
"%s must support rotation %d." % [stable_id, quarter_turns],
|
||||||
|
)
|
||||||
|
if coast_variant == null:
|
||||||
|
continue
|
||||||
|
_check(
|
||||||
|
not generator._variant_respects_ocean_boundary(
|
||||||
|
coast_variant,
|
||||||
|
Vector2i(3, 3),
|
||||||
|
),
|
||||||
|
"%s must never place its ocean edge inside the map." % stable_id,
|
||||||
|
)
|
||||||
|
if stable_id != &"chunk_0007":
|
||||||
|
continue
|
||||||
|
var boundary_coordinate := Vector2i(3, 3)
|
||||||
|
var ocean_edges := coast_variant.rotated_edge_mask(
|
||||||
|
grass_ocean_corner.ocean_facing_edges
|
||||||
|
)
|
||||||
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
if (ocean_edges & (1 << edge_value)) == 0:
|
||||||
|
continue
|
||||||
|
match edge_value as TerrainChunkTopology.Edge:
|
||||||
|
TerrainChunkTopology.Edge.NORTH:
|
||||||
|
boundary_coordinate.y = 0
|
||||||
|
TerrainChunkTopology.Edge.EAST:
|
||||||
|
boundary_coordinate.x = generator.grid_size.x - 1
|
||||||
|
TerrainChunkTopology.Edge.SOUTH:
|
||||||
|
boundary_coordinate.y = generator.grid_size.y - 1
|
||||||
|
TerrainChunkTopology.Edge.WEST:
|
||||||
|
boundary_coordinate.x = 0
|
||||||
|
_check(
|
||||||
|
generator._variant_respects_ocean_boundary(
|
||||||
|
coast_variant,
|
||||||
|
boundary_coordinate,
|
||||||
|
),
|
||||||
|
"Each grass 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.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(summary.get("solver_variant_count", 0))
|
||||||
|
== EXPECTED_SOLVER_VARIANT_COUNT,
|
||||||
|
"Equivalent visual rotations must share one solver constraint.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(summary.get("adjacent_repeat_edges", -1))
|
||||||
|
== generator._count_adjacent_repeat_edges(),
|
||||||
|
"The summary must report adjacent repeated chunk definitions.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
str(summary.get("layout_fingerprint", ""))
|
||||||
|
== generator.placement_fingerprint(),
|
||||||
|
"The summary must identify the exact resolved layout.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
generator.placement_fingerprint().length() == 64,
|
||||||
|
"The resolved layout fingerprint must be SHA-256.",
|
||||||
|
)
|
||||||
|
var variant_counts: Dictionary = summary.get("variant_counts", {})
|
||||||
|
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
|
||||||
|
_check(
|
||||||
|
int(variant_counts.get(stable_id, 0))
|
||||||
|
== EXPECTED_VARIANT_COUNTS[stable_id],
|
||||||
|
"The summary must report %s rotation candidates." % stable_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_variant(
|
||||||
|
generator: TerrainChunkGenerator,
|
||||||
|
stable_id: StringName,
|
||||||
|
quarter_turns: int,
|
||||||
|
) -> TerrainChunkVariant:
|
||||||
|
for variant: TerrainChunkVariant in generator._variants:
|
||||||
|
if (
|
||||||
|
variant.definition.stable_id == stable_id
|
||||||
|
and variant.quarter_turns == quarter_turns
|
||||||
|
):
|
||||||
|
return variant
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func _make_generator() -> TerrainChunkGenerator:
|
func _make_generator() -> TerrainChunkGenerator:
|
||||||
var generator := TerrainChunkGenerator.new()
|
var generator := TerrainChunkGenerator.new()
|
||||||
generator.catalog = CATALOG
|
generator.catalog = CATALOG
|
||||||
generator.grid_size = Vector2i(5, 5)
|
generator.grid_size = Vector2i(7, 7)
|
||||||
generator.generation_seed = TEST_SEED
|
generator.generation_seed = TEST_SEED
|
||||||
generator.generate_on_ready = false
|
generator.generate_on_ready = false
|
||||||
generator.build_collision = false
|
generator.build_collision = false
|
||||||
|
|
@ -201,6 +713,128 @@ func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
||||||
|
_check(
|
||||||
|
generator._neighbor_requirement_validation_error(
|
||||||
|
generator._placements
|
||||||
|
).is_empty(),
|
||||||
|
"Every generated chunk must satisfy its authored neighbor count.",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
generator._walkable_connectivity_validation_error(
|
||||||
|
generator._placements
|
||||||
|
).is_empty(),
|
||||||
|
"All walkable generated terrain must remain connected to spawn.",
|
||||||
|
)
|
||||||
|
var center := Vector2i(
|
||||||
|
generator.grid_size.x / 2,
|
||||||
|
generator.grid_size.y / 2,
|
||||||
|
)
|
||||||
|
var safe_spawn_neighbors := 0
|
||||||
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
var neighbor_coordinate := (
|
||||||
|
center
|
||||||
|
+ TerrainChunkTopology.grid_offset(
|
||||||
|
edge_value as TerrainChunkTopology.Edge
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var neighbor := generator._placements[
|
||||||
|
neighbor_coordinate.y * generator.grid_size.x
|
||||||
|
+ neighbor_coordinate.x
|
||||||
|
]
|
||||||
|
if "spawn_safe" in neighbor.definition.tags:
|
||||||
|
safe_spawn_neighbors += 1
|
||||||
|
_check(
|
||||||
|
safe_spawn_neighbors >= 3,
|
||||||
|
"At least three cardinal spawn neighbors must be safe flat grass.",
|
||||||
|
)
|
||||||
|
for index: int in generator._placements.size():
|
||||||
|
var placement := generator._placements[index]
|
||||||
|
var coordinate := Vector2i(
|
||||||
|
index % generator.grid_size.x,
|
||||||
|
index / generator.grid_size.x,
|
||||||
|
)
|
||||||
|
if "coast" in placement.definition.tags:
|
||||||
|
_check(
|
||||||
|
generator._variant_respects_ocean_boundary(
|
||||||
|
placement,
|
||||||
|
coordinate,
|
||||||
|
),
|
||||||
|
"Every coastline chunk must direct its ocean edge outside the grid.",
|
||||||
|
)
|
||||||
|
if placement.definition.stable_id == &"chunk_0006":
|
||||||
|
var touches_beach := false
|
||||||
|
var touches_grass_edge := false
|
||||||
|
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
|
||||||
|
]
|
||||||
|
touches_beach = (
|
||||||
|
touches_beach
|
||||||
|
or neighbor.definition.stable_id == &"chunk_0002"
|
||||||
|
)
|
||||||
|
touches_grass_edge = (
|
||||||
|
touches_grass_edge
|
||||||
|
or neighbor.definition.stable_id == &"chunk_0005"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
touches_beach and touches_grass_edge,
|
||||||
|
"Every grass/beach transition must join both authored edge types.",
|
||||||
|
)
|
||||||
|
if placement.definition.stable_id == &"chunk_0007":
|
||||||
|
var straight_edge_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_0005":
|
||||||
|
straight_edge_neighbors += 1
|
||||||
|
_check(
|
||||||
|
straight_edge_neighbors == 2,
|
||||||
|
"Every grass corner must join two straight grass ocean edges.",
|
||||||
|
)
|
||||||
|
if placement.definition.stable_id != &"chunk_0001":
|
||||||
|
continue
|
||||||
|
var touches_coast := false
|
||||||
|
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 "coast" in neighbor.definition.tags:
|
||||||
|
touches_coast = true
|
||||||
|
_check(
|
||||||
|
touches_coast,
|
||||||
|
"Every flat-sand chunk must remain directly attached to a beach.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _check(condition: bool, message: String) -> void:
|
func _check(condition: bool, message: String) -> void:
|
||||||
if not condition:
|
if not condition:
|
||||||
_failures.append(message)
|
_failures.append(message)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ Run through Blender rather than a standalone Python interpreter:
|
||||||
|
|
||||||
Collections use ``chunk_####_description`` and contain one primary terrain
|
Collections use ``chunk_####_description`` and contain one primary terrain
|
||||||
mesh named ``chunk_####``. Additional production objects may live in the same
|
mesh named ``chunk_####``. Additional production objects may live in the same
|
||||||
collection; unrelated collections are ignored.
|
collection. Reusable procedural props use individual ``prop_description``
|
||||||
|
mesh objects. They may be arranged anywhere in the source file because each
|
||||||
|
object's authored origin becomes the exported runtime anchor. A same-named
|
||||||
|
``prop_description`` collection remains supported for multi-object props.
|
||||||
|
Unrelated objects and collections are ignored.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -30,10 +34,14 @@ from mathutils import Vector
|
||||||
|
|
||||||
|
|
||||||
CHUNK_SIZE_METERS = 10.0
|
CHUNK_SIZE_METERS = 10.0
|
||||||
DEFAULT_TOLERANCE = 0.001
|
# Match the runtime boundary analyzer's five-millimeter authoring tolerance.
|
||||||
|
DEFAULT_TOLERANCE = 0.005
|
||||||
COLLECTION_PATTERN = re.compile(
|
COLLECTION_PATTERN = re.compile(
|
||||||
r"^chunk_(?P<number>\d{4})(?:_(?P<label>[a-z0-9]+(?:_[a-z0-9]+)*))?$"
|
r"^chunk_(?P<number>\d{4})(?:_(?P<label>[a-z0-9]+(?:_[a-z0-9]+)*))?$"
|
||||||
)
|
)
|
||||||
|
PROP_COLLECTION_PATTERN = re.compile(
|
||||||
|
r"^prop_(?P<label>[a-z0-9]+(?:_[a-z0-9]+)*)$"
|
||||||
|
)
|
||||||
ALLOWED_OBJECT_TYPES = {"EMPTY", "MESH"}
|
ALLOWED_OBJECT_TYPES = {"EMPTY", "MESH"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,6 +79,22 @@ class ChunkSource:
|
||||||
bounds: Bounds
|
bounds: Bounds
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PropSource:
|
||||||
|
stable_id: str
|
||||||
|
label: str
|
||||||
|
source_kind: str
|
||||||
|
source_name: str
|
||||||
|
collection: bpy.types.Collection
|
||||||
|
primary_mesh: bpy.types.Object
|
||||||
|
objects: tuple[bpy.types.Object, ...]
|
||||||
|
bounds: Bounds
|
||||||
|
anchor_world: tuple[float, float, float]
|
||||||
|
|
||||||
|
|
||||||
|
ExportSource = ChunkSource | PropSource
|
||||||
|
|
||||||
|
|
||||||
def _parse_arguments() -> argparse.Namespace:
|
def _parse_arguments() -> argparse.Namespace:
|
||||||
script_arguments: list[str] = []
|
script_arguments: list[str] = []
|
||||||
if "--" in sys.argv:
|
if "--" in sys.argv:
|
||||||
|
|
@ -82,7 +106,10 @@ def _parse_arguments() -> argparse.Namespace:
|
||||||
"--output",
|
"--output",
|
||||||
type=Path,
|
type=Path,
|
||||||
required=True,
|
required=True,
|
||||||
help="Directory that will receive chunk_####.glb and chunks.json.",
|
help=(
|
||||||
|
"Directory that will receive chunk_####.glb, prop_*.glb, and "
|
||||||
|
"chunks.json."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--tolerance",
|
"--tolerance",
|
||||||
|
|
@ -140,18 +167,47 @@ def _validate_transform(
|
||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def _validate_bounds(bounds: Bounds, tolerance: float) -> list[str]:
|
def _validate_bounds(
|
||||||
|
bounds: Bounds,
|
||||||
|
tolerance: float,
|
||||||
|
allow_open_east_edge: bool = False,
|
||||||
|
allow_open_south_edge: bool = False,
|
||||||
|
) -> list[str]:
|
||||||
problems: list[str] = []
|
problems: list[str] = []
|
||||||
expected_half_size = CHUNK_SIZE_METERS * 0.5
|
expected_half_size = CHUNK_SIZE_METERS * 0.5
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"minimum X": (bounds.minimum[0], -expected_half_size),
|
"minimum X": (bounds.minimum[0], -expected_half_size),
|
||||||
"maximum X": (bounds.maximum[0], expected_half_size),
|
|
||||||
"minimum Y": (bounds.minimum[1], -expected_half_size),
|
|
||||||
"maximum Y": (bounds.maximum[1], expected_half_size),
|
"maximum Y": (bounds.maximum[1], expected_half_size),
|
||||||
}
|
}
|
||||||
|
if not allow_open_east_edge:
|
||||||
|
expected_values["maximum X"] = (
|
||||||
|
bounds.maximum[0],
|
||||||
|
expected_half_size,
|
||||||
|
)
|
||||||
|
if not allow_open_south_edge:
|
||||||
|
expected_values["minimum Y"] = (
|
||||||
|
bounds.minimum[1],
|
||||||
|
-expected_half_size,
|
||||||
|
)
|
||||||
for label, (actual, expected) in expected_values.items():
|
for label, (actual, expected) in expected_values.items():
|
||||||
if not _close_enough(actual, expected, tolerance):
|
if not _close_enough(actual, expected, tolerance):
|
||||||
problems.append(f"{label} is {actual:.6f}, expected {expected:.6f}")
|
problems.append(f"{label} is {actual:.6f}, expected {expected:.6f}")
|
||||||
|
if (
|
||||||
|
allow_open_east_edge
|
||||||
|
and bounds.maximum[0] > expected_half_size + tolerance
|
||||||
|
):
|
||||||
|
problems.append(
|
||||||
|
f"maximum X is {bounds.maximum[0]:.6f}, expected no greater than "
|
||||||
|
f"{expected_half_size:.6f}"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
allow_open_south_edge
|
||||||
|
and bounds.minimum[1] < -expected_half_size - tolerance
|
||||||
|
):
|
||||||
|
problems.append(
|
||||||
|
f"minimum Y is {bounds.minimum[1]:.6f}, expected no less than "
|
||||||
|
f"{-expected_half_size:.6f}"
|
||||||
|
)
|
||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -223,7 +279,19 @@ def _discover_chunks(tolerance: float) -> list[ChunkSource]:
|
||||||
)
|
)
|
||||||
|
|
||||||
bounds = _world_bounds(primary_mesh)
|
bounds = _world_bounds(primary_mesh)
|
||||||
bound_problems = _validate_bounds(bounds, tolerance)
|
# 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
|
||||||
|
# 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
|
||||||
|
bound_problems = _validate_bounds(
|
||||||
|
bounds,
|
||||||
|
tolerance,
|
||||||
|
allow_open_east_edge=allow_open_east_edge,
|
||||||
|
allow_open_south_edge=allow_open_south_edge,
|
||||||
|
)
|
||||||
errors.extend(
|
errors.extend(
|
||||||
f"{collection.name}/{primary_mesh.name}: {problem}"
|
f"{collection.name}/{primary_mesh.name}: {problem}"
|
||||||
for problem in bound_problems
|
for problem in bound_problems
|
||||||
|
|
@ -261,58 +329,309 @@ def _discover_chunks(tolerance: float) -> list[ChunkSource]:
|
||||||
return sorted(chunks, key=lambda item: item.number)
|
return sorted(chunks, key=lambda item: item.number)
|
||||||
|
|
||||||
|
|
||||||
def _select_chunk(chunk: ChunkSource) -> None:
|
def _validate_prop_bounds(bounds: Bounds, tolerance: float) -> list[str]:
|
||||||
|
problems: list[str] = []
|
||||||
|
for axis, size in zip("XYZ", bounds.size):
|
||||||
|
if size <= tolerance:
|
||||||
|
problems.append(f"bounds size {axis} is {size:.6f}, expected positive")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def _rebased_prop_bounds(
|
||||||
|
mesh_objects: list[bpy.types.Object],
|
||||||
|
anchor_world: Vector,
|
||||||
|
) -> Bounds:
|
||||||
|
corners: list[Vector] = []
|
||||||
|
dependency_graph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
for mesh_object in mesh_objects:
|
||||||
|
evaluated_object = mesh_object.evaluated_get(dependency_graph)
|
||||||
|
corners.extend(
|
||||||
|
evaluated_object.matrix_world @ Vector(corner) - anchor_world
|
||||||
|
for corner in evaluated_object.bound_box
|
||||||
|
)
|
||||||
|
minimum = tuple(
|
||||||
|
min(corner[axis] for corner in corners) for axis in range(3)
|
||||||
|
)
|
||||||
|
maximum = tuple(
|
||||||
|
max(corner[axis] for corner in corners) for axis in range(3)
|
||||||
|
)
|
||||||
|
return Bounds(minimum=minimum, maximum=maximum)
|
||||||
|
|
||||||
|
|
||||||
|
def _prop_transform_problems(
|
||||||
|
source_objects: tuple[bpy.types.Object, ...],
|
||||||
|
tolerance: float,
|
||||||
|
) -> list[str]:
|
||||||
|
problems: list[str] = []
|
||||||
|
source_set = set(source_objects)
|
||||||
|
for item in source_objects:
|
||||||
|
if item.parent is not None and item.parent not in source_set:
|
||||||
|
problems.append(
|
||||||
|
f"{item.name} is parented outside its exported prop source"
|
||||||
|
)
|
||||||
|
for label, values in (
|
||||||
|
("location", item.location),
|
||||||
|
("rotation", item.rotation_euler),
|
||||||
|
("scale", item.scale),
|
||||||
|
):
|
||||||
|
if not all(math.isfinite(value) for value in values):
|
||||||
|
problems.append(f"{item.name} has a non-finite {label}")
|
||||||
|
for axis, value in zip("XYZ", item.scale):
|
||||||
|
if abs(value) <= tolerance:
|
||||||
|
problems.append(
|
||||||
|
f"{item.name} scale {axis} is {value:.6f}, expected nonzero"
|
||||||
|
)
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def _make_prop_source(
|
||||||
|
stable_id: str,
|
||||||
|
label: str,
|
||||||
|
source_kind: str,
|
||||||
|
source_name: str,
|
||||||
|
collection: bpy.types.Collection,
|
||||||
|
objects: tuple[bpy.types.Object, ...],
|
||||||
|
tolerance: float,
|
||||||
|
) -> tuple[PropSource | None, list[str]]:
|
||||||
|
errors: list[str] = []
|
||||||
|
invalid_objects = [
|
||||||
|
item for item in objects if item.type not in ALLOWED_OBJECT_TYPES
|
||||||
|
]
|
||||||
|
if invalid_objects:
|
||||||
|
details = ", ".join(
|
||||||
|
f"{item.name} ({item.type})" for item in invalid_objects
|
||||||
|
)
|
||||||
|
errors.append(f"{source_name}: contains unsupported objects: {details}")
|
||||||
|
|
||||||
|
mesh_objects = [item for item in objects if item.type == "MESH"]
|
||||||
|
named_meshes = [item for item in mesh_objects if item.name == stable_id]
|
||||||
|
if len(named_meshes) == 1:
|
||||||
|
primary_mesh = named_meshes[0]
|
||||||
|
elif len(mesh_objects) == 1:
|
||||||
|
primary_mesh = mesh_objects[0]
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
f"{source_name}: expected one mesh (preferably named "
|
||||||
|
f"{stable_id}), found {len(mesh_objects)}"
|
||||||
|
)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
errors.extend(
|
||||||
|
f"{source_name}: {problem}"
|
||||||
|
for problem in _prop_transform_problems(objects, tolerance)
|
||||||
|
)
|
||||||
|
anchor_vector = primary_mesh.matrix_world.translation.copy()
|
||||||
|
bounds = _rebased_prop_bounds(mesh_objects, anchor_vector)
|
||||||
|
errors.extend(
|
||||||
|
f"{source_name}/{primary_mesh.name}: {problem}"
|
||||||
|
for problem in _validate_prop_bounds(bounds, tolerance)
|
||||||
|
)
|
||||||
|
for mesh_object in mesh_objects:
|
||||||
|
if len(mesh_object.data.vertices) < 3:
|
||||||
|
errors.append(
|
||||||
|
f"{source_name}/{mesh_object.name}: has fewer than three vertices"
|
||||||
|
)
|
||||||
|
if len(mesh_object.data.materials) == 0:
|
||||||
|
errors.append(
|
||||||
|
f"{source_name}/{mesh_object.name}: has no material"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
PropSource(
|
||||||
|
stable_id=stable_id,
|
||||||
|
label=label,
|
||||||
|
source_kind=source_kind,
|
||||||
|
source_name=source_name,
|
||||||
|
collection=collection,
|
||||||
|
primary_mesh=primary_mesh,
|
||||||
|
objects=objects,
|
||||||
|
bounds=bounds,
|
||||||
|
anchor_world=tuple(anchor_vector),
|
||||||
|
),
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_props(tolerance: float) -> list[PropSource]:
|
||||||
|
props: list[PropSource] = []
|
||||||
|
errors: list[str] = []
|
||||||
|
claimed_objects: dict[int, str] = {}
|
||||||
|
claimed_ids: dict[str, str] = {}
|
||||||
|
|
||||||
|
for collection in sorted(bpy.data.collections, key=lambda item: item.name):
|
||||||
|
match = PROP_COLLECTION_PATTERN.fullmatch(collection.name)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
stable_id = collection.name
|
||||||
|
label = match.group("label")
|
||||||
|
objects = tuple(sorted(collection.all_objects, key=lambda item: item.name))
|
||||||
|
if not objects:
|
||||||
|
errors.append(f"{collection.name}: collection is empty")
|
||||||
|
continue
|
||||||
|
for item in objects:
|
||||||
|
object_key = item.as_pointer()
|
||||||
|
previous_source = claimed_objects.get(object_key)
|
||||||
|
if previous_source is not None:
|
||||||
|
errors.append(
|
||||||
|
f"{collection.name}: object {item.name} is also exported by "
|
||||||
|
f"{previous_source}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
claimed_objects[object_key] = collection.name
|
||||||
|
prop, source_errors = _make_prop_source(
|
||||||
|
stable_id,
|
||||||
|
label,
|
||||||
|
"collection",
|
||||||
|
collection.name,
|
||||||
|
collection,
|
||||||
|
objects,
|
||||||
|
tolerance,
|
||||||
|
)
|
||||||
|
errors.extend(source_errors)
|
||||||
|
if prop is not None:
|
||||||
|
props.append(prop)
|
||||||
|
claimed_ids[stable_id] = collection.name
|
||||||
|
|
||||||
|
for item in sorted(bpy.data.objects, key=lambda value: value.name):
|
||||||
|
match = PROP_COLLECTION_PATTERN.fullmatch(item.name)
|
||||||
|
if match is None or item.as_pointer() in claimed_objects:
|
||||||
|
continue
|
||||||
|
stable_id = item.name
|
||||||
|
if stable_id in claimed_ids:
|
||||||
|
errors.append(
|
||||||
|
f"{item.name}: duplicates prop ID already used by "
|
||||||
|
f"{claimed_ids[stable_id]}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if item.type != "MESH":
|
||||||
|
errors.append(f"{item.name}: prop object must be a mesh")
|
||||||
|
continue
|
||||||
|
collections = sorted(item.users_collection, key=lambda value: value.name)
|
||||||
|
if not collections:
|
||||||
|
errors.append(f"{item.name}: prop object belongs to no collection")
|
||||||
|
continue
|
||||||
|
prop, source_errors = _make_prop_source(
|
||||||
|
stable_id,
|
||||||
|
match.group("label"),
|
||||||
|
"object",
|
||||||
|
item.name,
|
||||||
|
collections[0],
|
||||||
|
(item,),
|
||||||
|
tolerance,
|
||||||
|
)
|
||||||
|
errors.extend(source_errors)
|
||||||
|
if prop is not None:
|
||||||
|
props.append(prop)
|
||||||
|
claimed_ids[stable_id] = item.name
|
||||||
|
claimed_objects[item.as_pointer()] = item.name
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise ExportValidationError("\n".join(errors))
|
||||||
|
return sorted(props, key=lambda item: item.stable_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_distinct_source_membership(
|
||||||
|
chunks: list[ChunkSource],
|
||||||
|
props: list[PropSource],
|
||||||
|
) -> None:
|
||||||
|
claimed_objects: dict[int, str] = {}
|
||||||
|
errors: list[str] = []
|
||||||
|
for source in [*chunks, *props]:
|
||||||
|
source_name = (
|
||||||
|
source.collection.name
|
||||||
|
if isinstance(source, ChunkSource)
|
||||||
|
else source.source_name
|
||||||
|
)
|
||||||
|
for item in source.objects:
|
||||||
|
object_key = item.as_pointer()
|
||||||
|
previous_source = claimed_objects.get(object_key)
|
||||||
|
if previous_source is not None:
|
||||||
|
errors.append(
|
||||||
|
f"{source_name}: object {item.name} is also exported by "
|
||||||
|
f"{previous_source}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
claimed_objects[object_key] = source_name
|
||||||
|
if errors:
|
||||||
|
raise ExportValidationError("\n".join(errors))
|
||||||
|
|
||||||
|
|
||||||
|
def _select_source(source: ExportSource) -> None:
|
||||||
if bpy.context.object is not None and bpy.context.object.mode != "OBJECT":
|
if bpy.context.object is not None and bpy.context.object.mode != "OBJECT":
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
# Blender's selection operator can miss objects after a glTF export has
|
# Blender's selection operator can miss objects after a glTF export has
|
||||||
# temporarily changed visibility/context. Set every object explicitly so
|
# temporarily changed visibility/context. Set every object explicitly so
|
||||||
# one chunk can never leak into a later chunk's selected-only export.
|
# one chunk can never leak into a later chunk's selected-only export.
|
||||||
selected_objects = set(chunk.objects)
|
selected_objects = set(source.objects)
|
||||||
for item in bpy.context.view_layer.objects:
|
for item in bpy.context.view_layer.objects:
|
||||||
item.select_set(item in selected_objects)
|
item.select_set(item in selected_objects)
|
||||||
for item in chunk.objects:
|
for item in source.objects:
|
||||||
item.hide_select = False
|
item.hide_select = False
|
||||||
item.hide_viewport = False
|
item.hide_viewport = False
|
||||||
item.hide_set(False)
|
item.hide_set(False)
|
||||||
item.select_set(True)
|
item.select_set(True)
|
||||||
bpy.context.view_layer.objects.active = chunk.primary_mesh
|
bpy.context.view_layer.objects.active = source.primary_mesh
|
||||||
|
|
||||||
|
|
||||||
def _export_chunk(chunk: ChunkSource, output_path: Path) -> None:
|
def _export_source(source: ExportSource, output_path: Path) -> None:
|
||||||
_select_chunk(chunk)
|
_select_source(source)
|
||||||
result = bpy.ops.export_scene.gltf(
|
saved_root_matrices: dict[bpy.types.Object, object] = {}
|
||||||
filepath=str(output_path),
|
if isinstance(source, PropSource):
|
||||||
check_existing=False,
|
source_objects = set(source.objects)
|
||||||
export_format="GLB",
|
anchor = Vector(source.anchor_world)
|
||||||
use_selection=True,
|
for item in source.objects:
|
||||||
export_apply=True,
|
if item.parent in source_objects:
|
||||||
export_animations=False,
|
continue
|
||||||
export_cameras=False,
|
saved_root_matrices[item] = item.matrix_world.copy()
|
||||||
export_lights=False,
|
rebased_matrix = item.matrix_world.copy()
|
||||||
export_materials="EXPORT",
|
rebased_matrix.translation -= anchor
|
||||||
export_morph=False,
|
item.matrix_world = rebased_matrix
|
||||||
export_normals=True,
|
bpy.context.view_layer.update()
|
||||||
export_skins=False,
|
|
||||||
export_tangents=False,
|
try:
|
||||||
export_texcoords=True,
|
result = bpy.ops.export_scene.gltf(
|
||||||
export_yup=True,
|
filepath=str(output_path),
|
||||||
export_extras=True,
|
check_existing=False,
|
||||||
will_save_settings=False,
|
export_format="GLB",
|
||||||
)
|
use_selection=True,
|
||||||
if result != {"FINISHED"}:
|
export_apply=True,
|
||||||
raise RuntimeError(
|
export_animations=False,
|
||||||
f"Blender failed to export {chunk.collection.name}: {result}"
|
export_cameras=False,
|
||||||
|
export_lights=False,
|
||||||
|
export_materials="EXPORT",
|
||||||
|
export_morph=False,
|
||||||
|
export_normals=True,
|
||||||
|
export_skins=False,
|
||||||
|
export_tangents=False,
|
||||||
|
export_texcoords=True,
|
||||||
|
export_yup=True,
|
||||||
|
export_extras=True,
|
||||||
|
will_save_settings=False,
|
||||||
)
|
)
|
||||||
_validate_exported_object_membership(chunk, output_path)
|
finally:
|
||||||
|
for item, matrix in saved_root_matrices.items():
|
||||||
|
item.matrix_world = matrix
|
||||||
|
if saved_root_matrices:
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
if result != {"FINISHED"}:
|
||||||
|
source_name = (
|
||||||
|
source.collection.name
|
||||||
|
if isinstance(source, ChunkSource)
|
||||||
|
else source.source_name
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Blender failed to export {source_name}: {result}"
|
||||||
|
)
|
||||||
|
_validate_exported_object_membership(source, output_path)
|
||||||
|
|
||||||
|
|
||||||
def _validate_exported_object_membership(
|
def _validate_exported_object_membership(
|
||||||
chunk: ChunkSource,
|
source: ExportSource,
|
||||||
output_path: Path,
|
output_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Confirm selected-only export did not leak objects from other chunks."""
|
"""Confirm selected-only export contains exactly the intended objects."""
|
||||||
with output_path.open("rb") as source:
|
with output_path.open("rb") as binary_file:
|
||||||
header = source.read(20)
|
header = binary_file.read(20)
|
||||||
if len(header) != 20 or header[:4] != b"glTF":
|
if len(header) != 20 or header[:4] != b"glTF":
|
||||||
raise RuntimeError(f"{output_path.name} is not a valid GLB file")
|
raise RuntimeError(f"{output_path.name} is not a valid GLB file")
|
||||||
json_length, json_kind = struct.unpack_from("<II", header, 12)
|
json_length, json_kind = struct.unpack_from("<II", header, 12)
|
||||||
|
|
@ -320,13 +639,13 @@ def _validate_exported_object_membership(
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{output_path.name} does not begin with a GLB JSON chunk"
|
f"{output_path.name} does not begin with a GLB JSON chunk"
|
||||||
)
|
)
|
||||||
document = json.loads(source.read(json_length))
|
document = json.loads(binary_file.read(json_length))
|
||||||
exported_names = {
|
exported_names = {
|
||||||
str(node.get("name", ""))
|
str(node.get("name", ""))
|
||||||
for node in document.get("nodes", [])
|
for node in document.get("nodes", [])
|
||||||
if str(node.get("name", ""))
|
if str(node.get("name", ""))
|
||||||
}
|
}
|
||||||
expected_names = {item.name for item in chunk.objects}
|
expected_names = {item.name for item in source.objects}
|
||||||
if exported_names != expected_names:
|
if exported_names != expected_names:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{output_path.name} object membership mismatch: expected "
|
f"{output_path.name} object membership mismatch: expected "
|
||||||
|
|
@ -365,14 +684,41 @@ def _manifest_entry(chunk: ChunkSource, output_path: Path) -> dict[str, object]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _prop_manifest_entry(
|
||||||
|
prop: PropSource,
|
||||||
|
output_path: Path,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": prop.stable_id,
|
||||||
|
"label": prop.label,
|
||||||
|
"source_kind": prop.source_kind,
|
||||||
|
"source_name": prop.source_name,
|
||||||
|
"collection": prop.collection.name,
|
||||||
|
"primary_mesh": prop.primary_mesh.name,
|
||||||
|
"objects": [item.name for item in prop.objects],
|
||||||
|
"source_anchor_world": _round_vector(prop.anchor_world),
|
||||||
|
"file": output_path.name,
|
||||||
|
"sha256": _sha256(output_path),
|
||||||
|
"bounds": {
|
||||||
|
"minimum": _round_vector(prop.bounds.minimum),
|
||||||
|
"maximum": _round_vector(prop.bounds.maximum),
|
||||||
|
"size": _round_vector(prop.bounds.size),
|
||||||
|
"center": _round_vector(prop.bounds.center),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _run() -> int:
|
def _run() -> int:
|
||||||
arguments = _parse_arguments()
|
arguments = _parse_arguments()
|
||||||
if arguments.tolerance <= 0.0:
|
if arguments.tolerance <= 0.0:
|
||||||
raise ExportValidationError("--tolerance must be greater than zero")
|
raise ExportValidationError("--tolerance must be greater than zero")
|
||||||
|
|
||||||
chunks = _discover_chunks(arguments.tolerance)
|
chunks = _discover_chunks(arguments.tolerance)
|
||||||
|
props = _discover_props(arguments.tolerance)
|
||||||
|
_validate_distinct_source_membership(chunks, props)
|
||||||
print(
|
print(
|
||||||
f"Validated {len(chunks)} terrain chunks from "
|
f"Validated {len(chunks)} terrain chunks and {len(props)} reusable "
|
||||||
|
f"props from "
|
||||||
f"{Path(bpy.data.filepath).resolve()}"
|
f"{Path(bpy.data.filepath).resolve()}"
|
||||||
)
|
)
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
|
|
@ -382,6 +728,13 @@ def _run() -> int:
|
||||||
f"{_format_vector(chunk.bounds.maximum)} | "
|
f"{_format_vector(chunk.bounds.maximum)} | "
|
||||||
f"{len(chunk.objects)} object(s)"
|
f"{len(chunk.objects)} object(s)"
|
||||||
)
|
)
|
||||||
|
for prop in props:
|
||||||
|
print(
|
||||||
|
f" {prop.stable_id}: reusable {prop.source_kind} prop | bounds "
|
||||||
|
f"{_format_vector(prop.bounds.minimum)} to "
|
||||||
|
f"{_format_vector(prop.bounds.maximum)} | "
|
||||||
|
f"{len(prop.objects)} object(s)"
|
||||||
|
)
|
||||||
|
|
||||||
if arguments.dry_run:
|
if arguments.dry_run:
|
||||||
print("Dry run complete; no files written.")
|
print("Dry run complete; no files written.")
|
||||||
|
|
@ -390,10 +743,11 @@ def _run() -> int:
|
||||||
output_directory = arguments.output.expanduser().resolve()
|
output_directory = arguments.output.expanduser().resolve()
|
||||||
output_directory.mkdir(parents=True, exist_ok=True)
|
output_directory.mkdir(parents=True, exist_ok=True)
|
||||||
entries: list[dict[str, object]] = []
|
entries: list[dict[str, object]] = []
|
||||||
|
prop_entries: list[dict[str, object]] = []
|
||||||
expected_files: set[str] = set()
|
expected_files: set[str] = set()
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
output_path = output_directory / f"{chunk.stable_id}.glb"
|
output_path = output_directory / f"{chunk.stable_id}.glb"
|
||||||
_export_chunk(chunk, output_path)
|
_export_source(chunk, output_path)
|
||||||
expected_files.add(output_path.name)
|
expected_files.add(output_path.name)
|
||||||
entry = _manifest_entry(chunk, output_path)
|
entry = _manifest_entry(chunk, output_path)
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
|
|
@ -402,12 +756,24 @@ def _run() -> int:
|
||||||
f"{entry['sha256']}"
|
f"{entry['sha256']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for prop in props:
|
||||||
|
output_path = output_directory / f"{prop.stable_id}.glb"
|
||||||
|
_export_source(prop, output_path)
|
||||||
|
expected_files.add(output_path.name)
|
||||||
|
entry = _prop_manifest_entry(prop, output_path)
|
||||||
|
prop_entries.append(entry)
|
||||||
|
print(
|
||||||
|
f"Exported {output_path.name} | {output_path.stat().st_size} bytes | "
|
||||||
|
f"{entry['sha256']}"
|
||||||
|
)
|
||||||
|
|
||||||
manifest = {
|
manifest = {
|
||||||
"format_version": 1,
|
"format_version": 2,
|
||||||
"chunk_size_meters": CHUNK_SIZE_METERS,
|
"chunk_size_meters": CHUNK_SIZE_METERS,
|
||||||
"source_blend": str(Path(bpy.data.filepath).resolve()),
|
"source_blend": str(Path(bpy.data.filepath).resolve()),
|
||||||
"blender_version": bpy.app.version_string,
|
"blender_version": bpy.app.version_string,
|
||||||
"chunks": entries,
|
"chunks": entries,
|
||||||
|
"props": prop_entries,
|
||||||
}
|
}
|
||||||
manifest_path = output_directory / "chunks.json"
|
manifest_path = output_directory / "chunks.json"
|
||||||
manifest_path.write_text(
|
manifest_path.write_text(
|
||||||
|
|
@ -416,14 +782,15 @@ def _run() -> int:
|
||||||
)
|
)
|
||||||
expected_files.add(manifest_path.name)
|
expected_files.add(manifest_path.name)
|
||||||
|
|
||||||
stale_files = sorted(
|
stale_files = sorted({
|
||||||
path.name
|
path.name
|
||||||
for path in output_directory.glob("chunk_*.glb")
|
for pattern in ("chunk_*.glb", "prop_*.glb")
|
||||||
|
for path in output_directory.glob(pattern)
|
||||||
if path.name not in expected_files
|
if path.name not in expected_files
|
||||||
)
|
})
|
||||||
if stale_files:
|
if stale_files:
|
||||||
print(
|
print(
|
||||||
"WARNING: stale chunk outputs were retained: "
|
"WARNING: stale generated-world outputs were retained: "
|
||||||
+ ", ".join(stale_files)
|
+ ", ".join(stale_files)
|
||||||
)
|
)
|
||||||
print(f"Wrote manifest {manifest_path}")
|
print(f"Wrote manifest {manifest_path}")
|
||||||
|
|
|
||||||
20
world/generation/biomes/definitions/biome_coast.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Rule_sand_tree"]
|
||||||
|
script = ExtResource("2_rule")
|
||||||
|
procedural_group = &"sand_tree"
|
||||||
|
placement_chance = 4200
|
||||||
|
adjacency_bonus = 1200
|
||||||
|
maximum_density = 5000
|
||||||
|
minimum_placements = 1
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_palm")
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"biome_coast"
|
||||||
|
label = "coast"
|
||||||
|
required_chunk_tags = PackedStringArray("sand")
|
||||||
|
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_sand_tree")])
|
||||||
31
world/generation/biomes/definitions/biome_forest.tres
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
|
||||||
|
|
||||||
|
[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
|
||||||
|
minimum_placements = 2
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||||
|
script = ExtResource("2_rule")
|
||||||
|
procedural_group = &"grass_detail"
|
||||||
|
placement_chance = 3500
|
||||||
|
adjacency_bonus = 1200
|
||||||
|
maximum_density = 5000
|
||||||
|
minimum_placements = 1
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"biome_forest"
|
||||||
|
label = "forest"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
forbidden_chunk_tags = PackedStringArray("spawn")
|
||||||
|
region_weight = 1.4
|
||||||
|
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])
|
||||||
31
world/generation/biomes/definitions/biome_pine_forest.tres
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
|
||||||
|
|
||||||
|
[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
|
||||||
|
minimum_placements = 2
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_pine")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||||
|
script = ExtResource("2_rule")
|
||||||
|
procedural_group = &"grass_detail"
|
||||||
|
placement_chance = 2500
|
||||||
|
adjacency_bonus = 1000
|
||||||
|
maximum_density = 4000
|
||||||
|
minimum_placements = 1
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"biome_pine_forest"
|
||||||
|
label = "pine forest"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
forbidden_chunk_tags = PackedStringArray("spawn")
|
||||||
|
region_weight = 1.2
|
||||||
|
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])
|
||||||
28
world/generation/biomes/definitions/biome_plains.tres
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Rule_grass_tree"]
|
||||||
|
script = ExtResource("2_rule")
|
||||||
|
procedural_group = &"grass_tree"
|
||||||
|
placement_chance = 1600
|
||||||
|
adjacency_bonus = 900
|
||||||
|
maximum_density = 2200
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||||
|
script = ExtResource("2_rule")
|
||||||
|
procedural_group = &"grass_detail"
|
||||||
|
placement_chance = 700
|
||||||
|
adjacency_bonus = 400
|
||||||
|
maximum_density = 1200
|
||||||
|
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"biome_plains"
|
||||||
|
label = "plains"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
region_weight = 1.0
|
||||||
|
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])
|
||||||
12
world/generation/biomes/terrain_biome_catalog.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainBiomeCatalog" load_steps=7 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_catalog.gd" id="1_catalog"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_plains.tres" id="2_plains"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_forest.tres" id="3_forest"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_pine_forest.tres" id="4_pine"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_coast.tres" id="5_coast"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="6_definition_script"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_catalog")
|
||||||
|
definitions = Array[ExtResource("6_definition_script")]([ExtResource("2_plains"), ExtResource("3_forest"), ExtResource("4_pine"), ExtResource("5_coast")])
|
||||||
BIN
world/generation/chunks/assets/chunk_0005.glb
Normal file
45
world/generation/chunks/assets/chunk_0005.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cg6jtwiq41mbr"
|
||||||
|
path="res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0005.glb"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.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
|
||||||
BIN
world/generation/chunks/assets/chunk_0005_cliff_wall.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://diybhc4ciplal"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "fc97184f537cc23f83a731327e10c836"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0005_cliff_wall.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex", "res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0005_grass_lite.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://i6vbptsv8276"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0005_grass_lite.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex", "res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0006.glb
Normal file
45
world/generation/chunks/assets/chunk_0006.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://ddjqd4kl4tkhg"
|
||||||
|
path="res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0006.glb"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.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
|
||||||
BIN
world/generation/chunks/assets/chunk_0006_cliff_wall.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://beiaoue0vy8c7"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "fc97184f537cc23f83a731327e10c836"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0006_cliff_wall.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex", "res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0006_grass_lite.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://djrg33nkvpnih"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0006_grass_lite.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex", "res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0006_sand.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
45
world/generation/chunks/assets/chunk_0006_sand.png.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://vxomtbcs2mc0"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.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_0006_sand.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex", "res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0007.glb
Normal file
45
world/generation/chunks/assets/chunk_0007.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cniughsb3gacd"
|
||||||
|
path="res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0007.glb"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.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
|
||||||
BIN
world/generation/chunks/assets/chunk_0007_cliff_wall.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b2xro1br3b8hr"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "fc97184f537cc23f83a731327e10c836"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0007_cliff_wall.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex", "res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.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=true
|
||||||
|
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
|
||||||
BIN
world/generation/chunks/assets/chunk_0007_grass_lite.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://3skvv046r4cn"
|
||||||
|
path.s3tc="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/chunks/assets/chunk_0007_grass_lite.png"
|
||||||
|
dest_files=["res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex", "res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.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=true
|
||||||
|
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
|
||||||
|
|
@ -11,4 +11,6 @@ packed_scene = ExtResource("2_scene")
|
||||||
primary_mesh_name = &"chunk_0000"
|
primary_mesh_name = &"chunk_0000"
|
||||||
allowed_rotation_mask = 15
|
allowed_rotation_mask = 15
|
||||||
selection_weight = 4.0
|
selection_weight = 4.0
|
||||||
tags = PackedStringArray("land", "grass", "flat")
|
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn_safe")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast", "fresh_water")
|
||||||
|
preferred_neighbor_tags = PackedStringArray("grass")
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,8 @@ packed_scene = ExtResource("2_scene")
|
||||||
primary_mesh_name = &"chunk_0001"
|
primary_mesh_name = &"chunk_0001"
|
||||||
allowed_rotation_mask = 15
|
allowed_rotation_mask = 15
|
||||||
selection_weight = 1.5
|
selection_weight = 1.5
|
||||||
tags = PackedStringArray("land", "sand", "flat")
|
tags = PackedStringArray("land", "walkable", "sand", "flat")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("sand", "grass")
|
||||||
|
required_neighbor_tags = PackedStringArray("coast")
|
||||||
|
minimum_required_neighbors = 1
|
||||||
|
preferred_neighbor_tags = PackedStringArray("coast", "sand")
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,10 @@ label = "beach"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
primary_mesh_name = &"chunk_0002"
|
primary_mesh_name = &"chunk_0002"
|
||||||
selection_weight = 0.75
|
selection_weight = 0.75
|
||||||
maximum_placements = 10
|
maximum_placements = 20
|
||||||
tags = PackedStringArray("land", "sand", "coast", "slope")
|
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("sand", "grass")
|
||||||
|
required_neighbor_tags = PackedStringArray("sand")
|
||||||
|
minimum_required_neighbors = 1
|
||||||
|
preferred_neighbor_tags = PackedStringArray("sand", "grass")
|
||||||
|
ocean_facing_edges = 2
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ label = "stream"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
primary_mesh_name = &"chunk_0003"
|
primary_mesh_name = &"chunk_0003"
|
||||||
selection_weight = 0.6
|
selection_weight = 0.6
|
||||||
tags = PackedStringArray("land", "fresh_water", "river", "flowing")
|
tags = PackedStringArray("land", "walkable", "fresh_water", "river", "flowing")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass")
|
||||||
|
preferred_neighbor_tags = PackedStringArray("grass")
|
||||||
water_inlet_edges = 1
|
water_inlet_edges = 1
|
||||||
water_outlet_edges = 4
|
water_outlet_edges = 4
|
||||||
water_surface_size = Vector2(2.1, 10)
|
water_surface_size = Vector2(2.1, 10)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ label = "pond"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
primary_mesh_name = &"chunk_0004"
|
primary_mesh_name = &"chunk_0004"
|
||||||
selection_weight = 0.35
|
selection_weight = 0.35
|
||||||
tags = PackedStringArray("land", "fresh_water", "pond")
|
tags = PackedStringArray("land", "walkable", "fresh_water", "pond")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass")
|
||||||
|
preferred_neighbor_tags = PackedStringArray("grass")
|
||||||
water_inlet_edges = 1
|
water_inlet_edges = 1
|
||||||
water_surface_size = Vector2(6.7, 7.75)
|
water_surface_size = Vector2(6.7, 7.75)
|
||||||
water_surface_offset = Vector2(-0.32, -1.03)
|
water_surface_offset = Vector2(-0.32, -1.03)
|
||||||
|
|
|
||||||
21
world/generation/chunks/definitions/chunk_0005.tres
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[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" uid="uid://cg6jtwiq41mbr" path="res://world/generation/chunks/assets/chunk_0005.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"chunk_0005"
|
||||||
|
label = "grass ocean edge"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
primary_mesh_name = &"chunk_0005"
|
||||||
|
selection_weight = 1.0
|
||||||
|
maximum_placements = 20
|
||||||
|
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "grass_ocean_edge")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
|
||||||
|
north_allowed_neighbor_tags = PackedStringArray("coast")
|
||||||
|
south_allowed_neighbor_tags = PackedStringArray("coast")
|
||||||
|
west_allowed_neighbor_tags = PackedStringArray("grass")
|
||||||
|
preferred_neighbor_tags = PackedStringArray("grass", "coast")
|
||||||
|
prefers_map_boundary = true
|
||||||
|
ocean_facing_edges = 2
|
||||||
23
world/generation/chunks/definitions/chunk_0006.tres
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ddjqd4kl4tkhg" path="res://world/generation/chunks/assets/chunk_0006.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"chunk_0006"
|
||||||
|
label = "grass to beach ocean edge"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
primary_mesh_name = &"chunk_0006"
|
||||||
|
selection_weight = 0.35
|
||||||
|
maximum_placements = 8
|
||||||
|
tags = PackedStringArray("land", "walkable", "sand", "coast", "transition", "slope")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
|
||||||
|
north_allowed_neighbor_tags = PackedStringArray("coast")
|
||||||
|
south_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
|
||||||
|
west_allowed_neighbor_tags = PackedStringArray("grass")
|
||||||
|
required_neighbor_tags = PackedStringArray("coast")
|
||||||
|
minimum_required_neighbors = 2
|
||||||
|
preferred_neighbor_tags = PackedStringArray("coast")
|
||||||
|
prefers_map_boundary = true
|
||||||
|
ocean_facing_edges = 2
|
||||||
22
world/generation/chunks/definitions/chunk_0007.tres
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cniughsb3gacd" path="res://world/generation/chunks/assets/chunk_0007.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"chunk_0007"
|
||||||
|
label = "grass ocean corner"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
primary_mesh_name = &"chunk_0007"
|
||||||
|
selection_weight = 0.2
|
||||||
|
maximum_placements = 4
|
||||||
|
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass", "coast")
|
||||||
|
north_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
|
||||||
|
west_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
|
||||||
|
required_neighbor_tags = PackedStringArray("grass_ocean_edge")
|
||||||
|
minimum_required_neighbors = 2
|
||||||
|
preferred_neighbor_tags = PackedStringArray("coast")
|
||||||
|
prefers_map_boundary = true
|
||||||
|
ocean_facing_edges = 6
|
||||||
|
|
@ -12,4 +12,8 @@ primary_mesh_name = &"chunk_0000"
|
||||||
allowed_rotation_mask = 1
|
allowed_rotation_mask = 1
|
||||||
selection_weight = 0.01
|
selection_weight = 0.01
|
||||||
maximum_placements = 1
|
maximum_placements = 1
|
||||||
tags = PackedStringArray("land", "grass", "flat", "spawn")
|
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn")
|
||||||
|
allowed_neighbor_tags = PackedStringArray("grass", "coast")
|
||||||
|
required_neighbor_tags = PackedStringArray("spawn_safe")
|
||||||
|
minimum_required_neighbors = 3
|
||||||
|
preferred_neighbor_tags = PackedStringArray("spawn_safe")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=9 format=3]
|
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=12 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
|
||||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
|
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
|
||||||
|
|
@ -8,8 +8,11 @@
|
||||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0004.tres" id="6_pond"]
|
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0004.tres" id="6_pond"]
|
||||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="7_definition_script"]
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="7_definition_script"]
|
||||||
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_spawn.tres" id="8_spawn"]
|
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_spawn.tres" id="8_spawn"]
|
||||||
|
[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"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_catalog")
|
script = ExtResource("1_catalog")
|
||||||
chunk_size = 10.0
|
chunk_size = 10.0
|
||||||
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("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("8_spawn")])
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,6 @@ const FishingShopInteractionType = preload(
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
const PROP_SOURCE: PackedScene = preload(
|
|
||||||
"res://art/exported/environment/terrain/starter_island.glb"
|
|
||||||
)
|
|
||||||
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
||||||
const SALT_WATER_MATERIAL: Material = preload(
|
const SALT_WATER_MATERIAL: Material = preload(
|
||||||
"res://world/materials/stylized_water.tres"
|
"res://world/materials/stylized_water.tres"
|
||||||
|
|
@ -25,16 +22,21 @@ const POND_POOL: FishPool = preload(
|
||||||
const OCEAN_POOL: FishPool = preload(
|
const OCEAN_POOL: FishPool = preload(
|
||||||
"res://fish/pools/starter_ocean_pool.tres"
|
"res://fish/pools/starter_ocean_pool.tres"
|
||||||
)
|
)
|
||||||
const TREE_SOURCE_NAME := "tree_2_001"
|
|
||||||
const PALM_SOURCE_NAME := "plam_tree"
|
|
||||||
const TREE_RUNTIME_NAME := "regular_tree"
|
|
||||||
const PALM_RUNTIME_NAME := "palm_tree"
|
|
||||||
const WATER_HEIGHT := -0.25
|
const WATER_HEIGHT := -0.25
|
||||||
const PROP_EDGE_MARGIN := 2.2
|
const PROP_EDGE_MARGIN := 2.2
|
||||||
const TREE_CHANCE := 0.58
|
const PROP_PLACEMENT_ATTEMPTS := 12
|
||||||
const PALM_CHANCE := 0.42
|
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||||
|
const PROP_CHANCE_SCALE := 10000
|
||||||
|
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||||
|
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
||||||
|
&"grass_tree",
|
||||||
|
&"grass_detail",
|
||||||
|
&"sand_tree",
|
||||||
|
]
|
||||||
|
|
||||||
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||||
|
@export var prop_catalog: TerrainPropCatalog
|
||||||
|
@export var biome_catalog: TerrainBiomeCatalog
|
||||||
|
|
||||||
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
|
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
|
||||||
@onready var _player_spawn: Marker3D = %PlayerSpawn
|
@onready var _player_spawn: Marker3D = %PlayerSpawn
|
||||||
|
|
@ -56,15 +58,17 @@ const PALM_CHANCE := 0.42
|
||||||
|
|
||||||
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||||
var _light_performance_profile := false
|
var _light_performance_profile := false
|
||||||
var _source_meshes: Dictionary[String, Mesh] = {}
|
var _placed_prop_positions: Array[Vector3] = []
|
||||||
var _source_bases: Dictionary[String, Basis] = {}
|
var _placed_prop_clearance_radii: Array[float] = []
|
||||||
var _source_minimum_y: Dictionary[String, float] = {}
|
var _placed_prop_groups: Array[StringName] = []
|
||||||
|
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
||||||
|
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_generator.generation_completed.connect(_on_generation_completed)
|
_generator.generation_completed.connect(_on_generation_completed)
|
||||||
_cache_prop_source(TREE_SOURCE_NAME)
|
_validate_prop_catalog()
|
||||||
_cache_prop_source(PALM_SOURCE_NAME)
|
_validate_biome_catalog()
|
||||||
_configure_static_water()
|
_configure_static_water()
|
||||||
_build_shoreline_reference()
|
_build_shoreline_reference()
|
||||||
if not generate_world(initial_seed):
|
if not generate_world(initial_seed):
|
||||||
|
|
@ -107,6 +111,18 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||||
return _shoreline_reference
|
return _shoreline_reference
|
||||||
|
|
||||||
|
|
||||||
|
func get_prop_catalog() -> TerrainPropCatalog:
|
||||||
|
return prop_catalog
|
||||||
|
|
||||||
|
|
||||||
|
func get_biome_catalog() -> TerrainBiomeCatalog:
|
||||||
|
return biome_catalog
|
||||||
|
|
||||||
|
|
||||||
|
func get_biome_at(coordinate: Vector2i) -> StringName:
|
||||||
|
return _biome_assignments.get(coordinate, &"")
|
||||||
|
|
||||||
|
|
||||||
func set_light_performance_profile(enabled: bool) -> void:
|
func set_light_performance_profile(enabled: bool) -> void:
|
||||||
_light_performance_profile = enabled
|
_light_performance_profile = enabled
|
||||||
_apply_water_materials()
|
_apply_water_materials()
|
||||||
|
|
@ -118,10 +134,9 @@ func get_spawn_surface_triangles(
|
||||||
minimum_up_dot: float = 0.6,
|
minimum_up_dot: float = 0.6,
|
||||||
) -> Array[PackedVector3Array]:
|
) -> Array[PackedVector3Array]:
|
||||||
var triangles: Array[PackedVector3Array] = []
|
var triangles: Array[PackedVector3Array] = []
|
||||||
var root: Node3D = _generator.get_generated_chunks_root()
|
if material_names.is_empty():
|
||||||
if root == null or material_names.is_empty():
|
|
||||||
return triangles
|
return triangles
|
||||||
for mesh_instance: MeshInstance3D in _collect_mesh_instances(root):
|
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
||||||
var mesh: Mesh = mesh_instance.mesh
|
var mesh: Mesh = mesh_instance.mesh
|
||||||
if mesh == null:
|
if mesh == null:
|
||||||
continue
|
continue
|
||||||
|
|
@ -143,15 +158,31 @@ func get_spawn_surface_triangles(
|
||||||
|
|
||||||
func _on_generation_completed(summary: Dictionary) -> void:
|
func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
var records: Array[Dictionary] = _generator.placement_records()
|
var records: Array[Dictionary] = _generator.placement_records()
|
||||||
|
_assign_biomes(records, summary)
|
||||||
var spawn_position := Vector3.ZERO
|
var spawn_position := Vector3.ZERO
|
||||||
_clear_children(_decorations)
|
_clear_children(_decorations)
|
||||||
_clear_children(_tree_anchors)
|
_clear_children(_tree_anchors)
|
||||||
|
_placed_prop_positions.clear()
|
||||||
|
_placed_prop_clearance_radii.clear()
|
||||||
|
_placed_prop_groups.clear()
|
||||||
|
_placed_group_coordinates.clear()
|
||||||
var random := RandomNumberGenerator.new()
|
var random := RandomNumberGenerator.new()
|
||||||
random.seed = _current_seed ^ 0x5EED71
|
random.seed = _current_seed ^ 0x5EED71
|
||||||
var grass_centers: Array[Vector3] = []
|
var terrain_triangles := _terrain_surface_triangles()
|
||||||
var sand_centers: Array[Vector3] = []
|
var biomes: Array[TerrainBiomeDefinition] = []
|
||||||
var tree_count := 0
|
if biome_catalog != null:
|
||||||
var palm_count := 0
|
biomes.assign(biome_catalog.definitions)
|
||||||
|
var eligible_records: Dictionary[StringName, Array] = {}
|
||||||
|
var group_counts: Dictionary[StringName, int] = {}
|
||||||
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
||||||
|
for biome: TerrainBiomeDefinition in biomes:
|
||||||
|
var rule := biome.prop_rule_for_group(group)
|
||||||
|
if rule == null:
|
||||||
|
continue
|
||||||
|
var group_key := _biome_group_key(biome.stable_id, group)
|
||||||
|
eligible_records[group_key] = []
|
||||||
|
group_counts[group_key] = 0
|
||||||
|
_placed_group_coordinates[group_key] = []
|
||||||
for record: Dictionary in records:
|
for record: Dictionary in records:
|
||||||
var position: Vector3 = record.get("position", Vector3.ZERO)
|
var position: Vector3 = record.get("position", Vector3.ZERO)
|
||||||
var tags: PackedStringArray = record.get(
|
var tags: PackedStringArray = record.get(
|
||||||
|
|
@ -159,50 +190,124 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
if "spawn" in tags:
|
if "spawn" in tags:
|
||||||
spawn_position = position
|
spawn_position = position
|
||||||
if "spawn" not in tags and "grass" in tags:
|
continue
|
||||||
grass_centers.append(position)
|
var coordinate: Vector2i = record.get(
|
||||||
if _maybe_add_prop(
|
"coordinate",
|
||||||
TREE_SOURCE_NAME,
|
Vector2i.ZERO,
|
||||||
TREE_RUNTIME_NAME,
|
|
||||||
position,
|
|
||||||
TREE_CHANCE,
|
|
||||||
random,
|
|
||||||
true,
|
|
||||||
):
|
|
||||||
tree_count += 1
|
|
||||||
elif "sand" in tags:
|
|
||||||
sand_centers.append(position)
|
|
||||||
if _maybe_add_prop(
|
|
||||||
PALM_SOURCE_NAME,
|
|
||||||
PALM_RUNTIME_NAME,
|
|
||||||
position,
|
|
||||||
PALM_CHANCE,
|
|
||||||
random,
|
|
||||||
false,
|
|
||||||
):
|
|
||||||
palm_count += 1
|
|
||||||
if tree_count == 0 and not grass_centers.is_empty():
|
|
||||||
_add_prop(
|
|
||||||
TREE_SOURCE_NAME,
|
|
||||||
TREE_RUNTIME_NAME,
|
|
||||||
grass_centers[random.randi_range(0, grass_centers.size() - 1)],
|
|
||||||
random,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
if palm_count == 0 and not sand_centers.is_empty():
|
|
||||||
_add_prop(
|
|
||||||
PALM_SOURCE_NAME,
|
|
||||||
PALM_RUNTIME_NAME,
|
|
||||||
sand_centers[random.randi_range(0, sand_centers.size() - 1)],
|
|
||||||
random,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
|
var biome_id: StringName = record.get("biome_id", &"")
|
||||||
|
if biome_catalog == null:
|
||||||
|
continue
|
||||||
|
var biome := biome_catalog.definition_for_id(biome_id)
|
||||||
|
if biome == null:
|
||||||
|
continue
|
||||||
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
||||||
|
var rule := biome.prop_rule_for_group(group)
|
||||||
|
if rule == null:
|
||||||
|
continue
|
||||||
|
var definitions := _spawn_distance_eligible_definitions(
|
||||||
|
_prop_definitions_for(rule, tags),
|
||||||
|
coordinate,
|
||||||
|
)
|
||||||
|
if definitions.is_empty():
|
||||||
|
continue
|
||||||
|
var group_key := _biome_group_key(biome_id, group)
|
||||||
|
var group_records: Array = eligible_records[group_key]
|
||||||
|
group_records.append(record)
|
||||||
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
||||||
|
for biome: TerrainBiomeDefinition in biomes:
|
||||||
|
var rule := biome.prop_rule_for_group(group)
|
||||||
|
if rule == null:
|
||||||
|
continue
|
||||||
|
var group_key := _biome_group_key(biome.stable_id, group)
|
||||||
|
var group_records: Array = eligible_records[group_key]
|
||||||
|
var maximum := _prop_group_maximum(rule, group_records.size())
|
||||||
|
for record: Dictionary in group_records:
|
||||||
|
if group_counts.get(group_key, 0) >= maximum:
|
||||||
|
break
|
||||||
|
var coordinate: Vector2i = record.get(
|
||||||
|
"coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
var tags: PackedStringArray = record.get(
|
||||||
|
"tags",
|
||||||
|
PackedStringArray(),
|
||||||
|
)
|
||||||
|
var definitions := _spawn_distance_eligible_definitions(
|
||||||
|
_prop_definitions_for(rule, tags),
|
||||||
|
coordinate,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
terrain_triangles,
|
||||||
|
):
|
||||||
|
group_counts[group_key] += 1
|
||||||
|
_record_prop_group_coordinate(
|
||||||
|
group_key,
|
||||||
|
coordinate,
|
||||||
|
)
|
||||||
|
_ensure_minimum_props(
|
||||||
|
eligible_records,
|
||||||
|
group_counts,
|
||||||
|
random,
|
||||||
|
terrain_triangles,
|
||||||
|
)
|
||||||
_place_spawn_amenities(spawn_position)
|
_place_spawn_amenities(spawn_position)
|
||||||
_configure_fresh_water(records)
|
_configure_fresh_water(records)
|
||||||
_configure_diggable_area()
|
_configure_diggable_area()
|
||||||
world_generated.emit(_current_seed, summary)
|
world_generated.emit(_current_seed, summary)
|
||||||
|
|
||||||
|
|
||||||
|
func _assign_biomes(
|
||||||
|
records: Array[Dictionary],
|
||||||
|
summary: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
_biome_assignments = TerrainBiomeAssigner.assign(
|
||||||
|
biome_catalog,
|
||||||
|
records,
|
||||||
|
_current_seed,
|
||||||
|
)
|
||||||
|
for index: int in records.size():
|
||||||
|
var record := records[index]
|
||||||
|
var coordinate: Vector2i = record.get(
|
||||||
|
"coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
record["biome_id"] = _biome_assignments.get(coordinate, &"")
|
||||||
|
records[index] = record
|
||||||
|
var generated_chunks := _generator.get_generated_chunks_root()
|
||||||
|
if generated_chunks != null:
|
||||||
|
for child: Node in generated_chunks.get_children():
|
||||||
|
var coordinate: Vector2i = child.get_meta(
|
||||||
|
&"terrain_chunk_coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
child.set_meta(
|
||||||
|
&"terrain_biome_id",
|
||||||
|
_biome_assignments.get(coordinate, &""),
|
||||||
|
)
|
||||||
|
summary["biome_counts"] = TerrainBiomeAssigner.counts(
|
||||||
|
_biome_assignments
|
||||||
|
)
|
||||||
|
summary["biome_fingerprint"] = TerrainBiomeAssigner.fingerprint(
|
||||||
|
_biome_assignments
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _place_spawn_amenities(center: Vector3) -> void:
|
func _place_spawn_amenities(center: Vector3) -> void:
|
||||||
_player_spawn.position = center + Vector3(0.0, 0.18, 2.2)
|
_player_spawn.position = center + Vector3(0.0, 0.18, 2.2)
|
||||||
_safe_spawn.position = _player_spawn.position
|
_safe_spawn.position = _player_spawn.position
|
||||||
|
|
@ -312,60 +417,410 @@ func _light_water_material(color: Color) -> StandardMaterial3D:
|
||||||
|
|
||||||
|
|
||||||
func _maybe_add_prop(
|
func _maybe_add_prop(
|
||||||
source_name: String,
|
definitions: Array[TerrainPropDefinition],
|
||||||
runtime_name: String,
|
|
||||||
chunk_center: Vector3,
|
chunk_center: Vector3,
|
||||||
chance: float,
|
chunk_coordinate: Vector2i,
|
||||||
|
biome_id: StringName,
|
||||||
random: RandomNumberGenerator,
|
random: RandomNumberGenerator,
|
||||||
add_anchor: bool,
|
terrain_triangles: Array[PackedVector3Array],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if random.randf() > chance or not _source_meshes.has(source_name):
|
var definition := _pick_prop_definition(
|
||||||
return false
|
definitions,
|
||||||
_add_prop(
|
|
||||||
source_name,
|
|
||||||
runtime_name,
|
|
||||||
chunk_center,
|
chunk_center,
|
||||||
random,
|
random,
|
||||||
add_anchor,
|
|
||||||
)
|
)
|
||||||
return true
|
if definition == null:
|
||||||
|
return false
|
||||||
|
return _add_prop(
|
||||||
|
definition,
|
||||||
|
chunk_center,
|
||||||
|
chunk_coordinate,
|
||||||
|
biome_id,
|
||||||
|
random,
|
||||||
|
terrain_triangles,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _add_prop(
|
func _add_prop(
|
||||||
source_name: String,
|
definition: TerrainPropDefinition,
|
||||||
runtime_name: String,
|
|
||||||
chunk_center: Vector3,
|
chunk_center: Vector3,
|
||||||
|
chunk_coordinate: Vector2i,
|
||||||
|
biome_id: StringName,
|
||||||
random: RandomNumberGenerator,
|
random: RandomNumberGenerator,
|
||||||
add_anchor: bool,
|
terrain_triangles: Array[PackedVector3Array],
|
||||||
) -> void:
|
) -> bool:
|
||||||
if not _source_meshes.has(source_name):
|
if definition == null or definition.packed_scene == null:
|
||||||
return
|
return false
|
||||||
|
var placement := Vector3.ZERO
|
||||||
|
var found_surface := false
|
||||||
|
for attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||||
|
var offset := Vector3(
|
||||||
|
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||||
|
0.0,
|
||||||
|
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||||
|
)
|
||||||
|
placement = chunk_center + offset
|
||||||
|
var surface_height := _surface_height_at(
|
||||||
|
placement,
|
||||||
|
terrain_triangles,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
surface_height > -INF
|
||||||
|
and surface_height
|
||||||
|
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
|
||||||
|
and _has_prop_clearance(
|
||||||
|
placement,
|
||||||
|
definition.clearance_radius,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
placement.y = surface_height
|
||||||
|
found_surface = true
|
||||||
|
break
|
||||||
|
if not found_surface:
|
||||||
|
return false
|
||||||
|
var packed_instance := definition.packed_scene.instantiate()
|
||||||
|
var visual_root := packed_instance as Node3D
|
||||||
|
if visual_root == null:
|
||||||
|
packed_instance.free()
|
||||||
|
return false
|
||||||
var prop := Node3D.new()
|
var prop := Node3D.new()
|
||||||
prop.name = "%s_%d" % [runtime_name, _decorations.get_child_count()]
|
prop.name = "%s_%d" % [
|
||||||
var offset := Vector3(
|
String(definition.stable_id).trim_prefix("prop_"),
|
||||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
_decorations.get_child_count(),
|
||||||
0.0,
|
]
|
||||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
prop.set_meta(&"terrain_prop_id", definition.stable_id)
|
||||||
)
|
prop.set_meta(&"terrain_prop_group", definition.procedural_group)
|
||||||
prop.position = chunk_center + offset
|
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
|
||||||
|
prop.set_meta(&"terrain_biome_id", biome_id)
|
||||||
|
prop.position = placement
|
||||||
prop.rotation.y = random.randf_range(-PI, PI)
|
prop.rotation.y = random.randf_range(-PI, PI)
|
||||||
_decorations.add_child(prop)
|
_decorations.add_child(prop)
|
||||||
var visual := MeshInstance3D.new()
|
visual_root.name = "Visual"
|
||||||
visual.name = "Visual"
|
prop.add_child(visual_root)
|
||||||
visual.mesh = _source_meshes[source_name]
|
_configure_prop_visuals(visual_root)
|
||||||
visual.basis = _source_bases[source_name]
|
_add_prop_collision(prop, definition)
|
||||||
visual.position.y = -_source_minimum_y.get(source_name, 0.0)
|
_placed_prop_positions.append(placement)
|
||||||
visual.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
_placed_prop_clearance_radii.append(definition.clearance_radius)
|
||||||
prop.add_child(visual)
|
_placed_prop_groups.append(definition.procedural_group)
|
||||||
_add_prop_collision(prop, source_name == TREE_SOURCE_NAME)
|
if definition.gatherable_anchor_height > 0.0:
|
||||||
if add_anchor:
|
|
||||||
var anchor := Marker3D.new()
|
var anchor := Marker3D.new()
|
||||||
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
||||||
anchor.position = prop.position + Vector3(0.0, 2.15, 0.0)
|
anchor.position = prop.position + Vector3(
|
||||||
|
0.0,
|
||||||
|
definition.gatherable_anchor_height,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
_tree_anchors.add_child(anchor)
|
_tree_anchors.add_child(anchor)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
|
func _validate_prop_catalog() -> void:
|
||||||
|
if prop_catalog == null:
|
||||||
|
push_error("Generated world terrain prop catalog is unavailable.")
|
||||||
|
return
|
||||||
|
for error: String in prop_catalog.validation_errors():
|
||||||
|
push_error("Generated world terrain prop catalog: %s" % error)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_biome_catalog() -> void:
|
||||||
|
if biome_catalog == null:
|
||||||
|
push_error("Generated world terrain biome catalog is unavailable.")
|
||||||
|
return
|
||||||
|
for error: String in biome_catalog.validation_errors():
|
||||||
|
push_error("Generated world terrain biome catalog: %s" % error)
|
||||||
|
if prop_catalog == null:
|
||||||
|
return
|
||||||
|
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
|
||||||
|
if biome == null:
|
||||||
|
continue
|
||||||
|
for rule: TerrainBiomePropRule in biome.prop_rules:
|
||||||
|
if rule == null:
|
||||||
|
continue
|
||||||
|
if rule.procedural_group not in PROCEDURAL_PROP_GROUPS:
|
||||||
|
push_error(
|
||||||
|
"Biome %s references unsupported prop group %s."
|
||||||
|
% [biome.stable_id, rule.procedural_group]
|
||||||
|
)
|
||||||
|
for prop_id_value: String in rule.allowed_prop_ids:
|
||||||
|
var prop_id := StringName(prop_id_value)
|
||||||
|
var definition := prop_catalog.definition_for_id(prop_id)
|
||||||
|
if definition == null:
|
||||||
|
push_error(
|
||||||
|
"Biome %s references missing prop %s."
|
||||||
|
% [biome.stable_id, prop_id]
|
||||||
|
)
|
||||||
|
elif definition.procedural_group != rule.procedural_group:
|
||||||
|
push_error(
|
||||||
|
"Biome %s assigns prop %s to the wrong group."
|
||||||
|
% [biome.stable_id, prop_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _prop_definitions_for(
|
||||||
|
rule: TerrainBiomePropRule,
|
||||||
|
tags: PackedStringArray,
|
||||||
|
) -> Array[TerrainPropDefinition]:
|
||||||
|
if prop_catalog == null or rule == null:
|
||||||
|
return []
|
||||||
|
var result: Array[TerrainPropDefinition] = []
|
||||||
|
for definition: TerrainPropDefinition in (
|
||||||
|
prop_catalog.procedural_definitions(rule.procedural_group, tags)
|
||||||
|
):
|
||||||
|
if rule.allows_prop(definition):
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _prop_group_roll_succeeds(
|
||||||
|
rule: TerrainBiomePropRule,
|
||||||
|
group_key: StringName,
|
||||||
|
coordinate: Vector2i,
|
||||||
|
random: RandomNumberGenerator,
|
||||||
|
) -> bool:
|
||||||
|
var threshold := rule.placement_chance
|
||||||
|
if _group_has_adjacent_placement(group_key, coordinate):
|
||||||
|
threshold += rule.adjacency_bonus
|
||||||
|
threshold = mini(threshold, PROP_CHANCE_SCALE)
|
||||||
|
return random.randi_range(0, PROP_CHANCE_SCALE - 1) < threshold
|
||||||
|
|
||||||
|
|
||||||
|
func _pick_prop_definition(
|
||||||
|
definitions: Array[TerrainPropDefinition],
|
||||||
|
chunk_center: Vector3,
|
||||||
|
random: RandomNumberGenerator,
|
||||||
|
) -> TerrainPropDefinition:
|
||||||
|
var total_weight := 0
|
||||||
|
var weights: Array[int] = []
|
||||||
|
for definition: TerrainPropDefinition in definitions:
|
||||||
|
var weight := maxi(
|
||||||
|
1,
|
||||||
|
roundi(
|
||||||
|
definition.selection_weight
|
||||||
|
* float(PROP_SELECTION_WEIGHT_SCALE)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not definition.preferred_nearby_prop_groups.is_empty():
|
||||||
|
var multiplier := definition.nearby_preference_weight_multiplier
|
||||||
|
if _has_preferred_prop_near(definition, chunk_center):
|
||||||
|
weight = maxi(roundi(float(weight) * multiplier), 1)
|
||||||
|
else:
|
||||||
|
weight = maxi(roundi(float(weight) / multiplier), 1)
|
||||||
|
weights.append(weight)
|
||||||
|
total_weight += weight
|
||||||
|
if total_weight <= 0:
|
||||||
|
return null
|
||||||
|
var roll := random.randi_range(1, total_weight)
|
||||||
|
for index: int in definitions.size():
|
||||||
|
roll -= weights[index]
|
||||||
|
if roll <= 0:
|
||||||
|
return definitions[index]
|
||||||
|
return definitions.back() if not definitions.is_empty() else null
|
||||||
|
|
||||||
|
|
||||||
|
func _prop_group_maximum(
|
||||||
|
rule: TerrainBiomePropRule,
|
||||||
|
eligible_count: int,
|
||||||
|
) -> int:
|
||||||
|
if eligible_count <= 0:
|
||||||
|
return 0
|
||||||
|
var maximum := ceili(
|
||||||
|
float(eligible_count * rule.maximum_density)
|
||||||
|
/ float(PROP_CHANCE_SCALE)
|
||||||
|
)
|
||||||
|
return maxi(maximum, rule.minimum_placements)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_distance_eligible_definitions(
|
||||||
|
definitions: Array[TerrainPropDefinition],
|
||||||
|
coordinate: Vector2i,
|
||||||
|
) -> Array[TerrainPropDefinition]:
|
||||||
|
var result: Array[TerrainPropDefinition] = []
|
||||||
|
var center := Vector2i(
|
||||||
|
_generator.grid_size.x / 2,
|
||||||
|
_generator.grid_size.y / 2,
|
||||||
|
)
|
||||||
|
var distance := (
|
||||||
|
absi(coordinate.x - center.x)
|
||||||
|
+ absi(coordinate.y - center.y)
|
||||||
|
)
|
||||||
|
for definition: TerrainPropDefinition in definitions:
|
||||||
|
if distance >= definition.minimum_spawn_chunk_distance:
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _group_has_adjacent_placement(
|
||||||
|
group: StringName,
|
||||||
|
coordinate: Vector2i,
|
||||||
|
) -> bool:
|
||||||
|
var coordinates: Array = _placed_group_coordinates.get(group, [])
|
||||||
|
for coordinate_value: Variant in coordinates:
|
||||||
|
var other: Vector2i = coordinate_value
|
||||||
|
if (
|
||||||
|
absi(coordinate.x - other.x)
|
||||||
|
+ absi(coordinate.y - other.y)
|
||||||
|
== 1
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _record_prop_group_coordinate(
|
||||||
|
group: StringName,
|
||||||
|
coordinate: Vector2i,
|
||||||
|
) -> void:
|
||||||
|
var coordinates: Array = _placed_group_coordinates.get(group, [])
|
||||||
|
coordinates.append(coordinate)
|
||||||
|
_placed_group_coordinates[group] = coordinates
|
||||||
|
|
||||||
|
|
||||||
|
func _has_preferred_prop_near(
|
||||||
|
definition: TerrainPropDefinition,
|
||||||
|
position: Vector3,
|
||||||
|
) -> bool:
|
||||||
|
for index: int in _placed_prop_positions.size():
|
||||||
|
if (
|
||||||
|
_placed_prop_groups[index]
|
||||||
|
not in definition.preferred_nearby_prop_groups
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var other := _placed_prop_positions[index]
|
||||||
|
if Vector2(position.x - other.x, position.z - other.z).length() <= (
|
||||||
|
definition.preferred_nearby_radius
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_minimum_props(
|
||||||
|
eligible_records: Dictionary[StringName, Array],
|
||||||
|
group_counts: Dictionary[StringName, int],
|
||||||
|
random: RandomNumberGenerator,
|
||||||
|
terrain_triangles: Array[PackedVector3Array],
|
||||||
|
) -> void:
|
||||||
|
if biome_catalog == null:
|
||||||
|
return
|
||||||
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
||||||
|
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
|
||||||
|
var rule := biome.prop_rule_for_group(group)
|
||||||
|
if rule == null or rule.minimum_placements <= 0:
|
||||||
|
continue
|
||||||
|
var group_key := _biome_group_key(biome.stable_id, group)
|
||||||
|
var records: Array = eligible_records.get(group_key, [])
|
||||||
|
if records.is_empty():
|
||||||
|
continue
|
||||||
|
var maximum_attempts := maxi(
|
||||||
|
PROP_PLACEMENT_ATTEMPTS,
|
||||||
|
records.size() * rule.minimum_placements * 2,
|
||||||
|
)
|
||||||
|
for _attempt: int in maximum_attempts:
|
||||||
|
if (
|
||||||
|
group_counts.get(group_key, 0)
|
||||||
|
>= rule.minimum_placements
|
||||||
|
):
|
||||||
|
break
|
||||||
|
var record: Dictionary = records[
|
||||||
|
random.randi_range(0, records.size() - 1)
|
||||||
|
]
|
||||||
|
var tags: PackedStringArray = record.get(
|
||||||
|
"tags",
|
||||||
|
PackedStringArray(),
|
||||||
|
)
|
||||||
|
var coordinate: Vector2i = record.get(
|
||||||
|
"coordinate",
|
||||||
|
Vector2i.ZERO,
|
||||||
|
)
|
||||||
|
var definitions := _spawn_distance_eligible_definitions(
|
||||||
|
_prop_definitions_for(rule, tags),
|
||||||
|
coordinate,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _biome_group_key(
|
||||||
|
biome_id: StringName,
|
||||||
|
group: StringName,
|
||||||
|
) -> StringName:
|
||||||
|
return StringName("%s:%s" % [biome_id, group])
|
||||||
|
|
||||||
|
|
||||||
|
func _has_prop_clearance(position: Vector3, radius: float) -> bool:
|
||||||
|
for index: int in _placed_prop_positions.size():
|
||||||
|
var other := _placed_prop_positions[index]
|
||||||
|
var distance := Vector2(
|
||||||
|
position.x - other.x,
|
||||||
|
position.z - other.z,
|
||||||
|
).length()
|
||||||
|
if distance < radius + _placed_prop_clearance_radii[index]:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _configure_prop_visuals(root_node: Node) -> void:
|
||||||
|
if root_node is GeometryInstance3D:
|
||||||
|
(root_node as GeometryInstance3D).cast_shadow = (
|
||||||
|
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
)
|
||||||
|
for child: Node in root_node.get_children():
|
||||||
|
_configure_prop_visuals(child)
|
||||||
|
|
||||||
|
|
||||||
|
func _terrain_surface_triangles() -> Array[PackedVector3Array]:
|
||||||
|
var triangles: Array[PackedVector3Array] = []
|
||||||
|
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
||||||
|
if mesh_instance.mesh == null:
|
||||||
|
continue
|
||||||
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||||
|
_append_surface_triangles(
|
||||||
|
triangles,
|
||||||
|
mesh_instance,
|
||||||
|
mesh_instance.mesh.surface_get_arrays(surface_index),
|
||||||
|
-INF,
|
||||||
|
0.35,
|
||||||
|
)
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
func _surface_height_at(
|
||||||
|
position: Vector3,
|
||||||
|
triangles: Array[PackedVector3Array],
|
||||||
|
) -> float:
|
||||||
|
var highest := -INF
|
||||||
|
var segment_start := position + Vector3.UP * 100.0
|
||||||
|
var segment_end := position + Vector3.DOWN * 100.0
|
||||||
|
for triangle: PackedVector3Array in triangles:
|
||||||
|
if triangle.size() != 3:
|
||||||
|
continue
|
||||||
|
var hit: Variant = Geometry3D.segment_intersects_triangle(
|
||||||
|
segment_start,
|
||||||
|
segment_end,
|
||||||
|
triangle[0],
|
||||||
|
triangle[1],
|
||||||
|
triangle[2],
|
||||||
|
)
|
||||||
|
if hit is Vector3:
|
||||||
|
highest = maxf(highest, (hit as Vector3).y)
|
||||||
|
return highest
|
||||||
|
|
||||||
|
|
||||||
|
func _add_prop_collision(
|
||||||
|
prop: Node3D,
|
||||||
|
definition: TerrainPropDefinition,
|
||||||
|
) -> void:
|
||||||
|
if not definition.has_collision():
|
||||||
|
return
|
||||||
var body := StaticBody3D.new()
|
var body := StaticBody3D.new()
|
||||||
body.name = "TrunkCollision"
|
body.name = "TrunkCollision"
|
||||||
body.collision_layer = 1
|
body.collision_layer = 1
|
||||||
|
|
@ -373,44 +828,21 @@ func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
|
||||||
prop.add_child(body)
|
prop.add_child(body)
|
||||||
var collision := CollisionShape3D.new()
|
var collision := CollisionShape3D.new()
|
||||||
collision.name = "CollisionShape"
|
collision.name = "CollisionShape"
|
||||||
var shape := CylinderShape3D.new()
|
if definition.has_box_collision():
|
||||||
shape.radius = 0.5 if broad_tree else 0.35
|
var box := BoxShape3D.new()
|
||||||
shape.height = 4.2 if broad_tree else 4.8
|
box.size = definition.collision_box_size
|
||||||
collision.position.y = shape.height * 0.5
|
collision.shape = box
|
||||||
collision.shape = shape
|
collision.position = definition.collision_offset
|
||||||
body.add_child(collision)
|
|
||||||
|
|
||||||
|
|
||||||
func _cache_prop_source(source_name: String) -> void:
|
|
||||||
var source_root := PROP_SOURCE.instantiate()
|
|
||||||
var source := source_root.find_child(
|
|
||||||
source_name,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
) as MeshInstance3D
|
|
||||||
if source != null and source.mesh != null:
|
|
||||||
_source_meshes[source_name] = source.mesh
|
|
||||||
_source_bases[source_name] = source.transform.basis
|
|
||||||
_source_minimum_y[source_name] = _minimum_transformed_mesh_y(
|
|
||||||
source.mesh,
|
|
||||||
source.transform.basis,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
push_warning("Generated terrain prop source '%s' is unavailable." % source_name)
|
var cylinder := CylinderShape3D.new()
|
||||||
source_root.free()
|
cylinder.radius = definition.collision_radius
|
||||||
|
cylinder.height = definition.collision_height
|
||||||
|
collision.shape = cylinder
|
||||||
func _minimum_transformed_mesh_y(mesh: Mesh, basis: Basis) -> float:
|
collision.position = (
|
||||||
var bounds := mesh.get_aabb()
|
definition.collision_offset
|
||||||
var minimum_y := INF
|
+ Vector3.UP * cylinder.height * 0.5
|
||||||
for corner_index: int in 8:
|
|
||||||
var corner := bounds.position + Vector3(
|
|
||||||
bounds.size.x if (corner_index & 1) != 0 else 0.0,
|
|
||||||
bounds.size.y if (corner_index & 2) != 0 else 0.0,
|
|
||||||
bounds.size.z if (corner_index & 4) != 0 else 0.0,
|
|
||||||
)
|
)
|
||||||
minimum_y = minf(minimum_y, (basis * corner).y)
|
body.add_child(collision)
|
||||||
return minimum_y if minimum_y < INF else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _build_shoreline_reference() -> void:
|
func _build_shoreline_reference() -> void:
|
||||||
|
|
@ -492,15 +924,6 @@ func _append_triangle(
|
||||||
result.append(PackedVector3Array([a, b, c]))
|
result.append(PackedVector3Array([a, b, c]))
|
||||||
|
|
||||||
|
|
||||||
func _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
|
||||||
var result: Array[MeshInstance3D] = []
|
|
||||||
if root is MeshInstance3D:
|
|
||||||
result.append(root as MeshInstance3D)
|
|
||||||
for child: Node in root.get_children():
|
|
||||||
result.append_array(_collect_mesh_instances(child))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
func _clear_children(root: Node) -> void:
|
func _clear_children(root: Node) -> void:
|
||||||
for child: Node in root.get_children():
|
for child: Node in root.get_children():
|
||||||
root.remove_child(child)
|
root.remove_child(child)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=12 format=3]
|
[gd_scene load_steps=14 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
|
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
|
||||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
||||||
|
|
@ -11,9 +11,13 @@
|
||||||
[ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="9_anchors"]
|
[ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="9_anchors"]
|
||||||
[ext_resource type="Resource" path="res://fish/pools/starter_ocean_pool.tres" id="10_ocean_pool"]
|
[ext_resource type="Resource" path="res://fish/pools/starter_ocean_pool.tres" id="10_ocean_pool"]
|
||||||
[ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"]
|
[ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"]
|
||||||
|
|
||||||
[node name="GeneratedWorldRegion" type="Node3D"]
|
[node name="GeneratedWorldRegion" type="Node3D"]
|
||||||
script = ExtResource("1_region")
|
script = ExtResource("1_region")
|
||||||
|
prop_catalog = ExtResource("12_prop_catalog")
|
||||||
|
biome_catalog = ExtResource("13_biome_catalog")
|
||||||
region_id = &"generated_world"
|
region_id = &"generated_world"
|
||||||
fishable_water_root = NodePath("WaterBodies")
|
fishable_water_root = NodePath("WaterBodies")
|
||||||
water_recovery_root = NodePath("WaterBodies")
|
water_recovery_root = NodePath("WaterBodies")
|
||||||
|
|
@ -27,12 +31,12 @@ gatherable_anchor_root = NodePath("GatherableAnchors")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("2_generator")
|
script = ExtResource("2_generator")
|
||||||
catalog = ExtResource("3_catalog")
|
catalog = ExtResource("3_catalog")
|
||||||
grid_size = Vector2i(5, 5)
|
grid_size = Vector2i(7, 7)
|
||||||
generation_seed = 13001
|
generation_seed = 13001
|
||||||
generate_on_ready = false
|
generate_on_ready = false
|
||||||
build_collision = true
|
build_collision = true
|
||||||
force_center_chunk_id = &"chunk_spawn"
|
force_center_chunk_id = &"chunk_spawn"
|
||||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
|
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007")
|
||||||
required_chunk_weight_multiplier = 64.0
|
required_chunk_weight_multiplier = 64.0
|
||||||
maximum_backtracks = 100000
|
maximum_backtracks = 100000
|
||||||
|
|
||||||
|
|
|
||||||
BIN
world/generation/props/assets/prop_bridge.glb
Normal file
45
world/generation/props/assets/prop_bridge.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://d02hbwdie8qvm"
|
||||||
|
path="res://.godot/imported/prop_bridge.glb-f46c3958ff825fa424225306a13cf192.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_bridge.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_bridge.glb-f46c3958ff825fa424225306a13cf192.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
|
||||||
BIN
world/generation/props/assets/prop_dock.glb
Normal file
45
world/generation/props/assets/prop_dock.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://doq8pbpyvm6ue"
|
||||||
|
path="res://.godot/imported/prop_dock.glb-1bab9084db9428b6aab78b02d3957fdb.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_dock.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_dock.glb-1bab9084db9428b6aab78b02d3957fdb.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
|
||||||
BIN
world/generation/props/assets/prop_log.glb
Normal file
45
world/generation/props/assets/prop_log.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://tjvd2e6pu13v"
|
||||||
|
path="res://.godot/imported/prop_log.glb-2315521a55d44d3ed4d9bffda15d9534.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_log.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_log.glb-2315521a55d44d3ed4d9bffda15d9534.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
|
||||||
BIN
world/generation/props/assets/prop_log_post_1.glb
Normal file
45
world/generation/props/assets/prop_log_post_1.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bus7qaun1g63r"
|
||||||
|
path="res://.godot/imported/prop_log_post_1.glb-276ebe594961fac10b1b3228d38fdabf.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_log_post_1.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_log_post_1.glb-276ebe594961fac10b1b3228d38fdabf.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
|
||||||
BIN
world/generation/props/assets/prop_log_post_2.glb
Normal file
45
world/generation/props/assets/prop_log_post_2.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cb76u4p4xw0on"
|
||||||
|
path="res://.godot/imported/prop_log_post_2.glb-da62150ce947f75be274e2a5c99daa99.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_log_post_2.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_log_post_2.glb-da62150ce947f75be274e2a5c99daa99.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
|
||||||
BIN
world/generation/props/assets/prop_mushroom.glb
Normal file
45
world/generation/props/assets/prop_mushroom.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dpwkottav2wmg"
|
||||||
|
path="res://.godot/imported/prop_mushroom.glb-7427f0262b74e22f1f26f32e05833572.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_mushroom.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_mushroom.glb-7427f0262b74e22f1f26f32e05833572.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
|
||||||
BIN
world/generation/props/assets/prop_palm.glb
Normal file
45
world/generation/props/assets/prop_palm.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://fourevm3if2u"
|
||||||
|
path="res://.godot/imported/prop_palm.glb-bb3dfaeaead57bcc8695fc4152589ab2.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_palm.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_palm.glb-bb3dfaeaead57bcc8695fc4152589ab2.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
|
||||||
BIN
world/generation/props/assets/prop_pine.glb
Normal file
45
world/generation/props/assets/prop_pine.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://c7hw7rdlhy8rw"
|
||||||
|
path="res://.godot/imported/prop_pine.glb-372fa54a69ad2e1228537194c2ebb4b8.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_pine.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_pine.glb-372fa54a69ad2e1228537194c2ebb4b8.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
|
||||||
BIN
world/generation/props/assets/prop_timber_1.glb
Normal file
45
world/generation/props/assets/prop_timber_1.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bk5g0cbea3sr7"
|
||||||
|
path="res://.godot/imported/prop_timber_1.glb-c24976466df75b6076557ecf1717e207.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_timber_1.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_timber_1.glb-c24976466df75b6076557ecf1717e207.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
|
||||||
BIN
world/generation/props/assets/prop_timber_2.glb
Normal file
45
world/generation/props/assets/prop_timber_2.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bvjtbge6isr5m"
|
||||||
|
path="res://.godot/imported/prop_timber_2.glb-9d476f292645d06049bc0a83c82d7389.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_timber_2.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_timber_2.glb-9d476f292645d06049bc0a83c82d7389.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
|
||||||
BIN
world/generation/props/assets/prop_tree_1.glb
Normal file
45
world/generation/props/assets/prop_tree_1.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dgp6c7syxu0b4"
|
||||||
|
path="res://.godot/imported/prop_tree_1.glb-e6234d9deb054d47d88f248b79266362.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_tree_1.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_tree_1.glb-e6234d9deb054d47d88f248b79266362.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
|
||||||
BIN
world/generation/props/assets/prop_tree_2.glb
Normal file
45
world/generation/props/assets/prop_tree_2.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dt26eb0s5o7h1"
|
||||||
|
path="res://.godot/imported/prop_tree_2.glb-e0700c61f161e28a6cdb5966d1110da2.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_tree_2.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_tree_2.glb-e0700c61f161e28a6cdb5966d1110da2.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
|
||||||
BIN
world/generation/props/assets/prop_tree_3.glb
Normal file
45
world/generation/props/assets/prop_tree_3.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cprt30ic278ea"
|
||||||
|
path="res://.godot/imported/prop_tree_3.glb-57719668b78f614e94bfdab89d68d54d.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://world/generation/props/assets/prop_tree_3.glb"
|
||||||
|
dest_files=["res://.godot/imported/prop_tree_3.glb-57719668b78f614e94bfdab89d68d54d.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
|
||||||
11
world/generation/props/definitions/prop_bridge.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_bridge.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_bridge"
|
||||||
|
label = "small bridge"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 3.75
|
||||||
11
world/generation/props/definitions/prop_dock.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_dock.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_dock"
|
||||||
|
label = "dock"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 6.0
|
||||||
17
world/generation/props/definitions/prop_log.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_log.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_log"
|
||||||
|
label = "decorative log"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_detail"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 1.0
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 2.8
|
||||||
|
collision_box_size = Vector3(1.1, 0.9, 5.0)
|
||||||
|
collision_offset = Vector3(0, 0.45, 0)
|
||||||
11
world/generation/props/definitions/prop_log_post_1.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_log_post_1.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_log_post_1"
|
||||||
|
label = "large decorative post"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 0.5
|
||||||
11
world/generation/props/definitions/prop_log_post_2.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_log_post_2.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_log_post_2"
|
||||||
|
label = "small decorative post"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 0.35
|
||||||
18
world/generation/props/definitions/prop_mushroom.tres
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_mushroom.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_mushroom"
|
||||||
|
label = "mushrooms"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_detail"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 2.0
|
||||||
|
minimum_spawn_chunk_distance = 1
|
||||||
|
preferred_nearby_prop_groups = PackedStringArray("grass_tree")
|
||||||
|
preferred_nearby_radius = 12.0
|
||||||
|
nearby_preference_weight_multiplier = 3.0
|
||||||
|
clearance_radius = 0.8
|
||||||
16
world/generation/props/definitions/prop_palm.tres
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_palm.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_palm"
|
||||||
|
label = "palm"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"sand_tree"
|
||||||
|
required_chunk_tags = PackedStringArray("sand")
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 2.0
|
||||||
|
collision_radius = 0.4
|
||||||
|
collision_height = 6.0
|
||||||
18
world/generation/props/definitions/prop_pine.tres
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_pine.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_pine"
|
||||||
|
label = "pine"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_tree"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 0.7
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 1.55
|
||||||
|
collision_radius = 0.5
|
||||||
|
collision_height = 4.0
|
||||||
|
gatherable_anchor_height = 2.15
|
||||||
11
world/generation/props/definitions/prop_timber_1.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_timber_1.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_timber_1"
|
||||||
|
label = "large path timber"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 2.5
|
||||||
11
world/generation/props/definitions/prop_timber_2.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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_timber_2.glb" id="2_scene"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_timber_2"
|
||||||
|
label = "small path timber"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
clearance_radius = 2.5
|
||||||
17
world/generation/props/definitions/prop_tree_1.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_tree_1"
|
||||||
|
label = "small tree"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_tree"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 1.3
|
||||||
|
collision_radius = 0.4
|
||||||
|
collision_height = 3.2
|
||||||
|
gatherable_anchor_height = 2.15
|
||||||
17
world/generation/props/definitions/prop_tree_2.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_tree_2"
|
||||||
|
label = "medium tree"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_tree"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 1.5
|
||||||
|
collision_radius = 0.5
|
||||||
|
collision_height = 3.8
|
||||||
|
gatherable_anchor_height = 2.15
|
||||||
17
world/generation/props/definitions/prop_tree_3.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 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"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
stable_id = &"prop_tree_3"
|
||||||
|
label = "large tree"
|
||||||
|
packed_scene = ExtResource("2_scene")
|
||||||
|
procedural_group = &"grass_tree"
|
||||||
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
minimum_spawn_chunk_distance = 2
|
||||||
|
clearance_radius = 1.8
|
||||||
|
collision_radius = 0.55
|
||||||
|
collision_height = 4.2
|
||||||
|
gatherable_anchor_height = 2.15
|
||||||
21
world/generation/props/terrain_prop_catalog.tres
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=16 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_prop_catalog.gd" id="1_catalog"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_bridge.tres" id="2_bridge"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_dock.tres" id="3_dock"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log.tres" id="4_log"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log_post_1.tres" id="5_post_1"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log_post_2.tres" id="6_post_2"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_mushroom.tres" id="7_mushroom"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_palm.tres" id="8_palm"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_pine.tres" id="9_pine"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_timber_1.tres" id="10_timber_1"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_timber_2.tres" id="11_timber_2"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_1.tres" id="12_tree_1"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_2.tres" id="13_tree_2"]
|
||||||
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_3.tres" id="14_tree_3"]
|
||||||
|
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="15_definition_script"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_catalog")
|
||||||
|
definitions = Array[ExtResource("15_definition_script")]([ExtResource("2_bridge"), ExtResource("3_dock"), ExtResource("4_log"), ExtResource("5_post_1"), ExtResource("6_post_2"), ExtResource("7_mushroom"), ExtResource("8_palm"), ExtResource("9_pine"), ExtResource("10_timber_1"), ExtResource("11_timber_2"), ExtResource("12_tree_1"), ExtResource("13_tree_2"), ExtResource("14_tree_3")])
|
||||||
220
world/generation/terrain_biome_assigner.gd
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
class_name TerrainBiomeAssigner
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const BIOME_SEED_SALT := 0xB10E5
|
||||||
|
const SCORE_JITTER := 0.2
|
||||||
|
|
||||||
|
|
||||||
|
static func assign(
|
||||||
|
catalog: TerrainBiomeCatalog,
|
||||||
|
records: Array[Dictionary],
|
||||||
|
generation_seed: int,
|
||||||
|
) -> Dictionary[Vector2i, StringName]:
|
||||||
|
var assignments: Dictionary[Vector2i, StringName] = {}
|
||||||
|
if catalog == null:
|
||||||
|
return assignments
|
||||||
|
var records_by_signature: Dictionary[String, Array] = {}
|
||||||
|
var definitions_by_signature: Dictionary[String, Array] = {}
|
||||||
|
for record: Dictionary in records:
|
||||||
|
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
||||||
|
var definitions := catalog.definitions_for_tags(tags)
|
||||||
|
if definitions.is_empty():
|
||||||
|
continue
|
||||||
|
var signature := _definition_signature(definitions)
|
||||||
|
var grouped_records: Array = records_by_signature.get(signature, [])
|
||||||
|
grouped_records.append(record)
|
||||||
|
records_by_signature[signature] = grouped_records
|
||||||
|
definitions_by_signature[signature] = definitions
|
||||||
|
|
||||||
|
var signatures := PackedStringArray()
|
||||||
|
for signature_value: Variant in records_by_signature:
|
||||||
|
signatures.append(str(signature_value))
|
||||||
|
signatures.sort()
|
||||||
|
for signature: String in signatures:
|
||||||
|
var grouped_records: Array[Dictionary] = []
|
||||||
|
grouped_records.assign(records_by_signature[signature])
|
||||||
|
grouped_records.sort_custom(_record_less_than)
|
||||||
|
var definitions: Array[TerrainBiomeDefinition] = []
|
||||||
|
definitions.assign(definitions_by_signature[signature])
|
||||||
|
if definitions.size() == 1:
|
||||||
|
for record: Dictionary in grouped_records:
|
||||||
|
assignments[record.get("coordinate", Vector2i.ZERO)] = (
|
||||||
|
definitions[0].stable_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
_assign_clustered_group(
|
||||||
|
assignments,
|
||||||
|
grouped_records,
|
||||||
|
definitions,
|
||||||
|
generation_seed,
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
return assignments
|
||||||
|
|
||||||
|
|
||||||
|
static func counts(
|
||||||
|
assignments: Dictionary[Vector2i, StringName],
|
||||||
|
) -> Dictionary[StringName, int]:
|
||||||
|
var result: Dictionary[StringName, int] = {}
|
||||||
|
for biome_id: StringName in assignments.values():
|
||||||
|
result[biome_id] = result.get(biome_id, 0) + 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func fingerprint(
|
||||||
|
assignments: Dictionary[Vector2i, StringName],
|
||||||
|
) -> String:
|
||||||
|
var coordinates: Array[Vector2i] = []
|
||||||
|
coordinates.assign(assignments.keys())
|
||||||
|
coordinates.sort_custom(_coordinate_less_than)
|
||||||
|
var tokens := PackedStringArray()
|
||||||
|
for coordinate: Vector2i in coordinates:
|
||||||
|
tokens.append(
|
||||||
|
"%d,%d:%s"
|
||||||
|
% [coordinate.x, coordinate.y, assignments[coordinate]]
|
||||||
|
)
|
||||||
|
return "\n".join(tokens).sha256_text()
|
||||||
|
|
||||||
|
|
||||||
|
static func _assign_clustered_group(
|
||||||
|
assignments: Dictionary[Vector2i, StringName],
|
||||||
|
records: Array[Dictionary],
|
||||||
|
definitions: Array[TerrainBiomeDefinition],
|
||||||
|
generation_seed: int,
|
||||||
|
signature: String,
|
||||||
|
) -> void:
|
||||||
|
var anchors := _build_anchors(
|
||||||
|
records,
|
||||||
|
definitions,
|
||||||
|
generation_seed,
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
for record: Dictionary in records:
|
||||||
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||||
|
var best_definition: TerrainBiomeDefinition
|
||||||
|
var best_score := INF
|
||||||
|
for definition: TerrainBiomeDefinition in definitions:
|
||||||
|
var anchor: Vector2i = anchors.get(
|
||||||
|
definition.stable_id,
|
||||||
|
coordinate,
|
||||||
|
)
|
||||||
|
var delta := coordinate - anchor
|
||||||
|
var score := (
|
||||||
|
float(delta.length_squared())
|
||||||
|
/ maxf(definition.region_weight, 0.1)
|
||||||
|
+ _score_jitter(
|
||||||
|
generation_seed,
|
||||||
|
coordinate,
|
||||||
|
definition.stable_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if score < best_score:
|
||||||
|
best_score = score
|
||||||
|
best_definition = definition
|
||||||
|
if best_definition != null:
|
||||||
|
assignments[coordinate] = best_definition.stable_id
|
||||||
|
|
||||||
|
|
||||||
|
static func _build_anchors(
|
||||||
|
records: Array[Dictionary],
|
||||||
|
definitions: Array[TerrainBiomeDefinition],
|
||||||
|
generation_seed: int,
|
||||||
|
signature: String,
|
||||||
|
) -> Dictionary[StringName, Vector2i]:
|
||||||
|
var anchors: Dictionary[StringName, Vector2i] = {}
|
||||||
|
var available_coordinates: Array[Vector2i] = []
|
||||||
|
for record: Dictionary in records:
|
||||||
|
available_coordinates.append(
|
||||||
|
record.get("coordinate", Vector2i.ZERO)
|
||||||
|
)
|
||||||
|
var random := RandomNumberGenerator.new()
|
||||||
|
random.seed = (
|
||||||
|
generation_seed
|
||||||
|
^ int(signature.hash())
|
||||||
|
^ BIOME_SEED_SALT
|
||||||
|
)
|
||||||
|
for definition: TerrainBiomeDefinition in definitions:
|
||||||
|
if available_coordinates.is_empty():
|
||||||
|
for record: Dictionary in records:
|
||||||
|
available_coordinates.append(
|
||||||
|
record.get("coordinate", Vector2i.ZERO)
|
||||||
|
)
|
||||||
|
var selected_index := 0
|
||||||
|
if anchors.is_empty():
|
||||||
|
selected_index = random.randi_range(
|
||||||
|
0,
|
||||||
|
available_coordinates.size() - 1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
selected_index = _most_separated_coordinate_index(
|
||||||
|
available_coordinates,
|
||||||
|
anchors.values(),
|
||||||
|
random,
|
||||||
|
)
|
||||||
|
anchors[definition.stable_id] = available_coordinates[selected_index]
|
||||||
|
available_coordinates.remove_at(selected_index)
|
||||||
|
return anchors
|
||||||
|
|
||||||
|
|
||||||
|
static func _most_separated_coordinate_index(
|
||||||
|
coordinates: Array[Vector2i],
|
||||||
|
anchor_values: Array,
|
||||||
|
random: RandomNumberGenerator,
|
||||||
|
) -> int:
|
||||||
|
var best_distance := -1
|
||||||
|
var best_indices: Array[int] = []
|
||||||
|
for index: int in coordinates.size():
|
||||||
|
var coordinate := coordinates[index]
|
||||||
|
var nearest_distance := 1 << 30
|
||||||
|
for anchor_value: Variant in anchor_values:
|
||||||
|
var anchor: Vector2i = anchor_value
|
||||||
|
nearest_distance = mini(
|
||||||
|
nearest_distance,
|
||||||
|
(coordinate - anchor).length_squared(),
|
||||||
|
)
|
||||||
|
if nearest_distance > best_distance:
|
||||||
|
best_distance = nearest_distance
|
||||||
|
best_indices = [index]
|
||||||
|
elif nearest_distance == best_distance:
|
||||||
|
best_indices.append(index)
|
||||||
|
return best_indices[random.randi_range(0, best_indices.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
static func _score_jitter(
|
||||||
|
generation_seed: int,
|
||||||
|
coordinate: Vector2i,
|
||||||
|
biome_id: StringName,
|
||||||
|
) -> float:
|
||||||
|
var token := "%d:%d:%d:%s" % [
|
||||||
|
generation_seed,
|
||||||
|
coordinate.x,
|
||||||
|
coordinate.y,
|
||||||
|
biome_id,
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
float(posmod(token.hash(), 1000))
|
||||||
|
/ 1000.0
|
||||||
|
* SCORE_JITTER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _definition_signature(
|
||||||
|
definitions: Array[TerrainBiomeDefinition],
|
||||||
|
) -> String:
|
||||||
|
var ids := PackedStringArray()
|
||||||
|
for definition: TerrainBiomeDefinition in definitions:
|
||||||
|
ids.append(String(definition.stable_id))
|
||||||
|
return "|".join(ids)
|
||||||
|
|
||||||
|
|
||||||
|
static func _record_less_than(first: Dictionary, second: Dictionary) -> bool:
|
||||||
|
return _coordinate_less_than(
|
||||||
|
first.get("coordinate", Vector2i.ZERO),
|
||||||
|
second.get("coordinate", Vector2i.ZERO),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _coordinate_less_than(first: Vector2i, second: Vector2i) -> bool:
|
||||||
|
if first.y == second.y:
|
||||||
|
return first.x < second.x
|
||||||
|
return first.y < second.y
|
||||||
1
world/generation/terrain_biome_assigner.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c3fsddgk0ksdq
|
||||||
74
world/generation/terrain_biome_catalog.gd
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
class_name TerrainBiomeCatalog
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var definitions: Array[TerrainBiomeDefinition] = []
|
||||||
|
|
||||||
|
|
||||||
|
func definition_for_id(stable_id: StringName) -> TerrainBiomeDefinition:
|
||||||
|
for definition: TerrainBiomeDefinition in definitions:
|
||||||
|
if definition != null and definition.stable_id == stable_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func definitions_for_tags(
|
||||||
|
chunk_tags: PackedStringArray,
|
||||||
|
) -> Array[TerrainBiomeDefinition]:
|
||||||
|
var result: Array[TerrainBiomeDefinition] = []
|
||||||
|
for definition: TerrainBiomeDefinition in definitions:
|
||||||
|
if definition != null and definition.supports_chunk_tags(chunk_tags):
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func validation_errors() -> PackedStringArray:
|
||||||
|
var errors := PackedStringArray()
|
||||||
|
var seen_ids: Dictionary[StringName, bool] = {}
|
||||||
|
for index: int in definitions.size():
|
||||||
|
var definition := definitions[index]
|
||||||
|
if definition == null:
|
||||||
|
errors.append("Biome definition %d is empty." % index)
|
||||||
|
continue
|
||||||
|
if definition.stable_id == &"":
|
||||||
|
errors.append("Biome definition %d has no stable ID." % index)
|
||||||
|
elif seen_ids.has(definition.stable_id):
|
||||||
|
errors.append("Biome ID %s is duplicated." % definition.stable_id)
|
||||||
|
else:
|
||||||
|
seen_ids[definition.stable_id] = true
|
||||||
|
if definition.required_chunk_tags.is_empty():
|
||||||
|
errors.append(
|
||||||
|
"%s has no required chunk tags." % definition.stable_id
|
||||||
|
)
|
||||||
|
for required_tag: String in definition.required_chunk_tags:
|
||||||
|
if required_tag in definition.forbidden_chunk_tags:
|
||||||
|
errors.append(
|
||||||
|
"%s both requires and forbids chunk tag '%s'."
|
||||||
|
% [definition.stable_id, required_tag]
|
||||||
|
)
|
||||||
|
var seen_groups: Dictionary[StringName, bool] = {}
|
||||||
|
for rule_index: int in definition.prop_rules.size():
|
||||||
|
var rule := definition.prop_rules[rule_index]
|
||||||
|
if rule == null:
|
||||||
|
errors.append(
|
||||||
|
"%s prop rule %d is empty."
|
||||||
|
% [definition.stable_id, rule_index]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if rule.procedural_group == &"":
|
||||||
|
errors.append(
|
||||||
|
"%s prop rule %d has no group."
|
||||||
|
% [definition.stable_id, rule_index]
|
||||||
|
)
|
||||||
|
elif seen_groups.has(rule.procedural_group):
|
||||||
|
errors.append(
|
||||||
|
"%s duplicates prop group %s."
|
||||||
|
% [definition.stable_id, rule.procedural_group]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_groups[rule.procedural_group] = true
|
||||||
|
if rule.minimum_placements > 0 and rule.placement_chance <= 0:
|
||||||
|
errors.append(
|
||||||
|
"%s requires %s props but gives them no placement chance."
|
||||||
|
% [definition.stable_id, rule.procedural_group]
|
||||||
|
)
|
||||||
|
return errors
|
||||||
1
world/generation/terrain_biome_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://jope30oqqt4
|
||||||
27
world/generation/terrain_biome_definition.gd
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
class_name TerrainBiomeDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var stable_id: StringName
|
||||||
|
@export var label := ""
|
||||||
|
@export var required_chunk_tags := PackedStringArray()
|
||||||
|
@export var forbidden_chunk_tags := PackedStringArray()
|
||||||
|
## Larger values give this biome more territory around its seeded anchor.
|
||||||
|
@export_range(0.1, 10.0, 0.1) var region_weight := 1.0
|
||||||
|
@export var prop_rules: Array[TerrainBiomePropRule] = []
|
||||||
|
|
||||||
|
|
||||||
|
func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool:
|
||||||
|
for required_tag: String in required_chunk_tags:
|
||||||
|
if required_tag not in chunk_tags:
|
||||||
|
return false
|
||||||
|
for forbidden_tag: String in forbidden_chunk_tags:
|
||||||
|
if forbidden_tag in chunk_tags:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func prop_rule_for_group(group: StringName) -> TerrainBiomePropRule:
|
||||||
|
for rule: TerrainBiomePropRule in prop_rules:
|
||||||
|
if rule != null and rule.procedural_group == group:
|
||||||
|
return rule
|
||||||
|
return null
|
||||||
1
world/generation/terrain_biome_definition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://g43mwisr1g3x
|
||||||
21
world/generation/terrain_biome_prop_rule.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
class_name TerrainBiomePropRule
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var procedural_group: StringName
|
||||||
|
@export_range(0, 10000, 1) var placement_chance := 0
|
||||||
|
@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
|
||||||
|
## Empty permits every compatible prop in the procedural group.
|
||||||
|
@export var allowed_prop_ids := PackedStringArray()
|
||||||
|
|
||||||
|
|
||||||
|
func allows_prop(definition: TerrainPropDefinition) -> bool:
|
||||||
|
return (
|
||||||
|
definition != null
|
||||||
|
and definition.procedural_group == procedural_group
|
||||||
|
and (
|
||||||
|
allowed_prop_ids.is_empty()
|
||||||
|
or String(definition.stable_id) in allowed_prop_ids
|
||||||
|
)
|
||||||
|
)
|
||||||
1
world/generation/terrain_biome_prop_rule.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bmsdftvupugx5
|
||||||
|
|
@ -32,6 +32,18 @@ func validation_errors() -> PackedStringArray:
|
||||||
errors.append("%s has no primary mesh name." % definition.stable_id)
|
errors.append("%s has no primary mesh name." % definition.stable_id)
|
||||||
if definition.allowed_rotation_mask == 0:
|
if definition.allowed_rotation_mask == 0:
|
||||||
errors.append("%s allows no rotations." % definition.stable_id)
|
errors.append("%s allows no rotations." % definition.stable_id)
|
||||||
|
var has_coast := "coast" in definition.tags
|
||||||
|
var has_ocean_edge := definition.ocean_facing_edges != 0
|
||||||
|
if has_coast and not has_ocean_edge:
|
||||||
|
errors.append(
|
||||||
|
"%s is coastal but has no ocean-facing edge."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
elif not has_coast and has_ocean_edge:
|
||||||
|
errors.append(
|
||||||
|
"%s has an ocean-facing edge without the coast tag."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
var has_fresh_water := "fresh_water" in definition.tags
|
var has_fresh_water := "fresh_water" in definition.tags
|
||||||
var has_water_footprint := (
|
var has_water_footprint := (
|
||||||
definition.water_surface_size.x > 0.0
|
definition.water_surface_size.x > 0.0
|
||||||
|
|
@ -47,4 +59,37 @@ func validation_errors() -> PackedStringArray:
|
||||||
"%s has a water footprint without the freshwater tag."
|
"%s has a water footprint without the freshwater tag."
|
||||||
% definition.stable_id
|
% definition.stable_id
|
||||||
)
|
)
|
||||||
|
for allowed_tag: String in definition.allowed_neighbor_tags:
|
||||||
|
if allowed_tag in definition.forbidden_neighbor_tags:
|
||||||
|
errors.append(
|
||||||
|
(
|
||||||
|
"%s both allows and forbids neighbor tag '%s'."
|
||||||
|
% [definition.stable_id, allowed_tag]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
var edge := edge_value as TerrainChunkTopology.Edge
|
||||||
|
for allowed_tag: String in (
|
||||||
|
definition.directional_allowed_neighbor_tags(edge)
|
||||||
|
):
|
||||||
|
if allowed_tag not in definition.forbidden_neighbor_tags:
|
||||||
|
continue
|
||||||
|
errors.append(
|
||||||
|
(
|
||||||
|
"%s both allows and forbids neighbor tag '%s' on its %s edge."
|
||||||
|
% [
|
||||||
|
definition.stable_id,
|
||||||
|
allowed_tag,
|
||||||
|
TerrainChunkTopology.edge_name(edge),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
definition.minimum_required_neighbors > 0
|
||||||
|
and definition.required_neighbor_tags.is_empty()
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"%s requires neighbors but defines no required neighbor tags."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,38 @@ var allowed_rotation_mask := 15
|
||||||
## Negative values allow unlimited placements; zero disables random placement.
|
## Negative values allow unlimited placements; zero disables random placement.
|
||||||
@export_range(-1, 1024, 1) var maximum_placements := -1
|
@export_range(-1, 1024, 1) var maximum_placements := -1
|
||||||
@export var tags := PackedStringArray()
|
@export var tags := PackedStringArray()
|
||||||
|
## Empty permits any non-water neighbor. Otherwise at least one listed tag
|
||||||
|
## must be present on the neighboring chunk. Water connectors are governed by
|
||||||
|
## their inlet/outlet topology instead.
|
||||||
|
@export var allowed_neighbor_tags := PackedStringArray()
|
||||||
|
## These tags always reject a non-water neighbor, even when it also carries an
|
||||||
|
## allowed tag.
|
||||||
|
@export var forbidden_neighbor_tags := PackedStringArray()
|
||||||
|
## Optional canonical per-edge overrides for allowed_neighbor_tags. Empty means
|
||||||
|
## the edge inherits the chunk-wide list. These describe the visible surface at
|
||||||
|
## a seam, such as a grass-backed coastline transition that must not meet sand.
|
||||||
|
@export_group("Directional Neighbor Rules")
|
||||||
|
@export var north_allowed_neighbor_tags := PackedStringArray()
|
||||||
|
@export var east_allowed_neighbor_tags := PackedStringArray()
|
||||||
|
@export var south_allowed_neighbor_tags := PackedStringArray()
|
||||||
|
@export var west_allowed_neighbor_tags := PackedStringArray()
|
||||||
|
@export_group("")
|
||||||
|
## When non-empty, at least this many in-grid cardinal neighbors must carry one
|
||||||
|
## of the listed tags. This supports statements such as "three sides around the
|
||||||
|
## spawn must be safe grass" without hard-coding a particular chunk ID.
|
||||||
|
@export var required_neighbor_tags := PackedStringArray()
|
||||||
|
@export_range(0, 4, 1) var minimum_required_neighbors := 0
|
||||||
|
## Matching already-placed neighbors increase this definition's selection
|
||||||
|
## weight. This shapes regions without turning a visual preference into a hard
|
||||||
|
## generation constraint.
|
||||||
|
@export var preferred_neighbor_tags := PackedStringArray()
|
||||||
|
## Coastal transition pieces should normally migrate toward the generated
|
||||||
|
## region's perimeter while remaining legal in the interior.
|
||||||
|
@export var prefers_map_boundary := false
|
||||||
|
## Edges that descend from land into the surrounding ocean. Every rotated edge
|
||||||
|
## in this mask must face outside the generated grid; it may never meet another
|
||||||
|
## terrain chunk.
|
||||||
|
@export_flags("North", "East", "South", "West") var ocean_facing_edges := 0
|
||||||
@export_flags("North", "East", "South", "West") var water_inlet_edges := 0
|
@export_flags("North", "East", "South", "West") var water_inlet_edges := 0
|
||||||
@export_flags("North", "East", "South", "West") var water_outlet_edges := 0
|
@export_flags("North", "East", "South", "West") var water_outlet_edges := 0
|
||||||
## Optional generated water footprint in the chunk's unrotated local X/Z plane.
|
## Optional generated water footprint in the chunk's unrotated local X/Z plane.
|
||||||
|
|
@ -22,3 +54,70 @@ var allowed_rotation_mask := 15
|
||||||
func allows_quarter_turn(quarter_turns: int) -> bool:
|
func allows_quarter_turn(quarter_turns: int) -> bool:
|
||||||
var normalized_turns := posmod(quarter_turns, 4)
|
var normalized_turns := posmod(quarter_turns, 4)
|
||||||
return (allowed_rotation_mask & (1 << normalized_turns)) != 0
|
return (allowed_rotation_mask & (1 << normalized_turns)) != 0
|
||||||
|
|
||||||
|
|
||||||
|
func allows_non_water_neighbor(neighbor: TerrainChunkDefinition) -> bool:
|
||||||
|
return _allows_neighbor_with_tags(neighbor, allowed_neighbor_tags)
|
||||||
|
|
||||||
|
|
||||||
|
func allows_non_water_neighbor_on_edge(
|
||||||
|
neighbor: TerrainChunkDefinition,
|
||||||
|
edge: TerrainChunkTopology.Edge,
|
||||||
|
) -> bool:
|
||||||
|
return _allows_neighbor_with_tags(
|
||||||
|
neighbor,
|
||||||
|
allowed_neighbor_tags_for_edge(edge),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func allowed_neighbor_tags_for_edge(
|
||||||
|
edge: TerrainChunkTopology.Edge,
|
||||||
|
) -> PackedStringArray:
|
||||||
|
var edge_tags := directional_allowed_neighbor_tags(edge)
|
||||||
|
return allowed_neighbor_tags if edge_tags.is_empty() else edge_tags
|
||||||
|
|
||||||
|
|
||||||
|
func directional_allowed_neighbor_tags(
|
||||||
|
edge: TerrainChunkTopology.Edge,
|
||||||
|
) -> PackedStringArray:
|
||||||
|
match edge:
|
||||||
|
TerrainChunkTopology.Edge.NORTH:
|
||||||
|
return north_allowed_neighbor_tags
|
||||||
|
TerrainChunkTopology.Edge.EAST:
|
||||||
|
return east_allowed_neighbor_tags
|
||||||
|
TerrainChunkTopology.Edge.SOUTH:
|
||||||
|
return south_allowed_neighbor_tags
|
||||||
|
TerrainChunkTopology.Edge.WEST:
|
||||||
|
return west_allowed_neighbor_tags
|
||||||
|
return PackedStringArray()
|
||||||
|
|
||||||
|
|
||||||
|
func _allows_neighbor_with_tags(
|
||||||
|
neighbor: TerrainChunkDefinition,
|
||||||
|
allowed_tags: PackedStringArray,
|
||||||
|
) -> bool:
|
||||||
|
if neighbor == null:
|
||||||
|
return false
|
||||||
|
for forbidden_tag: String in forbidden_neighbor_tags:
|
||||||
|
if forbidden_tag in neighbor.tags:
|
||||||
|
return false
|
||||||
|
if allowed_tags.is_empty():
|
||||||
|
return true
|
||||||
|
return has_any_tag(neighbor.tags, allowed_tags)
|
||||||
|
|
||||||
|
|
||||||
|
func prefers_neighbor(neighbor: TerrainChunkDefinition) -> bool:
|
||||||
|
return (
|
||||||
|
neighbor != null
|
||||||
|
and has_any_tag(neighbor.tags, preferred_neighbor_tags)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func has_any_tag(
|
||||||
|
available_tags: PackedStringArray,
|
||||||
|
requested_tags: PackedStringArray,
|
||||||
|
) -> bool:
|
||||||
|
for requested_tag: String in requested_tags:
|
||||||
|
if requested_tag in available_tags:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,19 @@ static func edge_normal(edge: Edge) -> Vector3:
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
static func grid_offset(edge: Edge) -> Vector2i:
|
||||||
|
match edge:
|
||||||
|
Edge.NORTH:
|
||||||
|
return Vector2i.UP
|
||||||
|
Edge.EAST:
|
||||||
|
return Vector2i.RIGHT
|
||||||
|
Edge.SOUTH:
|
||||||
|
return Vector2i.DOWN
|
||||||
|
Edge.WEST:
|
||||||
|
return Vector2i.LEFT
|
||||||
|
return Vector2i.ZERO
|
||||||
|
|
||||||
|
|
||||||
static func rotated_edge(edge: Edge, quarter_turns: int) -> Edge:
|
static func rotated_edge(edge: Edge, quarter_turns: int) -> Edge:
|
||||||
var angle := float(posmod(quarter_turns, 4)) * PI * 0.5
|
var angle := float(posmod(quarter_turns, 4)) * PI * 0.5
|
||||||
var normal := edge_normal(edge).rotated(Vector3.UP, angle)
|
var normal := edge_normal(edge).rotated(Vector3.UP, angle)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,23 @@ func stable_key() -> String:
|
||||||
return "%s@%d" % [definition.stable_id, posmod(quarter_turns, 4)]
|
return "%s@%d" % [definition.stable_id, posmod(quarter_turns, 4)]
|
||||||
|
|
||||||
|
|
||||||
|
func source_edge(edge: TerrainChunkTopology.Edge) -> TerrainChunkTopology.Edge:
|
||||||
|
return TerrainChunkTopology.rotated_edge(edge, -quarter_turns)
|
||||||
|
|
||||||
|
|
||||||
|
func allows_non_water_neighbor_on_edge(
|
||||||
|
neighbor: TerrainChunkVariant,
|
||||||
|
edge: TerrainChunkTopology.Edge,
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
neighbor != null
|
||||||
|
and definition.allows_non_water_neighbor_on_edge(
|
||||||
|
neighbor.definition,
|
||||||
|
source_edge(edge),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func topology_signature(quantization: float) -> String:
|
func topology_signature(quantization: float) -> String:
|
||||||
var tokens := PackedStringArray()
|
var tokens := PackedStringArray()
|
||||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
|
@ -26,6 +43,28 @@ func topology_signature(quantization: float) -> String:
|
||||||
return "|".join(tokens)
|
return "|".join(tokens)
|
||||||
|
|
||||||
|
|
||||||
|
func constraint_signature(quantization: float) -> String:
|
||||||
|
return "%s|neighbors:%s|ocean:%d|in:%d|out:%d" % [
|
||||||
|
topology_signature(quantization),
|
||||||
|
_neighbor_rule_signature(),
|
||||||
|
rotated_edge_mask(definition.ocean_facing_edges),
|
||||||
|
rotated_edge_mask(definition.water_inlet_edges),
|
||||||
|
rotated_edge_mask(definition.water_outlet_edges),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _neighbor_rule_signature() -> String:
|
||||||
|
var edge_tokens := PackedStringArray()
|
||||||
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
var edge := edge_value as TerrainChunkTopology.Edge
|
||||||
|
var tags: PackedStringArray = definition.allowed_neighbor_tags_for_edge(
|
||||||
|
source_edge(edge)
|
||||||
|
).duplicate()
|
||||||
|
tags.sort()
|
||||||
|
edge_tokens.append(",".join(tags))
|
||||||
|
return "/".join(edge_tokens)
|
||||||
|
|
||||||
|
|
||||||
func rotated_edge_mask(source_mask: int) -> int:
|
func rotated_edge_mask(source_mask: int) -> int:
|
||||||
var result := 0
|
var result := 0
|
||||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||||
|
|
|
||||||
85
world/generation/terrain_prop_catalog.gd
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
class_name TerrainPropCatalog
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var definitions: Array[TerrainPropDefinition] = []
|
||||||
|
|
||||||
|
|
||||||
|
func definition_for_id(stable_id: StringName) -> TerrainPropDefinition:
|
||||||
|
for definition: TerrainPropDefinition in definitions:
|
||||||
|
if definition != null and definition.stable_id == stable_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func procedural_definitions(
|
||||||
|
group: StringName,
|
||||||
|
chunk_tags: PackedStringArray,
|
||||||
|
) -> Array[TerrainPropDefinition]:
|
||||||
|
var result: Array[TerrainPropDefinition] = []
|
||||||
|
for definition: TerrainPropDefinition in definitions:
|
||||||
|
if (
|
||||||
|
definition != null
|
||||||
|
and definition.procedural_group == group
|
||||||
|
and definition.supports_chunk_tags(chunk_tags)
|
||||||
|
):
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func validation_errors() -> PackedStringArray:
|
||||||
|
var errors := PackedStringArray()
|
||||||
|
var seen_ids: Dictionary[StringName, bool] = {}
|
||||||
|
for index: int in definitions.size():
|
||||||
|
var definition := definitions[index]
|
||||||
|
if definition == null:
|
||||||
|
errors.append("Prop definition %d is empty." % index)
|
||||||
|
continue
|
||||||
|
if definition.stable_id == &"":
|
||||||
|
errors.append("Prop definition %d has no stable ID." % index)
|
||||||
|
elif seen_ids.has(definition.stable_id):
|
||||||
|
errors.append("Prop ID %s is duplicated." % definition.stable_id)
|
||||||
|
else:
|
||||||
|
seen_ids[definition.stable_id] = true
|
||||||
|
if definition.packed_scene == null:
|
||||||
|
errors.append("%s has no PackedScene." % definition.stable_id)
|
||||||
|
if (
|
||||||
|
definition.is_procedural()
|
||||||
|
and definition.required_chunk_tags.is_empty()
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"%s is procedural but has no required chunk tags."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not definition.preferred_nearby_prop_groups.is_empty()
|
||||||
|
and definition.preferred_nearby_radius <= 0.0
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"%s prefers nearby props but defines no search radius."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
var has_collision_radius := definition.collision_radius > 0.0
|
||||||
|
var has_collision_height := definition.collision_height > 0.0
|
||||||
|
if has_collision_radius != has_collision_height:
|
||||||
|
errors.append(
|
||||||
|
"%s must define both collision radius and height."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
definition.has_cylinder_collision()
|
||||||
|
and definition.has_box_collision()
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"%s defines more than one collision shape."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
definition.collision_box_size.x < 0.0
|
||||||
|
or definition.collision_box_size.y < 0.0
|
||||||
|
or definition.collision_box_size.z < 0.0
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"%s has a negative box collision dimension."
|
||||||
|
% definition.stable_id
|
||||||
|
)
|
||||||
|
return errors
|
||||||