Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
1733 lines
57 KiB
GDScript
1733 lines
57 KiB
GDScript
extends SceneTree
|
|
|
|
const RegionScene: PackedScene = preload(
|
|
"res://world/generation/generated_world_region.tscn"
|
|
)
|
|
const GeneratedPondPool: FishPool = preload(
|
|
"res://fish/pools/generated_pond_pool.tres"
|
|
)
|
|
const GeneratedLakePool: FishPool = preload(
|
|
"res://fish/pools/generated_lake_pool.tres"
|
|
)
|
|
const GeneratedRiverPool: FishPool = preload(
|
|
"res://fish/pools/generated_river_pool.tres"
|
|
)
|
|
const PrecipitationOcclusionType = preload(
|
|
"res://world/environment/precipitation_occlusion.gd"
|
|
)
|
|
const FIRST_SEED := 13001
|
|
const SECOND_SEED := 13002
|
|
const RIVER_FALLBACK_SEED := 13012
|
|
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_pine_large",
|
|
&"prop_timber_1",
|
|
&"prop_timber_2",
|
|
&"prop_tree_1",
|
|
&"prop_tree_2",
|
|
&"prop_tree_3",
|
|
&"prop_tree_large",
|
|
]
|
|
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_pine_large",
|
|
&"prop_tree_1",
|
|
&"prop_tree_2",
|
|
&"prop_tree_3",
|
|
&"prop_tree_large",
|
|
]
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred(&"_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var region := RegionScene.instantiate() as GeneratedWorldRegion
|
|
assert(region != null)
|
|
var configured_generator := region.get_node(
|
|
"Terrain/TerrainChunkGenerator"
|
|
) as TerrainChunkGenerator
|
|
assert(configured_generator != null)
|
|
configured_generator.elevated_cliff_third_level_chance = 1.0
|
|
configured_generator.elevated_cliff_third_tier_base_chance = 0.0
|
|
configured_generator.elevated_cliff_third_tier_stack_chance = 1.0
|
|
configured_generator.elevated_cliff_double_third_tier_chance = 0.0
|
|
root.add_child(region)
|
|
await process_frame
|
|
assert(region.generate_world(FIRST_SEED))
|
|
await physics_frame
|
|
|
|
var generator := region.get_node(
|
|
"Terrain/TerrainChunkGenerator"
|
|
) as TerrainChunkGenerator
|
|
assert(generator != null)
|
|
assert(region.get_generation_seed() == FIRST_SEED)
|
|
_validate_generated_region(region, generator)
|
|
|
|
var first_layout := generator.placement_keys()
|
|
var first_stacked_layout := generator.stacked_elevated_placement_keys()
|
|
var first_biomes := _biome_signature(region, generator)
|
|
var first_decorations := _decoration_signature(region)
|
|
assert(region.generate_world(FIRST_SEED))
|
|
assert(generator.placement_keys() == first_layout)
|
|
assert(_biome_signature(region, generator) == first_biomes)
|
|
assert(_decoration_signature(region) == first_decorations)
|
|
generator._elevated_feature_center = Vector2i(-1, -1)
|
|
assert(generator.generate_from_placement_keys(first_layout))
|
|
assert(generator.stacked_elevated_placement_keys() == first_stacked_layout)
|
|
|
|
configured_generator.elevated_cliff_third_tier_base_chance = 1.0
|
|
configured_generator.elevated_cliff_third_tier_stack_chance = 0.0
|
|
assert(region.generate_world(SECOND_SEED))
|
|
await process_frame
|
|
await physics_frame
|
|
await physics_frame
|
|
_validate_generated_region(region, generator)
|
|
assert(generator.placement_keys() != first_layout)
|
|
assert(_biome_signature(region, generator) != first_biomes)
|
|
var second_layout := generator.placement_keys()
|
|
|
|
configured_generator.elevated_cliff_double_third_tier_chance = 1.0
|
|
assert(region.generate_world(RIVER_FALLBACK_SEED))
|
|
await process_frame
|
|
await physics_frame
|
|
await physics_frame
|
|
_validate_generated_region(region, generator)
|
|
assert(generator.placement_keys() != first_layout)
|
|
assert(generator.placement_keys() != second_layout)
|
|
assert(generator._river_feature_candidate_index >= 0)
|
|
|
|
region.queue_free()
|
|
await process_frame
|
|
print("Generated world runtime validation: PASS")
|
|
quit()
|
|
|
|
|
|
func _validate_generated_region(
|
|
region: GeneratedWorldRegion,
|
|
generator: TerrainChunkGenerator,
|
|
) -> void:
|
|
var placements := generator.placement_keys()
|
|
var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
|
|
var center := Vector2i(
|
|
generator.grid_size.x / 2,
|
|
generator.grid_size.y / 2,
|
|
)
|
|
var center_index := center.y * generator.grid_size.x + center.x
|
|
assert(generator.grid_size == Vector2i(20, 20))
|
|
assert(generator._lake_feature_footprint_size.x >= 7)
|
|
assert(
|
|
generator._lake_feature_footprint_size.x
|
|
== generator._lake_feature_footprint_size.y
|
|
)
|
|
assert(maxi(
|
|
generator._river_feature_footprint_size.x,
|
|
generator._river_feature_footprint_size.y,
|
|
) >= 4)
|
|
assert(mini(
|
|
generator._river_feature_footprint_size.x,
|
|
generator._river_feature_footprint_size.y,
|
|
) >= 4)
|
|
assert(generator._river_feature_flow_direction in [
|
|
Vector2i.RIGHT,
|
|
Vector2i.DOWN,
|
|
Vector2i.LEFT,
|
|
Vector2i.UP,
|
|
])
|
|
assert(generator._river_feature_outlet_direction in [
|
|
Vector2i.RIGHT,
|
|
Vector2i.DOWN,
|
|
Vector2i.LEFT,
|
|
Vector2i.UP,
|
|
])
|
|
assert(
|
|
generator._river_feature_flow_direction.x
|
|
* generator._river_feature_outlet_direction.x
|
|
+ generator._river_feature_flow_direction.y
|
|
* generator._river_feature_outlet_direction.y
|
|
== 0
|
|
)
|
|
assert(generator.elevated_cliff_coastal_feature_required)
|
|
assert(placements.size() == expected_chunk_count)
|
|
assert(placements[center_index].begins_with("chunk_spawn@"))
|
|
assert(_placement_count(placements, "chunk_spawn") == 1)
|
|
assert(_placement_count(placements, "chunk_0001") >= 1)
|
|
var stream_count := (
|
|
_placement_count(placements, "chunk_0003")
|
|
+ _placement_count(placements, "chunk_0016")
|
|
)
|
|
assert(stream_count >= 1 and stream_count <= 3)
|
|
assert(_placement_count(placements, "chunk_0004") == 2)
|
|
var beach_count := _placement_count(placements, "chunk_0002")
|
|
var grass_coast_count := _placement_count(placements, "chunk_0005")
|
|
var grass_corner_count := _placement_count(placements, "chunk_0007")
|
|
var beach_corner_count := _placement_count(placements, "chunk_0008")
|
|
var has_secondary_elevation := (
|
|
generator._secondary_elevated_feature_center.x >= 0
|
|
)
|
|
var has_third_tier_base := (
|
|
_placement_count(placements, "chunk_0028") > 0
|
|
)
|
|
var coastline_edge_count := (
|
|
2 * (generator.grid_size.x - 2)
|
|
+ 2 * (generator.grid_size.y - 2)
|
|
)
|
|
assert(
|
|
beach_count
|
|
== coastline_edge_count - (4 if has_secondary_elevation else 0) - 2
|
|
and grass_coast_count == 0
|
|
)
|
|
assert(
|
|
beach_corner_count == (3 if has_secondary_elevation else 4)
|
|
and grass_corner_count == 0
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0009")
|
|
== (
|
|
(1 if has_secondary_elevation else 0)
|
|
+ (0 if has_third_tier_base else 9)
|
|
)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0010")
|
|
== (
|
|
(1 if has_secondary_elevation else 0)
|
|
+ (0 if has_third_tier_base else 4)
|
|
)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0011")
|
|
+ _placement_count(placements, "chunk_0012")
|
|
== (
|
|
(2 if has_secondary_elevation else 0)
|
|
+ (0 if has_third_tier_base else 12)
|
|
)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0012")
|
|
== (
|
|
(1 if has_secondary_elevation else 0)
|
|
+ (0 if has_third_tier_base else 1)
|
|
)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0028")
|
|
== (9 if has_third_tier_base else 0)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0027")
|
|
== (4 if has_third_tier_base else 0)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0026")
|
|
== (10 if has_third_tier_base else 0)
|
|
)
|
|
assert(_placement_count(placements, "chunk_0029") == 1)
|
|
assert(_placement_count(placements, "chunk_0030") == 1)
|
|
assert(_placement_count(placements, "chunk_0031") == 1)
|
|
assert(_placement_count(placements, "chunk_0032") == 1)
|
|
assert(_placement_count(placements, "chunk_0033") == 1)
|
|
assert(_placement_count(placements, "chunk_0034") == 1)
|
|
assert(_placement_count(placements, "chunk_0035") == 1)
|
|
assert(_placement_count(placements, "chunk_0036") == 1)
|
|
assert(_placement_count(placements, "chunk_0038") == 2)
|
|
assert(
|
|
_placement_count(placements, "chunk_0014")
|
|
== (2 if has_secondary_elevation else 0)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_0015")
|
|
== (1 if has_secondary_elevation else 0)
|
|
)
|
|
assert(_placement_count(placements, "chunk_0018") == 0)
|
|
assert(_placement_count(placements, "chunk_0020") == 4)
|
|
assert(_placement_count(placements, "chunk_0022") == 4)
|
|
assert(_placement_count(placements, "chunk_0023") == 4)
|
|
var lake_edge_count := _placement_count(placements, "chunk_0017")
|
|
var lake_edge_variant_count := _placement_count(placements, "chunk_0037")
|
|
var lake_fill_count := _placement_count(placements, "chunk_0019")
|
|
var lake_narrow_count := _placement_count(placements, "chunk_0021")
|
|
var lake_size := generator._lake_feature_footprint_size.x
|
|
assert(lake_size in [7, 8])
|
|
assert(lake_fill_count == (lake_size - 2) * (lake_size - 2))
|
|
assert(lake_edge_count + lake_edge_variant_count == (lake_size - 6) * 4)
|
|
assert(lake_edge_count > 0)
|
|
assert(lake_edge_variant_count > 0)
|
|
assert(lake_narrow_count == 8)
|
|
assert(_placement_count(placements, "chunk_9999") == 0)
|
|
assert(_placement_count(placements, "chunk_9998") == 0)
|
|
assert(
|
|
_placement_count(placements, "chunk_9997")
|
|
== (1 if has_secondary_elevation else 0)
|
|
)
|
|
assert(
|
|
_placement_count(placements, "chunk_9996")
|
|
== (1 if has_secondary_elevation else 0)
|
|
)
|
|
assert(_placement_count(placements, "chunk_9995") == 0)
|
|
var river_coordinate_count := generator._river_feature_coordinates.size()
|
|
var river_edge_count := (
|
|
_placement_count(placements, "chunk_0024")
|
|
+ _placement_count(placements, "chunk_0025")
|
|
+ _placement_count(placements, "chunk_0038")
|
|
)
|
|
assert(river_coordinate_count >= 12)
|
|
assert(generator._river_feature_placements.size() == river_coordinate_count)
|
|
assert(river_edge_count == river_coordinate_count - 8)
|
|
var stone_bridge_specs: Array[Dictionary] = []
|
|
for spec: Dictionary in generator._river_feature_placements:
|
|
if spec.get("id", &"") == &"chunk_0038":
|
|
stone_bridge_specs.append(spec)
|
|
assert(stone_bridge_specs.size() == 2)
|
|
var first_bridge_coordinate := stone_bridge_specs[0].get(
|
|
"coordinate",
|
|
Vector2i(-1, -1),
|
|
) as Vector2i
|
|
var second_bridge_coordinate := stone_bridge_specs[1].get(
|
|
"coordinate",
|
|
Vector2i(-1, -1),
|
|
) as Vector2i
|
|
var bridge_delta := second_bridge_coordinate - first_bridge_coordinate
|
|
assert(absi(bridge_delta.x) + absi(bridge_delta.y) == 1)
|
|
assert(posmod(
|
|
int(stone_bridge_specs[1].get("turns", -1))
|
|
- int(stone_bridge_specs[0].get("turns", -1)),
|
|
4,
|
|
) == 2)
|
|
var river_source_coordinates := generator._river_source_coordinates(
|
|
generator._river_feature_origin,
|
|
generator._river_feature_footprint_size,
|
|
generator._river_feature_flow_direction,
|
|
)
|
|
assert(river_source_coordinates.size() == 2)
|
|
for source_coordinate: Vector2i in river_source_coordinates:
|
|
var source_index := (
|
|
source_coordinate.y * generator.grid_size.x + source_coordinate.x
|
|
)
|
|
assert(
|
|
placements[source_index].begins_with("chunk_0029@")
|
|
or placements[source_index].begins_with("chunk_0030@")
|
|
)
|
|
var source_offset := source_coordinate - generator._elevated_feature_center
|
|
assert(
|
|
maxi(absi(source_offset.x), absi(source_offset.y))
|
|
== TerrainChunkGenerator.ELEVATED_FEATURE_RADIUS
|
|
)
|
|
var outlet_coordinates: Array[Vector2i] = []
|
|
for index: int in placements.size():
|
|
if (
|
|
placements[index].begins_with("chunk_0031@")
|
|
or placements[index].begins_with("chunk_0032@")
|
|
):
|
|
var outlet_coordinate := Vector2i(
|
|
index % generator.grid_size.x,
|
|
index / generator.grid_size.x,
|
|
)
|
|
outlet_coordinates.append(outlet_coordinate)
|
|
assert(
|
|
not generator._coordinate_is_inside_grid(
|
|
outlet_coordinate
|
|
+ generator._river_feature_outlet_direction
|
|
)
|
|
)
|
|
assert(outlet_coordinates.size() == 2)
|
|
var outlet_delta := outlet_coordinates[1] - outlet_coordinates[0]
|
|
assert(absi(outlet_delta.x) + absi(outlet_delta.y) == 1)
|
|
for beach_coordinate: Vector2i in [
|
|
outlet_coordinates[0] - outlet_delta,
|
|
outlet_coordinates[1] + outlet_delta,
|
|
]:
|
|
assert(generator._coordinate_is_inside_grid(beach_coordinate))
|
|
assert(generator._coordinate_is_on_boundary(beach_coordinate))
|
|
var beach_index := (
|
|
beach_coordinate.y * generator.grid_size.x + beach_coordinate.x
|
|
)
|
|
assert(placements[beach_index].begins_with("chunk_0002@"))
|
|
for index: int in placements.size():
|
|
var coordinate := Vector2i(
|
|
index % generator.grid_size.x,
|
|
index / generator.grid_size.x,
|
|
)
|
|
if placements[index].begins_with("chunk_0001@"):
|
|
assert(generator._distance_from_map_boundary(coordinate) <= 1)
|
|
elif placements[index].begins_with("chunk_0013@"):
|
|
assert(generator._distance_from_map_boundary(coordinate) <= 2)
|
|
assert(_placement_count(placements, "chunk_0013") >= 1)
|
|
assert(
|
|
generator.get_generated_chunks_root().get_child_count()
|
|
== expected_chunk_count
|
|
)
|
|
var generated_root := generator.get_generated_chunks_root()
|
|
var terrain_collision_batches := generated_root.find_children(
|
|
"TerrainCollisionBatch_*",
|
|
"StaticBody3D",
|
|
true,
|
|
false,
|
|
)
|
|
assert(not terrain_collision_batches.is_empty())
|
|
assert(
|
|
terrain_collision_batches.size()
|
|
<= ceili(float(generator.grid_size.x) / generator.collision_batch_size)
|
|
* ceili(float(generator.grid_size.y) / generator.collision_batch_size)
|
|
)
|
|
assert(
|
|
generated_root.find_children(
|
|
"TerrainCollision",
|
|
"StaticBody3D",
|
|
true,
|
|
false,
|
|
).is_empty()
|
|
)
|
|
assert(
|
|
generator.get_primary_terrain_meshes().size()
|
|
== (
|
|
expected_chunk_count
|
|
+ generator.stacked_elevated_placement_keys().size()
|
|
)
|
|
)
|
|
_validate_elevated_cliff_feature(generator)
|
|
|
|
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
|
|
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
|
|
assert(shop != null and shop.has_node("Shopkeeper"))
|
|
assert(storage != null and storage.has_node("InteractionArea"))
|
|
var spawn_chunk_center := generator.chunk_position(center)
|
|
var shop_offset := shop.position - spawn_chunk_center
|
|
var storage_offset := storage.position - spawn_chunk_center
|
|
assert(absf(shop_offset.x) < 5.0 and absf(shop_offset.z) < 5.0)
|
|
assert(absf(storage_offset.x) < 5.0 and absf(storage_offset.z) < 5.0)
|
|
assert(region.get_fishing_shop() != null)
|
|
assert(region.get_player_storage() != null)
|
|
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 fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
|
|
assert(ocean != null and fresh_root != null)
|
|
assert(ocean.water_type == WaterType.Type.SALT_WATER)
|
|
assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT))
|
|
var ocean_recovery := ocean.get_node("RecoveryRegion") as PlayerWaterTrigger
|
|
assert(
|
|
ocean_recovery.entry_height_reference
|
|
== PlayerWaterTrigger.EntryHeightReference.BODY_CENTER
|
|
)
|
|
assert(is_equal_approx(ocean_recovery.entry_depth_threshold, 0.35))
|
|
var fresh_placement_count := 0
|
|
var river_placement_count := 0
|
|
var polygon_sizes_by_coordinate: Dictionary[Vector2i, int] = {}
|
|
for record: Dictionary in generator.placement_records():
|
|
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
|
if "coast" in tags:
|
|
_validate_ocean_facing_record(record, generator.grid_size)
|
|
if "fresh_water" in tags:
|
|
fresh_placement_count += 1
|
|
if "river" in tags:
|
|
river_placement_count += 1
|
|
assert(not "pond" in tags)
|
|
assert(not "lake" in tags)
|
|
var surface_polygon: PackedVector2Array = record.get(
|
|
"water_surface_polygon",
|
|
PackedVector2Array(),
|
|
)
|
|
if surface_polygon.size() >= 3:
|
|
polygon_sizes_by_coordinate[
|
|
record.get("coordinate", Vector2i.ZERO)
|
|
] = surface_polygon.size()
|
|
_validate_complete_coastline(generator)
|
|
assert(fresh_root.get_child_count() == fresh_placement_count)
|
|
assert(river_placement_count == river_coordinate_count)
|
|
assert(
|
|
polygon_sizes_by_coordinate.size()
|
|
== 20 + river_placement_count
|
|
)
|
|
var river_body_count := 0
|
|
for child: Node in fresh_root.get_children():
|
|
var fresh := child as WaterBodyAuthoring
|
|
assert(fresh != null)
|
|
assert(fresh.water_type == WaterType.Type.FRESH_WATER)
|
|
assert(is_equal_approx(fresh.position.y, GeneratedWorldRegion.WATER_HEIGHT))
|
|
assert(fresh.visible)
|
|
assert(fresh.surface_size.x <= 10.0 and fresh.surface_size.y <= 10.0)
|
|
assert(not fresh.visual_surface_enabled)
|
|
assert(not (fresh.get_node("VisualWater") as MeshInstance3D).visible)
|
|
var generated_bed := fresh.get_node_or_null(
|
|
"GeneratedBed",
|
|
) as MeshInstance3D
|
|
assert(generated_bed != null and generated_bed.mesh != null)
|
|
assert(
|
|
is_equal_approx(
|
|
generated_bed.global_position.y,
|
|
GeneratedWorldRegion.GENERATED_FRESH_WATER_BED_HEIGHT,
|
|
)
|
|
)
|
|
assert(
|
|
generated_bed.material_override != null
|
|
and generated_bed.material_override.resource_name == "dirt"
|
|
)
|
|
var fresh_recovery := (
|
|
fresh.get_node("RecoveryRegion") as PlayerWaterTrigger
|
|
)
|
|
assert(
|
|
fresh_recovery.entry_height_reference
|
|
== PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN
|
|
)
|
|
assert(is_equal_approx(fresh_recovery.entry_depth_threshold, 0.1))
|
|
assert(is_equal_approx(
|
|
fresh_recovery.entry_confirmation_seconds,
|
|
0.2,
|
|
))
|
|
var coordinate_tokens := child.name.trim_prefix("FreshWater_").split("_")
|
|
assert(coordinate_tokens.size() == 2)
|
|
var coordinate := Vector2i(
|
|
int(coordinate_tokens[0]),
|
|
int(coordinate_tokens[1]),
|
|
)
|
|
if polygon_sizes_by_coordinate.has(coordinate):
|
|
assert(
|
|
fresh.surface_polygon.size()
|
|
== polygon_sizes_by_coordinate[coordinate]
|
|
)
|
|
assert(
|
|
fresh.get_node("FishingRegion").get_child_count() > 1
|
|
)
|
|
else:
|
|
assert(fresh.surface_polygon.is_empty())
|
|
if &"river" in fresh.location_tags:
|
|
river_body_count += 1
|
|
assert(fresh.fish_pool == GeneratedRiverPool)
|
|
elif &"lake" in fresh.location_tags:
|
|
assert(fresh.fish_pool == GeneratedLakePool)
|
|
else:
|
|
assert(&"pond" in fresh.location_tags)
|
|
assert(fresh.fish_pool == GeneratedPondPool)
|
|
assert(river_body_count == river_placement_count)
|
|
|
|
_validate_authored_chunk_surfaces(region, generator)
|
|
_validate_projected_terrain_materials(
|
|
generator.get_generated_chunks_root()
|
|
)
|
|
|
|
var decorations := region.get_node("Decorations") as Node3D
|
|
var grass_prop_surface_triangles := region.get_spawn_surface_triangles(
|
|
[&"grass_lite"],
|
|
-INF,
|
|
0.35,
|
|
)
|
|
var sand_prop_surface_triangles := region.get_spawn_surface_triangles(
|
|
[&"sand"],
|
|
-INF,
|
|
0.35,
|
|
)
|
|
_validate_water_recovery_positions(
|
|
region,
|
|
grass_prop_surface_triangles,
|
|
sand_prop_surface_triangles,
|
|
)
|
|
var anchors := region.get_node(
|
|
"GatherableAnchors/ReachableTreeTrunks"
|
|
) as GatherableAnchorSet3D
|
|
assert(decorations != null and decorations.get_child_count() > 0)
|
|
assert(anchors != null)
|
|
assert(anchors.get_spawn_positions().size() > 0)
|
|
_validate_tree_gatherable_anchors(region, decorations, anchors)
|
|
var palm_clusters: Dictionary[int, Array] = {}
|
|
var tree_scales: Array[float] = []
|
|
var tree_yaws: Dictionary[float, bool] = {}
|
|
var ocean_edges_by_coordinate: Dictionary[Vector2i, int] = {}
|
|
for record: Dictionary in generator.placement_records():
|
|
var record_edges := int(record.get("ocean_facing_edges", 0))
|
|
if record_edges != 0:
|
|
ocean_edges_by_coordinate[
|
|
record.get("coordinate", Vector2i.ZERO)
|
|
] = record_edges
|
|
for child: Node in decorations.get_children():
|
|
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
|
assert(PROCEDURAL_PROP_IDS.has(prop_id))
|
|
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
|
assert(definition != null)
|
|
if definition.procedural_group in [&"grass_tree", &"sand_tree"]:
|
|
assert(
|
|
_has_precipitation_occlusion_layer(child),
|
|
"Precipitation occlusion layer missing from %s (%s)."
|
|
% [child.name, prop_id],
|
|
)
|
|
var biome_id := StringName(
|
|
child.get_meta(&"terrain_biome_id", &"")
|
|
)
|
|
var biome := region.get_biome_catalog().definition_for_id(biome_id)
|
|
assert(biome != null)
|
|
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 spawn_coordinate := Vector2i(
|
|
generator.grid_size.x / 2,
|
|
generator.grid_size.y / 2,
|
|
)
|
|
var spawn_distance := (
|
|
absi(coordinate.x - spawn_coordinate.x)
|
|
+ absi(coordinate.y - spawn_coordinate.y)
|
|
)
|
|
assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
|
|
_validate_decoration_transform(
|
|
region,
|
|
child as Node3D,
|
|
definition,
|
|
grass_prop_surface_triangles,
|
|
sand_prop_surface_triangles,
|
|
)
|
|
if prop_id == &"prop_palm":
|
|
var cluster_id := int(
|
|
child.get_meta(&"terrain_prop_cluster_id", -1)
|
|
)
|
|
var members: Array = palm_clusters.get(cluster_id, [])
|
|
members.append(child)
|
|
palm_clusters[cluster_id] = members
|
|
var ocean_direction := _ocean_direction(
|
|
ocean_edges_by_coordinate.get(coordinate, 0)
|
|
)
|
|
if ocean_direction.is_zero_approx():
|
|
ocean_direction = _nearest_ocean_direction(
|
|
region,
|
|
generator,
|
|
(child as Node3D).position,
|
|
)
|
|
var local_overhang := Vector3(
|
|
definition.local_overhang_direction.x,
|
|
0.0,
|
|
definition.local_overhang_direction.y,
|
|
).normalized()
|
|
var actual_overhang := local_overhang.rotated(
|
|
Vector3.UP,
|
|
(child as Node3D).rotation.y,
|
|
)
|
|
assert(
|
|
actual_overhang.dot(ocean_direction)
|
|
>= cos(deg_to_rad(definition.ocean_facing_spread_degrees))
|
|
- 0.001
|
|
)
|
|
elif prop_id in [
|
|
&"prop_tree_1",
|
|
&"prop_tree_2",
|
|
&"prop_tree_3",
|
|
&"prop_tree_large",
|
|
&"prop_pine",
|
|
&"prop_pine_large",
|
|
]:
|
|
tree_scales.append(
|
|
float(child.get_meta(&"terrain_prop_visual_scale", 1.0))
|
|
)
|
|
tree_yaws[snappedf((child as Node3D).rotation.y, 0.01)] = true
|
|
if prop_id in [
|
|
&"prop_tree_1",
|
|
&"prop_tree_2",
|
|
&"prop_tree_3",
|
|
&"prop_tree_large",
|
|
]:
|
|
assert(
|
|
_has_material_variant_override(
|
|
child.get_node_or_null("Visual"),
|
|
definition.material_variants,
|
|
)
|
|
)
|
|
assert(
|
|
_has_material_variant_override(
|
|
child.get_node_or_null("Visual"),
|
|
definition.secondary_material_variants,
|
|
)
|
|
)
|
|
assert(not palm_clusters.is_empty())
|
|
for members: Array in palm_clusters.values():
|
|
assert(members.size() >= 1 and members.size() <= 3)
|
|
for first_index: int in members.size():
|
|
for second_index: int in range(first_index + 1, members.size()):
|
|
var first := members[first_index] as Node3D
|
|
var second := members[second_index] as Node3D
|
|
var separation := Vector2(
|
|
first.position.x - second.position.x,
|
|
first.position.z - second.position.z,
|
|
).length()
|
|
assert(separation > 0.5 and separation < 6.1)
|
|
assert(tree_scales.max() - tree_scales.min() > 0.25)
|
|
assert(tree_yaws.size() >= 8)
|
|
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",
|
|
) >= 4
|
|
)
|
|
assert(
|
|
_decoration_biome_group_count(
|
|
region,
|
|
&"biome_pine_forest",
|
|
&"grass_tree",
|
|
) >= 4
|
|
)
|
|
|
|
|
|
func _validate_elevated_cliff_feature(
|
|
generator: TerrainChunkGenerator,
|
|
) -> void:
|
|
var elevated_coordinates: Dictionary[Vector2i, StringName] = {}
|
|
var top_coordinates: Array[Vector2i] = []
|
|
var ramp_records: Array[Dictionary] = []
|
|
var coastal_ids: Array[StringName] = [
|
|
&"chunk_0014",
|
|
&"chunk_0015",
|
|
&"chunk_9999",
|
|
&"chunk_9998",
|
|
&"chunk_9997",
|
|
&"chunk_9996",
|
|
]
|
|
var has_coastal_feature := false
|
|
for record: Dictionary in generator.placement_records():
|
|
var stable_id: StringName = record.get("stable_id", &"")
|
|
if stable_id not in [
|
|
&"chunk_0009",
|
|
&"chunk_0010",
|
|
&"chunk_0011",
|
|
&"chunk_0012",
|
|
&"chunk_0026",
|
|
&"chunk_0027",
|
|
&"chunk_0028",
|
|
&"chunk_0029",
|
|
&"chunk_0030",
|
|
&"chunk_0014",
|
|
&"chunk_0015",
|
|
&"chunk_9999",
|
|
&"chunk_9998",
|
|
&"chunk_9997",
|
|
&"chunk_9996",
|
|
]:
|
|
continue
|
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
|
if stable_id in coastal_ids:
|
|
has_coastal_feature = true
|
|
assert(
|
|
coordinate.x in [0, generator.grid_size.x - 1]
|
|
or coordinate.y in [0, generator.grid_size.y - 1]
|
|
)
|
|
else:
|
|
assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1)
|
|
assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1)
|
|
elevated_coordinates[coordinate] = stable_id
|
|
if stable_id in [&"chunk_0009", &"chunk_0028"]:
|
|
top_coordinates.append(coordinate)
|
|
elif stable_id == &"chunk_0012":
|
|
ramp_records.append(record)
|
|
assert(elevated_coordinates.size() == (34 if has_coastal_feature else 25))
|
|
var feature_center := generator._elevated_feature_center
|
|
assert(feature_center != Vector2i(-1, -1))
|
|
var center_index := feature_center.y * generator.grid_size.x + feature_center.x
|
|
assert(center_index >= 0 and center_index < generator.placement_keys().size())
|
|
var primary_top_id := StringName(
|
|
generator.placement_keys()[center_index].get_slice("@", 0)
|
|
)
|
|
assert(primary_top_id in [&"chunk_0009", &"chunk_0028"])
|
|
var primary_is_third_tier := primary_top_id == &"chunk_0028"
|
|
assert(
|
|
top_coordinates.size()
|
|
== (9 + (1 if has_coastal_feature else 0))
|
|
)
|
|
assert(
|
|
ramp_records.size()
|
|
== (
|
|
(0 if primary_is_third_tier else 1)
|
|
+ (1 if has_coastal_feature else 0)
|
|
)
|
|
)
|
|
var detected_feature_center := Vector2i(-1, -1)
|
|
for candidate: Vector2i in top_coordinates:
|
|
var neighboring_top_count := 0
|
|
for row_offset: int in range(-1, 2):
|
|
for column_offset: int in range(-1, 2):
|
|
neighboring_top_count += int(
|
|
elevated_coordinates.get(
|
|
candidate + Vector2i(column_offset, row_offset),
|
|
&"",
|
|
) == primary_top_id
|
|
)
|
|
if neighboring_top_count == 9:
|
|
detected_feature_center = candidate
|
|
break
|
|
assert(detected_feature_center == feature_center)
|
|
for row_offset: int in range(-2, 3):
|
|
for column_offset: int in range(-2, 3):
|
|
assert(
|
|
elevated_coordinates.has(
|
|
feature_center + Vector2i(column_offset, row_offset)
|
|
)
|
|
)
|
|
if has_coastal_feature:
|
|
var secondary_top := generator._secondary_elevated_feature_center
|
|
assert(secondary_top != Vector2i(-1, -1))
|
|
for row_offset: int in range(-1, 2):
|
|
for column_offset: int in range(-1, 2):
|
|
assert(
|
|
elevated_coordinates.has(
|
|
secondary_top + Vector2i(column_offset, row_offset)
|
|
)
|
|
)
|
|
var found_primary_ramp := false
|
|
for ramp_record: Dictionary in ramp_records:
|
|
var ramp_coordinate: Vector2i = ramp_record.get(
|
|
"coordinate", Vector2i(-1, -1)
|
|
)
|
|
var ramp_turns := int(ramp_record.get("rotation_quarters", 0))
|
|
var high_edge := TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.WEST,
|
|
ramp_turns,
|
|
)
|
|
var high_offset := TerrainChunkTopology.grid_offset(high_edge)
|
|
assert(
|
|
elevated_coordinates.get(ramp_coordinate + high_offset, &"")
|
|
== &"chunk_0009"
|
|
)
|
|
found_primary_ramp = (
|
|
found_primary_ramp
|
|
or elevated_coordinates.get(
|
|
ramp_coordinate + high_offset * 2,
|
|
&"",
|
|
) == &"chunk_0009"
|
|
)
|
|
var low_coordinate := ramp_coordinate - high_offset
|
|
var low_index := (
|
|
low_coordinate.y * generator.grid_size.x + low_coordinate.x
|
|
)
|
|
assert(
|
|
generator.placement_keys()[low_index].begins_with("chunk_0000@")
|
|
or generator.placement_keys()[low_index].begins_with("chunk_spawn@")
|
|
)
|
|
assert(found_primary_ramp == not primary_is_third_tier)
|
|
|
|
var generated := generator.get_generated_chunks_root()
|
|
assert(generated != null)
|
|
var layered_count := 0
|
|
for chunk_root: Node in generated.get_children():
|
|
var stable_id := StringName(
|
|
chunk_root.get_meta(&"terrain_chunk_id", &"")
|
|
)
|
|
if stable_id not in [
|
|
&"chunk_0009",
|
|
&"chunk_0010",
|
|
&"chunk_0011",
|
|
&"chunk_0012",
|
|
&"chunk_0026",
|
|
&"chunk_0027",
|
|
&"chunk_0028",
|
|
&"chunk_0029",
|
|
&"chunk_0030",
|
|
&"chunk_0014",
|
|
&"chunk_0015",
|
|
&"chunk_9999",
|
|
&"chunk_9998",
|
|
&"chunk_9997",
|
|
&"chunk_9996",
|
|
]:
|
|
continue
|
|
if stable_id in [&"chunk_0014", &"chunk_0015"]:
|
|
assert(chunk_root.get_node_or_null("TerrainBaseLayer") == null)
|
|
continue
|
|
if stable_id in [&"chunk_0029", &"chunk_0030"]:
|
|
assert(chunk_root.get_node_or_null("TerrainBaseLayer") == null)
|
|
var source_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
chunk_root,
|
|
stable_id,
|
|
)
|
|
assert(source_mesh != null and source_mesh.mesh != null)
|
|
continue
|
|
layered_count += 1
|
|
var base_layer := chunk_root.get_node_or_null("TerrainBaseLayer")
|
|
var overlay := chunk_root.get_node_or_null("TerrainOverlay")
|
|
assert(base_layer != null and overlay != null)
|
|
var transition_base_mesh := &"chunk_0000"
|
|
if stable_id in [&"chunk_9999", &"chunk_9998"]:
|
|
transition_base_mesh = &"chunk_0007"
|
|
elif stable_id in [&"chunk_9997", &"chunk_9996"]:
|
|
transition_base_mesh = &"chunk_0008"
|
|
var base_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
base_layer,
|
|
transition_base_mesh,
|
|
)
|
|
var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
overlay,
|
|
(
|
|
&"chunk_0010"
|
|
if stable_id in [
|
|
&"chunk_9999",
|
|
&"chunk_9998",
|
|
&"chunk_9997",
|
|
&"chunk_9996",
|
|
]
|
|
else stable_id
|
|
),
|
|
)
|
|
assert(base_mesh != null and base_mesh.mesh != null)
|
|
assert(overlay_mesh != null and overlay_mesh.mesh != null)
|
|
assert(layered_count == (29 if has_coastal_feature else 23))
|
|
var stacked_keys := generator.stacked_elevated_placement_keys()
|
|
assert(stacked_keys.size() in [0, 4])
|
|
if stacked_keys.is_empty():
|
|
return
|
|
for key: String in stacked_keys:
|
|
assert(
|
|
key.begins_with("chunk_0010@")
|
|
or key.begins_with("chunk_0027@")
|
|
)
|
|
var stacked_count := 0
|
|
for chunk_root: Node in generated.get_children():
|
|
for child: Node in chunk_root.get_children():
|
|
if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
|
|
continue
|
|
stacked_count += 1
|
|
var stacked_id := StringName(
|
|
child.get_meta(&"terrain_chunk_id", &"")
|
|
)
|
|
assert(stacked_id in [&"chunk_0010", &"chunk_0027"])
|
|
var expected_height := (
|
|
generator.elevated_cliff_level_height * 2.0
|
|
if primary_is_third_tier
|
|
else generator.elevated_cliff_level_height
|
|
)
|
|
assert(
|
|
is_equal_approx(
|
|
(child as Node3D).position.y,
|
|
expected_height,
|
|
)
|
|
)
|
|
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
|
child,
|
|
stacked_id,
|
|
)
|
|
assert(stacked_mesh != null and stacked_mesh.mesh != null)
|
|
assert(stacked_count == 4)
|
|
|
|
|
|
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_complete_coastline(generator: TerrainChunkGenerator) -> void:
|
|
for record: Dictionary in generator.placement_records():
|
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
|
var ocean_edges := int(record.get("ocean_facing_edges", 0))
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
var outside := (
|
|
neighbor.x < 0
|
|
or neighbor.y < 0
|
|
or neighbor.x >= generator.grid_size.x
|
|
or neighbor.y >= generator.grid_size.y
|
|
)
|
|
assert(
|
|
((ocean_edges & (1 << edge_value)) != 0) == outside
|
|
)
|
|
|
|
|
|
func _ocean_direction(ocean_edges: int) -> Vector3:
|
|
var result := Vector3.ZERO
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
if (ocean_edges & (1 << edge_value)) != 0:
|
|
result += TerrainChunkTopology.edge_normal(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
return result.normalized()
|
|
|
|
|
|
func _nearest_ocean_direction(
|
|
region: GeneratedWorldRegion,
|
|
generator: TerrainChunkGenerator,
|
|
position: Vector3,
|
|
) -> Vector3:
|
|
var half_extents := region.get_playable_half_extents()
|
|
var distance_x := half_extents.x - absf(position.x)
|
|
var distance_z := half_extents.y - absf(position.z)
|
|
var direction_x := Vector3.RIGHT if position.x >= 0.0 else Vector3.LEFT
|
|
var direction_z := Vector3.BACK if position.z >= 0.0 else Vector3.FORWARD
|
|
if absf(distance_x - distance_z) <= generator.catalog.chunk_size * 0.35:
|
|
return (direction_x + direction_z).normalized()
|
|
return direction_x if distance_x < distance_z else direction_z
|
|
|
|
|
|
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()
|
|
if definition.has_gatherable_surface():
|
|
assert(
|
|
definition.gatherable_surface_clearance >= 0.12
|
|
)
|
|
var mushroom := catalog.definition_for_id(&"prop_mushroom")
|
|
assert(mushroom != null and mushroom.has_collision())
|
|
assert(mushroom.visual_offset.y < 0.0)
|
|
for tree_id: StringName in [
|
|
&"prop_tree_1",
|
|
&"prop_tree_2",
|
|
&"prop_tree_3",
|
|
&"prop_tree_large",
|
|
]:
|
|
var tree := catalog.definition_for_id(tree_id)
|
|
assert(tree != null)
|
|
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
|
|
assert(tree.material_variants.size() >= 3)
|
|
assert(not tree.variant_material_slot_names.is_empty())
|
|
assert(tree.secondary_material_variants.size() >= 3)
|
|
assert(not tree.secondary_variant_material_slot_names.is_empty())
|
|
var pine := catalog.definition_for_id(&"prop_pine")
|
|
assert(pine != null)
|
|
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
|
|
assert(pine.material_variants.is_empty())
|
|
assert(not pine.has_gatherable_surface())
|
|
var large_pine := catalog.definition_for_id(&"prop_pine_large")
|
|
assert(large_pine != null)
|
|
assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale)
|
|
assert(large_pine.material_variants.is_empty())
|
|
assert(not large_pine.has_gatherable_surface())
|
|
var palm := catalog.definition_for_id(&"prop_palm")
|
|
assert(palm != null)
|
|
assert(is_equal_approx(palm.minimum_visual_scale, 0.5))
|
|
assert(is_equal_approx(palm.maximum_visual_scale, 1.0))
|
|
assert(palm.minimum_cluster_size == 2)
|
|
assert(palm.maximum_cluster_size == 3)
|
|
assert(palm.minimum_cluster_radius > palm.clearance_radius)
|
|
assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius)
|
|
assert(palm.prefer_ocean_facing)
|
|
assert(not palm.local_overhang_direction.is_zero_approx())
|
|
var aligned_tree_collisions: Dictionary[StringName, Vector2] = {
|
|
&"prop_tree_1": Vector2(0.08, 0.01),
|
|
&"prop_tree_2": Vector2(0.1, 0.075),
|
|
&"prop_tree_3": Vector2(-0.1, 0.18),
|
|
&"prop_tree_large": Vector2(0.084, 0.504),
|
|
&"prop_pine": Vector2(-0.044, 0.083),
|
|
&"prop_pine_large": Vector2(-0.04, 0.075),
|
|
&"prop_palm": Vector2(0.15, -0.1),
|
|
}
|
|
for tree_id: StringName in aligned_tree_collisions:
|
|
var aligned_tree := catalog.definition_for_id(tree_id)
|
|
assert(aligned_tree != null and aligned_tree.has_cylinder_collision())
|
|
var collision_center := Vector2(
|
|
aligned_tree.collision_offset.x,
|
|
aligned_tree.collision_offset.z,
|
|
)
|
|
assert(collision_center.is_equal_approx(
|
|
aligned_tree_collisions[tree_id]
|
|
))
|
|
var forest := region.get_biome_catalog().definition_for_id(&"biome_forest")
|
|
var forest_rule := forest.prop_rule_for_group(&"grass_tree")
|
|
var large_tree := catalog.definition_for_id(&"prop_tree_large")
|
|
var small_tree_weight := (
|
|
catalog.definition_for_id(&"prop_tree_1").selection_weight
|
|
+ catalog.definition_for_id(&"prop_tree_2").selection_weight
|
|
+ catalog.definition_for_id(&"prop_tree_3").selection_weight
|
|
)
|
|
assert(forest_rule.allows_prop(large_tree))
|
|
assert(large_tree.selection_weight <= small_tree_weight)
|
|
var pine_forest := region.get_biome_catalog().definition_for_id(
|
|
&"biome_pine_forest"
|
|
)
|
|
var pine_rule := pine_forest.prop_rule_for_group(&"grass_tree")
|
|
assert(pine_rule.allows_prop(large_pine))
|
|
assert(
|
|
is_equal_approx(large_pine.selection_weight, pine.selection_weight)
|
|
)
|
|
assert(forest_rule.minimum_placements == 4)
|
|
assert(forest_rule.placement_attempts_per_chunk == 4)
|
|
assert(pine_rule.minimum_placements == 4)
|
|
assert(pine_rule.placement_attempts_per_chunk == 4)
|
|
|
|
|
|
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 "mixed_surface" in tags:
|
|
assert(biome_id == &"")
|
|
elif "grass" in tags or "sand" in tags:
|
|
assert(biome_id != &"")
|
|
var biome := catalog.definition_for_id(biome_id)
|
|
assert(biome != null and biome.supports_chunk_tags(tags))
|
|
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,
|
|
grass_surface_triangles: Array[PackedVector3Array],
|
|
sand_surface_triangles: Array[PackedVector3Array],
|
|
) -> void:
|
|
assert(prop != null)
|
|
assert(prop.scale.is_equal_approx(Vector3.ONE))
|
|
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(
|
|
"TrunkCollision/CollisionShape"
|
|
) as CollisionShape3D
|
|
assert(visual_root != null)
|
|
assert(visual_root.position.is_equal_approx(definition.visual_offset))
|
|
var visual_scale := float(
|
|
prop.get_meta(&"terrain_prop_visual_scale", 1.0)
|
|
)
|
|
assert(visual_scale >= definition.minimum_visual_scale - 0.001)
|
|
assert(visual_scale <= definition.maximum_visual_scale + 0.001)
|
|
assert(
|
|
visual_root.scale.is_equal_approx(Vector3.ONE * visual_scale)
|
|
)
|
|
assert(visual != null and visual.mesh != null)
|
|
if definition.has_collision():
|
|
assert(collision != null)
|
|
assert(
|
|
collision.shape is CylinderShape3D
|
|
or collision.shape is BoxShape3D
|
|
)
|
|
assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
|
|
if collision.shape is CylinderShape3D:
|
|
var cylinder := collision.shape as CylinderShape3D
|
|
assert(is_equal_approx(
|
|
cylinder.radius,
|
|
definition.collision_radius * visual_scale,
|
|
))
|
|
assert(is_equal_approx(
|
|
cylinder.height,
|
|
definition.collision_height * visual_scale,
|
|
))
|
|
assert(collision.position.is_equal_approx(
|
|
definition.collision_offset * visual_scale
|
|
+ Vector3.UP * cylinder.height * 0.5
|
|
))
|
|
else:
|
|
assert(collision == null)
|
|
var query := PhysicsRayQueryParameters3D.create(
|
|
# Stay below neighboring cliff overhangs while still beginning above the
|
|
# authored walkable surface. Regional collision batches intentionally
|
|
# keep every original shape on one body, so excluding an overhang body
|
|
# would also exclude the ground beneath the prop.
|
|
prop.global_position + Vector3.UP * 0.1,
|
|
prop.global_position + Vector3.DOWN * 8.0,
|
|
1,
|
|
)
|
|
var excluded_colliders: Array[RID] = []
|
|
var decorations := region.get_node("Decorations") as Node3D
|
|
for decoration: Node in decorations.get_children():
|
|
var prop_body := decoration.get_node_or_null(
|
|
"TrunkCollision",
|
|
) as CollisionObject3D
|
|
if prop_body != null:
|
|
excluded_colliders.append(prop_body.get_rid())
|
|
query.exclude = excluded_colliders
|
|
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
|
|
# Stacked cliff walls can overhang a neighboring cell. Match the placement
|
|
# sampler by ignoring steep faces until the ray reaches walkable terrain.
|
|
while (
|
|
not hit.is_empty()
|
|
and absf((hit.get("normal", Vector3.ZERO) as Vector3).dot(Vector3.UP))
|
|
< 0.35
|
|
):
|
|
var steep_collider := hit.get("collider") as CollisionObject3D
|
|
assert(steep_collider != null)
|
|
excluded_colliders.append(steep_collider.get_rid())
|
|
query.exclude = excluded_colliders
|
|
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,
|
|
"Terrain height %s does not match %s (%s) at %s."
|
|
% [
|
|
ground_position.y,
|
|
prop.name,
|
|
definition.stable_id,
|
|
"%s in chunk %s; hit %s" % [
|
|
prop.global_position,
|
|
prop.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)),
|
|
(hit.get("collider") as Node).get_path(),
|
|
],
|
|
],
|
|
)
|
|
var required_surface_triangles: Array[PackedVector3Array] = (
|
|
sand_surface_triangles
|
|
if definition.procedural_group == &"sand_tree"
|
|
else grass_surface_triangles
|
|
)
|
|
var required_surface_height := _triangle_surface_height_at(
|
|
prop.global_position,
|
|
required_surface_triangles,
|
|
)
|
|
assert(
|
|
required_surface_height > -INF
|
|
and absf(required_surface_height - prop.global_position.y) <= 0.01,
|
|
"%s (%s) is not rooted on its required terrain material at %s."
|
|
% [prop.name, definition.stable_id, prop.global_position],
|
|
)
|
|
|
|
|
|
func _triangle_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 _validate_water_recovery_positions(
|
|
region: GeneratedWorldRegion,
|
|
grass_triangles: Array[PackedVector3Array],
|
|
sand_triangles: Array[PackedVector3Array],
|
|
) -> void:
|
|
var fresh_water_root := region.get_node(
|
|
"WaterBodies/FreshWaterBodies"
|
|
) as Node3D
|
|
assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
|
|
var fresh_water := fresh_water_root.get_child(0) as WaterBodyAuthoring
|
|
assert(fresh_water != null)
|
|
var entry_position := fresh_water.global_position
|
|
var fallback_position := region.get_player_spawn_transform().origin
|
|
var recovery_position := region.get_water_recovery_position(
|
|
entry_position,
|
|
fallback_position,
|
|
)
|
|
assert(recovery_position.is_finite())
|
|
assert(
|
|
_horizontal_distance_squared(entry_position, recovery_position)
|
|
< _horizontal_distance_squared(entry_position, fallback_position),
|
|
"Fresh-water recovery returned the distant generated-world spawn.",
|
|
)
|
|
assert(
|
|
recovery_position.y
|
|
> GeneratedWorldRegion.WATER_HEIGHT
|
|
+ GeneratedWorldRegion.WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
|
|
)
|
|
var recovery_triangles: Array[PackedVector3Array] = []
|
|
recovery_triangles.append_array(grass_triangles)
|
|
recovery_triangles.append_array(sand_triangles)
|
|
var center_height := _triangle_surface_height_at(
|
|
recovery_position,
|
|
recovery_triangles,
|
|
)
|
|
assert(center_height > -INF)
|
|
assert(absf(center_height - recovery_position.y) <= 0.01)
|
|
for direction: Vector2 in GeneratedWorldRegion.PROP_SURFACE_SAMPLE_DIRECTIONS:
|
|
var footprint_position := recovery_position + Vector3(
|
|
direction.x * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
|
|
0.0,
|
|
direction.y * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
|
|
)
|
|
var footprint_height := _triangle_surface_height_at(
|
|
footprint_position,
|
|
recovery_triangles,
|
|
)
|
|
assert(footprint_height > -INF)
|
|
assert(
|
|
absf(footprint_height - recovery_position.y)
|
|
<= GeneratedWorldRegion.PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
|
|
)
|
|
assert(
|
|
region.get_water_recovery_position(
|
|
entry_position,
|
|
fallback_position,
|
|
).is_equal_approx(recovery_position)
|
|
)
|
|
assert(
|
|
region.get_water_recovery_position(
|
|
Vector3(INF, 0.0, 0.0),
|
|
fallback_position,
|
|
).is_equal_approx(fallback_position)
|
|
)
|
|
|
|
|
|
func _horizontal_distance_squared(a: Vector3, b: Vector3) -> float:
|
|
return Vector2(a.x - b.x, a.z - b.z).length_squared()
|
|
|
|
|
|
func _validate_tree_gatherable_anchors(
|
|
region: GeneratedWorldRegion,
|
|
decorations: Node3D,
|
|
anchors: GatherableAnchorSet3D,
|
|
) -> void:
|
|
var eligible_props: Dictionary[StringName, Node3D] = {}
|
|
var expected_anchor_count := 0
|
|
for child: Node in decorations.get_children():
|
|
var prop := child as Node3D
|
|
if prop == null:
|
|
continue
|
|
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
|
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
|
var socket_count := _count_authored_beetle_sockets(prop)
|
|
if (
|
|
definition != null
|
|
and definition.has_gatherable_surface()
|
|
and socket_count > 0
|
|
):
|
|
eligible_props[prop.name] = prop
|
|
expected_anchor_count += socket_count
|
|
var positions := anchors.get_spawn_positions()
|
|
assert(positions.size() == expected_anchor_count)
|
|
assert(positions.size() >= 12)
|
|
for child: Node in anchors.get_children():
|
|
var anchor := child as Marker3D
|
|
assert(anchor != null)
|
|
assert(bool(anchor.get_meta(&"authored_beetle_socket", false)))
|
|
var prop_name := StringName(anchor.get_meta(&"terrain_prop_name", &""))
|
|
assert(eligible_props.has(prop_name))
|
|
var prop: Node3D = eligible_props[prop_name]
|
|
var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
|
|
var definition := region.get_prop_catalog().definition_for_id(prop_id)
|
|
assert(definition != null and definition.has_gatherable_surface())
|
|
var local_anchor := prop.to_local(anchor.global_position)
|
|
assert(prop_id != &"prop_palm")
|
|
assert(local_anchor.y >= 0.25 and local_anchor.y <= 2.0)
|
|
|
|
|
|
func _count_authored_beetle_sockets(root_node: Node) -> int:
|
|
var result := 0
|
|
if String(root_node.name).to_lower().contains("beetle_socket"):
|
|
result += 1
|
|
for child: Node in root_node.get_children():
|
|
result += _count_authored_beetle_sockets(child)
|
|
return result
|
|
|
|
|
|
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 _has_precipitation_occlusion_layer(root_node: Node) -> bool:
|
|
var mesh_instance := root_node as MeshInstance3D
|
|
if (
|
|
mesh_instance != null
|
|
and mesh_instance.get_layer_mask_value(
|
|
PrecipitationOcclusionType.RENDER_LAYER_NUMBER
|
|
)
|
|
):
|
|
return true
|
|
for child: Node in root_node.get_children():
|
|
if _has_precipitation_occlusion_layer(child):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _has_material_variant_override(
|
|
root_node: Node,
|
|
variants: Array[Material],
|
|
) -> bool:
|
|
if root_node == null:
|
|
return false
|
|
var mesh_instance := root_node as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.mesh != null:
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
var override := mesh_instance.get_surface_override_material(
|
|
surface_index
|
|
)
|
|
if override != null and override in variants:
|
|
return true
|
|
for child: Node in root_node.get_children():
|
|
if _has_material_variant_override(child, variants):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _validate_authored_chunk_surfaces(
|
|
region: GeneratedWorldRegion,
|
|
generator: TerrainChunkGenerator,
|
|
) -> void:
|
|
var generated := generator.get_generated_chunks_root()
|
|
var found_beach := false
|
|
var found_stream := false
|
|
var found_pond := false
|
|
var found_grass_ocean_edge := false
|
|
var found_grass_beach_transition := false
|
|
var found_grass_ocean_corner := false
|
|
var found_beach_ocean_corner := false
|
|
var found_grass_sand_diagonal := false
|
|
var beach_family := (
|
|
_placement_count(generator.placement_keys(), "chunk_0002") > 0
|
|
)
|
|
for chunk_root: Node in generated.get_children():
|
|
if chunk_root.name.begins_with("chunk_0002r"):
|
|
var beach := chunk_root.find_child(
|
|
"chunk_0002", true, false
|
|
) as MeshInstance3D
|
|
assert(beach != null and beach.mesh != null)
|
|
var material := beach.get_active_material(0)
|
|
assert(material != null and material.resource_name == "sand")
|
|
found_beach = true
|
|
elif (
|
|
chunk_root.name.begins_with("chunk_0003r")
|
|
or chunk_root.name.begins_with("chunk_0016r")
|
|
):
|
|
var stream_id := (
|
|
&"chunk_0016"
|
|
if chunk_root.name.begins_with("chunk_0016r")
|
|
else &"chunk_0003"
|
|
)
|
|
var stream := chunk_root.find_child(
|
|
String(stream_id), true, false
|
|
) as MeshInstance3D
|
|
assert(stream != null and stream.mesh != null)
|
|
var stream_materials := _material_names(stream)
|
|
assert("dirt" in stream_materials)
|
|
assert("grass_lite" in stream_materials)
|
|
assert("dirt_wall" in stream_materials)
|
|
if stream_id == &"chunk_0003":
|
|
found_stream = true
|
|
elif chunk_root.name.begins_with("chunk_0004r"):
|
|
var pond := chunk_root.find_child(
|
|
"chunk_0004", true, false
|
|
) as MeshInstance3D
|
|
assert(pond != null and pond.mesh != null)
|
|
for surface_index: int in pond.mesh.get_surface_count():
|
|
var arrays := pond.mesh.surface_get_arrays(surface_index)
|
|
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
|
|
for normal: Vector3 in normals:
|
|
assert(normal.y >= -0.001)
|
|
_validate_pond_collision(region, pond)
|
|
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
|
|
elif chunk_root.name.begins_with("chunk_0008r"):
|
|
var beach_ocean_corner := chunk_root.find_child(
|
|
"chunk_0008", true, false
|
|
) as MeshInstance3D
|
|
assert(beach_ocean_corner != null and beach_ocean_corner.mesh != null)
|
|
var beach_corner_materials := _material_names(beach_ocean_corner)
|
|
assert("sand" in beach_corner_materials)
|
|
found_beach_ocean_corner = true
|
|
elif chunk_root.name.begins_with("chunk_0013r"):
|
|
var grass_sand_diagonal := chunk_root.find_child(
|
|
"chunk_0013", true, false
|
|
) as MeshInstance3D
|
|
assert(grass_sand_diagonal != null and grass_sand_diagonal.mesh != null)
|
|
var diagonal_materials := _material_names(grass_sand_diagonal)
|
|
assert("grass_lite" in diagonal_materials)
|
|
assert("sand" in diagonal_materials)
|
|
found_grass_sand_diagonal = true
|
|
assert(found_stream)
|
|
assert(found_pond)
|
|
assert(found_grass_sand_diagonal)
|
|
if beach_family:
|
|
assert(found_beach)
|
|
assert(found_beach_ocean_corner)
|
|
assert(not found_grass_ocean_edge)
|
|
assert(not found_grass_ocean_corner)
|
|
else:
|
|
assert(not found_beach)
|
|
assert(not found_beach_ocean_corner)
|
|
assert(found_grass_ocean_edge)
|
|
assert(found_grass_ocean_corner)
|
|
assert(not found_grass_beach_transition)
|
|
|
|
|
|
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_projected_terrain_materials(root: Node) -> void:
|
|
var expected_sizes := {
|
|
"grass_lite": 1.75,
|
|
"sand": 2.6,
|
|
"dirt": 2.5,
|
|
}
|
|
var surface_counts := {
|
|
"grass_lite": 0,
|
|
"sand": 0,
|
|
"dirt": 0,
|
|
}
|
|
_validate_projected_material_node(root, expected_sizes, surface_counts)
|
|
for material_name: String in expected_sizes:
|
|
assert(surface_counts[material_name] > 0)
|
|
|
|
|
|
func _validate_projected_material_node(
|
|
root: Node,
|
|
expected_sizes: Dictionary,
|
|
surface_counts: Dictionary,
|
|
) -> void:
|
|
var mesh_instance := root as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.mesh != null:
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
var material := mesh_instance.get_active_material(surface_index)
|
|
if material == null or not expected_sizes.has(material.resource_name):
|
|
continue
|
|
var shader_material := material as ShaderMaterial
|
|
assert(shader_material != null and shader_material.shader != null)
|
|
assert(
|
|
shader_material.shader.resource_path
|
|
== "res://world/materials/terrain_surface_projection.gdshader"
|
|
)
|
|
assert(
|
|
is_equal_approx(
|
|
float(shader_material.get_shader_parameter(
|
|
&"tile_world_size"
|
|
)),
|
|
float(expected_sizes[material.resource_name]),
|
|
)
|
|
)
|
|
surface_counts[material.resource_name] += 1
|
|
for child: Node in root.get_children():
|
|
_validate_projected_material_node(
|
|
child,
|
|
expected_sizes,
|
|
surface_counts,
|
|
)
|
|
|
|
|
|
func _validate_pond_collision(
|
|
region: GeneratedWorldRegion,
|
|
pond: MeshInstance3D,
|
|
) -> void:
|
|
var best_centroid := Vector3.ZERO
|
|
var best_height := -INF
|
|
for surface_index: int in pond.mesh.get_surface_count():
|
|
var arrays := pond.mesh.surface_get_arrays(surface_index)
|
|
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
|
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
|
|
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
|
var triangle_count := (
|
|
indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
|
|
)
|
|
for triangle_index: int in triangle_count:
|
|
var offset := triangle_index * 3
|
|
var index_a: int = indices[offset] if not indices.is_empty() else offset
|
|
var index_b: int = (
|
|
indices[offset + 1] if not indices.is_empty() else offset + 1
|
|
)
|
|
var index_c: int = (
|
|
indices[offset + 2] if not indices.is_empty() else offset + 2
|
|
)
|
|
var a: Vector3 = vertices[index_a]
|
|
var b: Vector3 = vertices[index_b]
|
|
var c: Vector3 = vertices[index_c]
|
|
var normal := (
|
|
(normals[index_a] + normals[index_b] + normals[index_c]) / 3.0
|
|
).normalized()
|
|
var centroid := (a + b + c) / 3.0
|
|
if normal.y > 0.5 and centroid.y > best_height:
|
|
best_height = centroid.y
|
|
best_centroid = centroid
|
|
assert(best_height > -INF)
|
|
var target := pond.global_transform * best_centroid
|
|
var query := PhysicsRayQueryParameters3D.create(
|
|
target + Vector3.UP * 2.0,
|
|
target + Vector3.DOWN * 2.0,
|
|
1,
|
|
)
|
|
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
|
|
assert(not hit.is_empty())
|
|
var collider := hit.get("collider") as Node
|
|
assert(
|
|
collider != null
|
|
and collider.has_meta(&"terrain_collision_batch")
|
|
)
|
|
|
|
|
|
func _decoration_group_count(
|
|
region: GeneratedWorldRegion,
|
|
group: StringName,
|
|
) -> int:
|
|
var count := 0
|
|
var decorations := region.get_node("Decorations") as Node3D
|
|
for child: Node in decorations.get_children():
|
|
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
|
|
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:
|
|
var count := 0
|
|
for key: String in keys:
|
|
if key.begins_with(stable_id + "@"):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _decoration_signature(region: GeneratedWorldRegion) -> PackedStringArray:
|
|
var result := PackedStringArray()
|
|
var decorations := region.get_node("Decorations") as Node3D
|
|
for child: Node in decorations.get_children():
|
|
var prop := child as Node3D
|
|
result.append("%s:%s:%s" % [
|
|
prop.name,
|
|
prop.position,
|
|
prop.rotation,
|
|
])
|
|
return result
|