Generate complete freshwater and elevated terrain features
This commit is contained in:
parent
219c462181
commit
f2b96f2c1f
7 changed files with 2105 additions and 141 deletions
|
|
@ -199,6 +199,59 @@ func _validate_portable_water_body() -> void:
|
|||
mesh_derived_body.queue_free()
|
||||
await process_frame
|
||||
|
||||
var polygon_body := WaterBodyScene.instantiate() as WaterBodyAuthoring
|
||||
polygon_body.position = Vector3(30.0, 5.0, 20.0)
|
||||
polygon_body.surface_polygon = PackedVector2Array([
|
||||
Vector2(-4.0, -4.0),
|
||||
Vector2(4.0, -4.0),
|
||||
Vector2(4.0, 4.0),
|
||||
Vector2(0.0, 1.0),
|
||||
Vector2(-4.0, 4.0),
|
||||
])
|
||||
polygon_body.visual_surface_enabled = false
|
||||
polygon_body.fish_pool = PondPool
|
||||
root.add_child(polygon_body)
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
|
||||
var polygon_fishing_region := polygon_body.get_node(
|
||||
"FishingRegion"
|
||||
) as FishableWaterRegionType
|
||||
var polygon_recovery_region := polygon_body.get_node(
|
||||
"RecoveryRegion"
|
||||
) as Area3D
|
||||
assert(polygon_fishing_region.get_child_count() == 3)
|
||||
assert(polygon_recovery_region.get_child_count() == 3)
|
||||
for child: Node in polygon_fishing_region.get_children():
|
||||
var polygon_shape_node := child as CollisionShape3D
|
||||
assert(polygon_shape_node != null)
|
||||
assert(polygon_shape_node.shape is ConvexPolygonShape3D)
|
||||
|
||||
var polygon_resolver := FishingSurfaceResolverType.new()
|
||||
var polygon_space_state := (
|
||||
polygon_body.get_world_3d().direct_space_state
|
||||
)
|
||||
var polygon_inside: FishingSurfaceSampleType = (
|
||||
polygon_resolver.resolve_surface(
|
||||
polygon_space_state,
|
||||
Vector3(30.0, 0.0, 20.0),
|
||||
8.0,
|
||||
)
|
||||
)
|
||||
assert(polygon_inside.is_fishable())
|
||||
assert(is_equal_approx(polygon_inside.position.y, 5.0))
|
||||
var polygon_notch: FishingSurfaceSampleType = (
|
||||
polygon_resolver.resolve_surface(
|
||||
polygon_space_state,
|
||||
Vector3(30.0, 0.0, 23.0),
|
||||
8.0,
|
||||
)
|
||||
)
|
||||
assert(not polygon_notch.has_surface)
|
||||
|
||||
polygon_body.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_starter_region_surfaces() -> void:
|
||||
var region := StarterRegionScene.instantiate() as Node3D
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const GeneratedRiverPool: FishPool = preload(
|
|||
)
|
||||
const FIRST_SEED := 13001
|
||||
const SECOND_SEED := 13002
|
||||
const RIVER_FALLBACK_SEED := 13012
|
||||
const EXPECTED_PROP_IDS: Array[StringName] = [
|
||||
&"prop_bridge",
|
||||
&"prop_dock",
|
||||
|
|
@ -62,6 +63,9 @@ func _run() -> void:
|
|||
) 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
|
||||
await physics_frame
|
||||
|
|
@ -74,13 +78,19 @@ func _run() -> void:
|
|||
_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
|
||||
|
|
@ -88,6 +98,17 @@ func _run() -> void:
|
|||
_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
|
||||
|
|
@ -106,32 +127,192 @@ func _validate_generated_region(
|
|||
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,
|
||||
) >= 7)
|
||||
assert(mini(
|
||||
generator._river_feature_footprint_size.x,
|
||||
generator._river_feature_footprint_size.y,
|
||||
) == 2)
|
||||
assert(generator._river_feature_flow_direction in [
|
||||
Vector2i.RIGHT,
|
||||
Vector2i.DOWN,
|
||||
Vector2i.LEFT,
|
||||
Vector2i.UP,
|
||||
])
|
||||
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)
|
||||
assert(_placement_count(placements, "chunk_0003") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0004") >= 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")
|
||||
assert(beach_count == 44 and grass_coast_count == 0)
|
||||
assert(beach_corner_count == 3 and grass_corner_count == 0)
|
||||
assert(_placement_count(placements, "chunk_0009") == 10)
|
||||
assert(_placement_count(placements, "chunk_0010") == 5)
|
||||
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")
|
||||
== 14
|
||||
== (
|
||||
(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_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_0012") == 2)
|
||||
assert(_placement_count(placements, "chunk_0014") == 2)
|
||||
assert(_placement_count(placements, "chunk_0015") == 1)
|
||||
assert(_placement_count(placements, "chunk_0017") == 0)
|
||||
assert(_placement_count(placements, "chunk_0018") == 0)
|
||||
assert(_placement_count(placements, "chunk_0019") == 1)
|
||||
assert(_placement_count(placements, "chunk_0020") == 1)
|
||||
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_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_size - 6) * 4)
|
||||
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_length := maxi(
|
||||
generator._river_feature_footprint_size.x,
|
||||
generator._river_feature_footprint_size.y,
|
||||
)
|
||||
assert(river_length in [7, 8])
|
||||
assert(_placement_count(placements, "chunk_0024") == river_length - 2)
|
||||
assert(_placement_count(placements, "chunk_0025") == river_length - 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_flow_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,
|
||||
|
|
@ -159,8 +340,11 @@ func _validate_generated_region(
|
|||
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"))
|
||||
assert(absf(shop.position.x) < 5.0 and absf(shop.position.z) < 5.0)
|
||||
assert(absf(storage.position.x) < 5.0 and absf(storage.position.z) < 5.0)
|
||||
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)
|
||||
|
|
@ -173,30 +357,68 @@ func _validate_generated_region(
|
|||
assert(ocean.water_type == WaterType.Type.SALT_WATER)
|
||||
assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT))
|
||||
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_length * 2)
|
||||
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 or fresh.surface_size.y < 10.0)
|
||||
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 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)
|
||||
|
||||
|
|
@ -351,10 +573,10 @@ func _validate_elevated_cliff_feature(
|
|||
var coastal_ids: Array[StringName] = [
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
&"chunk_0019",
|
||||
&"chunk_0020",
|
||||
&"chunk_9999",
|
||||
&"chunk_9998",
|
||||
&"chunk_9997",
|
||||
&"chunk_9996",
|
||||
]
|
||||
var has_coastal_feature := false
|
||||
for record: Dictionary in generator.placement_records():
|
||||
|
|
@ -364,12 +586,17 @@ func _validate_elevated_cliff_feature(
|
|||
&"chunk_0010",
|
||||
&"chunk_0011",
|
||||
&"chunk_0012",
|
||||
&"chunk_0026",
|
||||
&"chunk_0027",
|
||||
&"chunk_0028",
|
||||
&"chunk_0029",
|
||||
&"chunk_0030",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
&"chunk_0019",
|
||||
&"chunk_0020",
|
||||
&"chunk_9999",
|
||||
&"chunk_9998",
|
||||
&"chunk_9997",
|
||||
&"chunk_9996",
|
||||
]:
|
||||
continue
|
||||
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
||||
|
|
@ -383,14 +610,32 @@ func _validate_elevated_cliff_feature(
|
|||
assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1)
|
||||
assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1)
|
||||
elevated_coordinates[coordinate] = stable_id
|
||||
if stable_id == &"chunk_0009":
|
||||
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))
|
||||
assert(top_coordinates.size() == (10 if has_coastal_feature else 9))
|
||||
assert(ramp_records.size() == (2 if has_coastal_feature else 1))
|
||||
var feature_center := Vector2i(-1, -1)
|
||||
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):
|
||||
|
|
@ -399,12 +644,12 @@ func _validate_elevated_cliff_feature(
|
|||
elevated_coordinates.get(
|
||||
candidate + Vector2i(column_offset, row_offset),
|
||||
&"",
|
||||
) == &"chunk_0009"
|
||||
) == primary_top_id
|
||||
)
|
||||
if neighboring_top_count == 9:
|
||||
feature_center = candidate
|
||||
detected_feature_center = candidate
|
||||
break
|
||||
assert(feature_center != Vector2i(-1, -1))
|
||||
assert(detected_feature_center == feature_center)
|
||||
for row_offset: int in range(-2, 3):
|
||||
for column_offset: int in range(-2, 3):
|
||||
assert(
|
||||
|
|
@ -413,14 +658,7 @@ func _validate_elevated_cliff_feature(
|
|||
)
|
||||
)
|
||||
if has_coastal_feature:
|
||||
var secondary_top := Vector2i(-1, -1)
|
||||
for coordinate: Vector2i in top_coordinates:
|
||||
if (
|
||||
absi(coordinate.x - feature_center.x) > 1
|
||||
or absi(coordinate.y - feature_center.y) > 1
|
||||
):
|
||||
secondary_top = coordinate
|
||||
break
|
||||
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):
|
||||
|
|
@ -429,7 +667,7 @@ func _validate_elevated_cliff_feature(
|
|||
secondary_top + Vector2i(column_offset, row_offset)
|
||||
)
|
||||
)
|
||||
var found_deep_top_landing := false
|
||||
var found_primary_ramp := false
|
||||
for ramp_record: Dictionary in ramp_records:
|
||||
var ramp_coordinate: Vector2i = ramp_record.get(
|
||||
"coordinate", Vector2i(-1, -1)
|
||||
|
|
@ -444,8 +682,8 @@ func _validate_elevated_cliff_feature(
|
|||
elevated_coordinates.get(ramp_coordinate + high_offset, &"")
|
||||
== &"chunk_0009"
|
||||
)
|
||||
found_deep_top_landing = (
|
||||
found_deep_top_landing
|
||||
found_primary_ramp = (
|
||||
found_primary_ramp
|
||||
or elevated_coordinates.get(
|
||||
ramp_coordinate + high_offset * 2,
|
||||
&"",
|
||||
|
|
@ -459,7 +697,7 @@ func _validate_elevated_cliff_feature(
|
|||
generator.placement_keys()[low_index].begins_with("chunk_0000@")
|
||||
or generator.placement_keys()[low_index].begins_with("chunk_spawn@")
|
||||
)
|
||||
assert(found_deep_top_landing)
|
||||
assert(found_primary_ramp == not primary_is_third_tier)
|
||||
|
||||
var generated := generator.get_generated_chunks_root()
|
||||
assert(generated != null)
|
||||
|
|
@ -473,25 +711,39 @@ func _validate_elevated_cliff_feature(
|
|||
&"chunk_0010",
|
||||
&"chunk_0011",
|
||||
&"chunk_0012",
|
||||
&"chunk_0026",
|
||||
&"chunk_0027",
|
||||
&"chunk_0028",
|
||||
&"chunk_0029",
|
||||
&"chunk_0030",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
&"chunk_0019",
|
||||
&"chunk_0020",
|
||||
&"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)
|
||||
assert(source_mesh.has_node("TerrainCollision"))
|
||||
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_0017", &"chunk_0018"]:
|
||||
if stable_id in [&"chunk_9999", &"chunk_9998"]:
|
||||
transition_base_mesh = &"chunk_0007"
|
||||
elif stable_id in [&"chunk_0019", &"chunk_0020"]:
|
||||
elif stable_id in [&"chunk_9997", &"chunk_9996"]:
|
||||
transition_base_mesh = &"chunk_0008"
|
||||
var base_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
base_layer,
|
||||
|
|
@ -502,10 +754,10 @@ func _validate_elevated_cliff_feature(
|
|||
(
|
||||
&"chunk_0015"
|
||||
if stable_id in [
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
&"chunk_0019",
|
||||
&"chunk_0020",
|
||||
&"chunk_9999",
|
||||
&"chunk_9998",
|
||||
&"chunk_9997",
|
||||
&"chunk_9996",
|
||||
]
|
||||
else stable_id
|
||||
),
|
||||
|
|
@ -514,32 +766,40 @@ func _validate_elevated_cliff_feature(
|
|||
assert(overlay_mesh != null and overlay_mesh.mesh != null)
|
||||
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
|
||||
assert(overlay_mesh.has_node("TerrainCollision"))
|
||||
assert(layered_count == (31 if has_coastal_feature else 25))
|
||||
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@"))
|
||||
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
|
||||
assert(
|
||||
StringName(child.get_meta(&"terrain_chunk_id", &""))
|
||||
== &"chunk_0010"
|
||||
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,
|
||||
generator.elevated_cliff_level_height,
|
||||
expected_height,
|
||||
)
|
||||
)
|
||||
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
child,
|
||||
&"chunk_0010",
|
||||
stacked_id,
|
||||
)
|
||||
assert(stacked_mesh != null and stacked_mesh.mesh != null)
|
||||
assert(stacked_mesh.has_node("TerrainCollision"))
|
||||
|
|
|
|||
|
|
@ -25,6 +25,23 @@ const EXPECTED_IDS: Array[String] = [
|
|||
"chunk_0018",
|
||||
"chunk_0019",
|
||||
"chunk_0020",
|
||||
"chunk_0021",
|
||||
"chunk_0022",
|
||||
"chunk_0023",
|
||||
"chunk_0024",
|
||||
"chunk_0025",
|
||||
"chunk_0026",
|
||||
"chunk_0027",
|
||||
"chunk_0028",
|
||||
"chunk_0029",
|
||||
"chunk_0030",
|
||||
"chunk_0031",
|
||||
"chunk_0032",
|
||||
"chunk_9999",
|
||||
"chunk_9998",
|
||||
"chunk_9997",
|
||||
"chunk_9996",
|
||||
"chunk_9995",
|
||||
"chunk_spawn",
|
||||
]
|
||||
const REQUIRED_IDS: Array[String] = [
|
||||
|
|
@ -57,8 +74,25 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
|
|||
&"chunk_0016": 4,
|
||||
&"chunk_0017": 4,
|
||||
&"chunk_0018": 4,
|
||||
&"chunk_0019": 4,
|
||||
&"chunk_0019": 1,
|
||||
&"chunk_0020": 4,
|
||||
&"chunk_0021": 4,
|
||||
&"chunk_0022": 4,
|
||||
&"chunk_0023": 4,
|
||||
&"chunk_0024": 4,
|
||||
&"chunk_0025": 4,
|
||||
&"chunk_0026": 4,
|
||||
&"chunk_0027": 4,
|
||||
&"chunk_0028": 4,
|
||||
&"chunk_0029": 4,
|
||||
&"chunk_0030": 4,
|
||||
&"chunk_0031": 4,
|
||||
&"chunk_0032": 4,
|
||||
&"chunk_9999": 4,
|
||||
&"chunk_9998": 4,
|
||||
&"chunk_9997": 4,
|
||||
&"chunk_9996": 4,
|
||||
&"chunk_9995": 4,
|
||||
&"chunk_spawn": 1,
|
||||
}
|
||||
|
||||
|
|
@ -104,15 +138,32 @@ func _validate_catalog() -> void:
|
|||
var pond := CATALOG.definition_for_id(&"chunk_0004")
|
||||
var cliff_sea_edge := CATALOG.definition_for_id(&"chunk_0014")
|
||||
var cliff_sea_corner := CATALOG.definition_for_id(&"chunk_0015")
|
||||
var cliff_sea_transition_right := CATALOG.definition_for_id(&"chunk_0017")
|
||||
var cliff_sea_transition_left := CATALOG.definition_for_id(&"chunk_0018")
|
||||
var cliff_beach_transition_right := CATALOG.definition_for_id(&"chunk_0019")
|
||||
var cliff_beach_transition_left := CATALOG.definition_for_id(&"chunk_0020")
|
||||
var cliff_sea_transition_right := CATALOG.definition_for_id(&"chunk_9999")
|
||||
var cliff_sea_transition_left := CATALOG.definition_for_id(&"chunk_9998")
|
||||
var cliff_beach_transition_right := CATALOG.definition_for_id(&"chunk_9997")
|
||||
var cliff_beach_transition_left := CATALOG.definition_for_id(&"chunk_9996")
|
||||
var stream_variant := CATALOG.definition_for_id(&"chunk_0016")
|
||||
var lake_edge := CATALOG.definition_for_id(&"chunk_0017")
|
||||
var lake_corner := CATALOG.definition_for_id(&"chunk_0018")
|
||||
var lake_fill := CATALOG.definition_for_id(&"chunk_0019")
|
||||
var lake_diagonal := CATALOG.definition_for_id(&"chunk_0020")
|
||||
var lake_narrow := CATALOG.definition_for_id(&"chunk_0021")
|
||||
var lake_regular_to_narrow := CATALOG.definition_for_id(&"chunk_0022")
|
||||
var lake_narrow_to_regular := CATALOG.definition_for_id(&"chunk_0023")
|
||||
var river_edge := CATALOG.definition_for_id(&"chunk_0024")
|
||||
var river_edge_variant := CATALOG.definition_for_id(&"chunk_0025")
|
||||
var river_source_right := CATALOG.definition_for_id(&"chunk_0029")
|
||||
var river_source_left := CATALOG.definition_for_id(&"chunk_0030")
|
||||
var river_outlet_right := CATALOG.definition_for_id(&"chunk_0031")
|
||||
var river_outlet_left := CATALOG.definition_for_id(&"chunk_0032")
|
||||
var river_cap := CATALOG.definition_for_id(&"chunk_9995")
|
||||
var cliff_top := CATALOG.definition_for_id(&"chunk_0009")
|
||||
var cliff_corner := CATALOG.definition_for_id(&"chunk_0010")
|
||||
var cliff_edge := CATALOG.definition_for_id(&"chunk_0011")
|
||||
var cliff_ramp := CATALOG.definition_for_id(&"chunk_0012")
|
||||
var cliff_third_tier_edge := CATALOG.definition_for_id(&"chunk_0026")
|
||||
var cliff_third_tier_corner := CATALOG.definition_for_id(&"chunk_0027")
|
||||
var cliff_third_tier_top := CATALOG.definition_for_id(&"chunk_0028")
|
||||
var spawn := CATALOG.definition_for_id(&"chunk_spawn")
|
||||
_check(
|
||||
(
|
||||
|
|
@ -132,10 +183,27 @@ func _validate_catalog() -> void:
|
|||
and cliff_beach_transition_right != null
|
||||
and cliff_beach_transition_left != null
|
||||
and stream_variant != null
|
||||
and lake_edge != null
|
||||
and lake_corner != null
|
||||
and lake_fill != null
|
||||
and lake_diagonal != null
|
||||
and lake_narrow != null
|
||||
and lake_regular_to_narrow != null
|
||||
and lake_narrow_to_regular != null
|
||||
and river_edge != null
|
||||
and river_edge_variant != null
|
||||
and river_source_right != null
|
||||
and river_source_left != null
|
||||
and river_outlet_right != null
|
||||
and river_outlet_left != null
|
||||
and river_cap != null
|
||||
and cliff_top != null
|
||||
and cliff_corner != null
|
||||
and cliff_edge != null
|
||||
and cliff_ramp != null
|
||||
and cliff_third_tier_edge != null
|
||||
and cliff_third_tier_corner != null
|
||||
and cliff_third_tier_top != null
|
||||
and spawn != null
|
||||
),
|
||||
"The authored biome-rule definitions must be available.",
|
||||
|
|
@ -157,10 +225,27 @@ func _validate_catalog() -> void:
|
|||
and cliff_beach_transition_right != null
|
||||
and cliff_beach_transition_left != null
|
||||
and stream_variant != null
|
||||
and lake_edge != null
|
||||
and lake_corner != null
|
||||
and lake_fill != null
|
||||
and lake_diagonal != null
|
||||
and lake_narrow != null
|
||||
and lake_regular_to_narrow != null
|
||||
and lake_narrow_to_regular != null
|
||||
and river_edge != null
|
||||
and river_edge_variant != null
|
||||
and river_source_right != null
|
||||
and river_source_left != null
|
||||
and river_outlet_right != null
|
||||
and river_outlet_left != null
|
||||
and river_cap != null
|
||||
and cliff_top != null
|
||||
and cliff_corner != null
|
||||
and cliff_edge != null
|
||||
and cliff_ramp != null
|
||||
and cliff_third_tier_edge != null
|
||||
and cliff_third_tier_corner != null
|
||||
and cliff_third_tier_top != null
|
||||
and spawn != null
|
||||
):
|
||||
_check(
|
||||
|
|
@ -314,10 +399,18 @@ func _validate_catalog() -> void:
|
|||
== (1 << int(TerrainChunkTopology.Edge.NORTH)),
|
||||
"A pond must terminate either end of an inland freshwater chain.",
|
||||
)
|
||||
_check(
|
||||
pond.maximum_placements == 2,
|
||||
"Generated worlds must contain only one pond/stream chain.",
|
||||
)
|
||||
_check(
|
||||
stream != null
|
||||
and is_equal_approx(stream.selection_weight, 0.3)
|
||||
and is_equal_approx(stream_variant.selection_weight, 0.3)
|
||||
and stream.maximum_placements == 2
|
||||
and stream_variant.maximum_placements == 1
|
||||
and "pond" in stream.tags
|
||||
and "river" not in stream.tags
|
||||
and stream_variant.tags == stream.tags
|
||||
and stream_variant.allowed_neighbor_tags
|
||||
== stream.allowed_neighbor_tags
|
||||
|
|
@ -331,10 +424,98 @@ func _validate_catalog() -> void:
|
|||
stream.water_surface_offset.y,
|
||||
),
|
||||
(
|
||||
"Both stream visuals must share one combined selection weight "
|
||||
"Both pond-stream visuals must share one combined selection weight "
|
||||
+ "and mirrored water footprints."
|
||||
),
|
||||
)
|
||||
_check(
|
||||
(
|
||||
not lake_edge.participates_in_base_solver
|
||||
and not lake_corner.participates_in_base_solver
|
||||
and not lake_fill.participates_in_base_solver
|
||||
and not lake_diagonal.participates_in_base_solver
|
||||
and not lake_narrow.participates_in_base_solver
|
||||
and not lake_regular_to_narrow.participates_in_base_solver
|
||||
and not lake_narrow_to_regular.participates_in_base_solver
|
||||
and lake_edge.water_inlet_edges == lake_edge.water_outlet_edges
|
||||
and lake_edge.water_inlet_edges == 7
|
||||
and lake_corner.water_inlet_edges
|
||||
== lake_corner.water_outlet_edges
|
||||
and lake_corner.water_inlet_edges == 6
|
||||
and lake_fill.water_inlet_edges
|
||||
== lake_fill.water_outlet_edges
|
||||
and lake_fill.water_inlet_edges == 15
|
||||
and lake_fill.water_surface_size == Vector2(10.0, 10.0)
|
||||
and lake_diagonal.water_inlet_edges
|
||||
== lake_diagonal.water_outlet_edges
|
||||
and lake_diagonal.water_inlet_edges == 6
|
||||
and lake_diagonal.maximum_placements == 4
|
||||
and lake_diagonal.water_surface_polygon.size() == 6
|
||||
and lake_narrow.water_inlet_edges == 14
|
||||
and lake_narrow.water_outlet_edges == 14
|
||||
and lake_narrow.water_surface_polygon.size() == 4
|
||||
and lake_regular_to_narrow.water_inlet_edges == 14
|
||||
and lake_regular_to_narrow.water_outlet_edges == 14
|
||||
and lake_regular_to_narrow.water_surface_polygon.size() == 5
|
||||
and lake_narrow_to_regular.water_inlet_edges == 14
|
||||
and lake_narrow_to_regular.water_outlet_edges == 14
|
||||
and lake_narrow_to_regular.water_surface_polygon.size() == 5
|
||||
and "lake" in lake_edge.tags
|
||||
and "lake" in lake_corner.tags
|
||||
and "lake" in lake_fill.tags
|
||||
and "lake" in lake_diagonal.tags
|
||||
and "lake" in lake_narrow.tags
|
||||
and "lake" in lake_regular_to_narrow.tags
|
||||
and "lake" in lake_narrow_to_regular.tags
|
||||
),
|
||||
"The deterministic lake ring and bottom must remain connected freshwater chunks.",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
not river_edge.participates_in_base_solver
|
||||
and not river_edge_variant.participates_in_base_solver
|
||||
and not river_source_right.participates_in_base_solver
|
||||
and not river_source_left.participates_in_base_solver
|
||||
and not river_outlet_right.participates_in_base_solver
|
||||
and not river_outlet_left.participates_in_base_solver
|
||||
and not river_cap.participates_in_base_solver
|
||||
and river_edge.water_inlet_edges == 14
|
||||
and river_edge.water_outlet_edges == 14
|
||||
and river_edge_variant.water_inlet_edges == 14
|
||||
and river_edge_variant.water_outlet_edges == 14
|
||||
and river_source_right.water_inlet_edges == 6
|
||||
and river_source_right.water_outlet_edges == 6
|
||||
and river_source_left.water_inlet_edges == 3
|
||||
and river_source_left.water_outlet_edges == 3
|
||||
and river_outlet_right.water_inlet_edges == 14
|
||||
and river_outlet_right.water_outlet_edges == 14
|
||||
and river_outlet_left.water_inlet_edges == 11
|
||||
and river_outlet_left.water_outlet_edges == 11
|
||||
and river_cap.water_inlet_edges == 6
|
||||
and river_cap.water_outlet_edges == 6
|
||||
and river_edge.water_surface_polygon.size() == 13
|
||||
and river_edge_variant.water_surface_polygon.size() == 13
|
||||
and river_source_right.water_surface_polygon.size() == 13
|
||||
and river_source_left.water_surface_polygon.size() == 13
|
||||
and river_outlet_right.water_surface_polygon.size() == 19
|
||||
and river_outlet_left.water_surface_polygon.size() == 19
|
||||
and river_outlet_right.ocean_facing_edges == 2
|
||||
and river_outlet_left.ocean_facing_edges == 2
|
||||
and river_cap.water_surface_polygon.size() == 6
|
||||
and river_cap.primary_mesh_name == &"chunk_0020"
|
||||
and "river" in river_edge.tags
|
||||
and "river" in river_edge_variant.tags
|
||||
and "river_source_right" in river_source_right.tags
|
||||
and "river_source_left" in river_source_left.tags
|
||||
and "river_outlet_right" in river_outlet_right.tags
|
||||
and "river_outlet_left" in river_outlet_left.tags
|
||||
and "river" in river_cap.tags
|
||||
),
|
||||
(
|
||||
"The river banks, cliff source, ocean outlet, and legacy caps "
|
||||
+ "must remain connected freshwater chunks."
|
||||
),
|
||||
)
|
||||
for cliff_definition: TerrainChunkDefinition in [
|
||||
cliff_top,
|
||||
cliff_corner,
|
||||
|
|
@ -348,6 +529,19 @@ func _validate_catalog() -> void:
|
|||
and cliff_definition.base_layer_mesh_name == &"chunk_0000",
|
||||
"Every second-tier piece must overlay level-one grass inland.",
|
||||
)
|
||||
for cliff_definition: TerrainChunkDefinition in [
|
||||
cliff_third_tier_top,
|
||||
cliff_third_tier_corner,
|
||||
cliff_third_tier_edge,
|
||||
]:
|
||||
_check(
|
||||
cliff_definition.overlay_only
|
||||
and cliff_definition.must_be_interior
|
||||
and cliff_definition.base_layer_scene != null
|
||||
and cliff_definition.base_layer_mesh_name == &"chunk_0000"
|
||||
and "elevation_3" in cliff_definition.tags,
|
||||
"Every third-tier piece must overlay level-one grass inland.",
|
||||
)
|
||||
for coastal_cliff: TerrainChunkDefinition in [
|
||||
cliff_sea_edge,
|
||||
cliff_sea_corner,
|
||||
|
|
@ -449,6 +643,13 @@ func _validate_catalog() -> void:
|
|||
and cliff_ramp.maximum_placements > 0,
|
||||
"The second-tier ramp must remain available without being required.",
|
||||
)
|
||||
_check(
|
||||
cliff_third_tier_top.minimum_required_neighbors == 4
|
||||
and "elevation_3" in cliff_third_tier_top.required_neighbor_tags
|
||||
and "ramp" not in cliff_third_tier_edge.tags
|
||||
and "ramp" not in cliff_third_tier_corner.tags,
|
||||
"The third-tier set must form a closed, intentionally unramped plateau.",
|
||||
)
|
||||
|
||||
|
||||
func _validate_profiles() -> void:
|
||||
|
|
@ -1099,6 +1300,242 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
),
|
||||
"No rotated diagonal edge may accept the opposite surface.",
|
||||
)
|
||||
var lake_diagonal_northwest := _find_variant(generator, &"chunk_0020", 0)
|
||||
var lake_diagonal_northeast := _find_variant(generator, &"chunk_0020", 3)
|
||||
var lake_narrow_top := _find_variant(generator, &"chunk_0021", 0)
|
||||
var lake_narrow_left := _find_variant(generator, &"chunk_0021", 1)
|
||||
var lake_regular_to_narrow := _find_variant(generator, &"chunk_0022", 0)
|
||||
var lake_narrow_to_regular := _find_variant(generator, &"chunk_0023", 0)
|
||||
var lake_regular_top := _find_variant(generator, &"chunk_0017", 3)
|
||||
_check(
|
||||
(
|
||||
lake_diagonal_northwest != null
|
||||
and lake_diagonal_northeast != null
|
||||
and lake_narrow_top != null
|
||||
and lake_narrow_left != null
|
||||
and lake_regular_to_narrow != null
|
||||
and lake_narrow_to_regular != null
|
||||
and lake_regular_top != null
|
||||
),
|
||||
"Every authored diagonal lake perimeter variant must be available.",
|
||||
)
|
||||
if (
|
||||
lake_diagonal_northwest != null
|
||||
and lake_diagonal_northeast != null
|
||||
and lake_narrow_top != null
|
||||
and lake_narrow_left != null
|
||||
and lake_regular_to_narrow != null
|
||||
and lake_narrow_to_regular != null
|
||||
and lake_regular_top != null
|
||||
):
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
lake_diagonal_northwest,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_narrow_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_diagonal_northwest,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
lake_narrow_left,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
),
|
||||
"Both diagonal banks must join the exact narrow lake profile.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
lake_narrow_top,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_narrow_to_regular,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_narrow_to_regular,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_regular_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_regular_top,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_regular_to_narrow,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_regular_to_narrow,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_narrow_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
),
|
||||
"The narrow lake run must transition exactly into the regular edge.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
lake_diagonal_northwest,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_narrow_to_regular,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_narrow_to_regular,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_regular_to_narrow,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
lake_regular_to_narrow,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
lake_diagonal_northeast,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
),
|
||||
"The five-chunk lake side must remain valid without a narrow run.",
|
||||
)
|
||||
var river_cap_northwest := _find_variant(generator, &"chunk_9995", 0)
|
||||
var river_cap_northeast := _find_variant(generator, &"chunk_9995", 3)
|
||||
var river_cap_southwest := _find_variant(generator, &"chunk_9995", 1)
|
||||
var river_cap_southeast := _find_variant(generator, &"chunk_9995", 2)
|
||||
var river_edge_top := _find_variant(generator, &"chunk_0024", 0)
|
||||
var river_edge_variant_top := _find_variant(generator, &"chunk_0025", 0)
|
||||
var river_edge_variant_bottom := _find_variant(generator, &"chunk_0025", 2)
|
||||
var river_source_right := _find_variant(generator, &"chunk_0029", 0)
|
||||
var river_source_left := _find_variant(generator, &"chunk_0030", 0)
|
||||
var river_outlet_right := _find_variant(generator, &"chunk_0031", 0)
|
||||
var river_outlet_left := _find_variant(generator, &"chunk_0032", 0)
|
||||
var beach_east := _find_variant(generator, &"chunk_0002", 0)
|
||||
_check(
|
||||
(
|
||||
river_cap_northwest != null
|
||||
and river_cap_northeast != null
|
||||
and river_cap_southwest != null
|
||||
and river_cap_southeast != null
|
||||
and river_edge_top != null
|
||||
and river_edge_variant_top != null
|
||||
and river_edge_variant_bottom != null
|
||||
and river_source_right != null
|
||||
and river_source_left != null
|
||||
and river_outlet_right != null
|
||||
and river_outlet_left != null
|
||||
and beach_east != null
|
||||
),
|
||||
(
|
||||
"Every authored river-bank, source, outlet, and legacy-cap variant "
|
||||
+ "must be available."
|
||||
),
|
||||
)
|
||||
if (
|
||||
river_cap_northwest != null
|
||||
and river_cap_northeast != null
|
||||
and river_cap_southwest != null
|
||||
and river_cap_southeast != null
|
||||
and river_edge_top != null
|
||||
and river_edge_variant_top != null
|
||||
and river_edge_variant_bottom != null
|
||||
and river_source_right != null
|
||||
and river_source_left != null
|
||||
and river_outlet_right != null
|
||||
and river_outlet_left != null
|
||||
and beach_east != null
|
||||
):
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
river_source_right,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_source_left,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_source_right,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_edge_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_source_left,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_edge_variant_bottom,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
),
|
||||
"The paired cliff source must open directly into both river banks.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
river_edge_top,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_outlet_right,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_edge_variant_bottom,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_outlet_left,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_outlet_right,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_outlet_left,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
),
|
||||
"Both river banks must terminate in the paired beach outlet.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
beach_east,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_outlet_right,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_outlet_left,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
beach_east,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
),
|
||||
"The ocean outlet must remain straddled by ordinary beach tiles.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
river_cap_northwest,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_edge_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_edge_top,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_edge_variant_top,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_edge_variant_top,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
river_cap_northeast,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
),
|
||||
"Both river-bank visuals must chain exactly between composite caps.",
|
||||
)
|
||||
_check(
|
||||
generator._edges_are_compatible(
|
||||
river_cap_northwest,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_cap_southwest,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_edge_top,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_edge_variant_bottom,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
)
|
||||
and generator._edges_are_compatible(
|
||||
river_cap_northeast,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
river_cap_southeast,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
),
|
||||
"Opposing river banks and end caps must close the water channel.",
|
||||
)
|
||||
for stable_id: StringName in [
|
||||
&"chunk_0005",
|
||||
&"chunk_0006",
|
||||
|
|
@ -1106,10 +1543,10 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
&"chunk_0008",
|
||||
&"chunk_0014",
|
||||
&"chunk_0015",
|
||||
&"chunk_0017",
|
||||
&"chunk_0018",
|
||||
&"chunk_0019",
|
||||
&"chunk_0020",
|
||||
&"chunk_9999",
|
||||
&"chunk_9998",
|
||||
&"chunk_9997",
|
||||
&"chunk_9996",
|
||||
]:
|
||||
for quarter_turns: int in 4:
|
||||
var coast_variant := _find_variant(
|
||||
|
|
@ -1161,8 +1598,8 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|||
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
|
||||
var summary := generator._build_summary()
|
||||
_check(
|
||||
int(summary.get("variant_count", 0)) == 85,
|
||||
"The current catalog must expose all 85 authored rotations.",
|
||||
int(summary.get("variant_count", 0)) == 150,
|
||||
"The current catalog must expose all 150 authored rotations.",
|
||||
)
|
||||
_check(
|
||||
int(summary.get("solver_variant_count", 0))
|
||||
|
|
|
|||
|
|
@ -356,7 +356,14 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void:
|
|||
var surface_size: Vector2 = record.get(
|
||||
"water_surface_size", Vector2.ZERO
|
||||
)
|
||||
if surface_size.x <= 0.0 or surface_size.y <= 0.0:
|
||||
var surface_polygon: PackedVector2Array = record.get(
|
||||
"water_surface_polygon",
|
||||
PackedVector2Array(),
|
||||
)
|
||||
if (
|
||||
(surface_size.x <= 0.0 or surface_size.y <= 0.0)
|
||||
and surface_polygon.size() < 3
|
||||
):
|
||||
continue
|
||||
var body := WATER_BODY_SCENE.instantiate() as WaterBodyAuthoring
|
||||
if body == null:
|
||||
|
|
@ -379,6 +386,9 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void:
|
|||
+ Vector3.UP * WATER_HEIGHT
|
||||
)
|
||||
body.rotation.y = angle
|
||||
if surface_polygon.size() >= 3:
|
||||
body.surface_polygon = surface_polygon
|
||||
else:
|
||||
body.surface_size = surface_size
|
||||
body.visual_surface_enabled = false
|
||||
body.water_material = _fresh_water_material()
|
||||
|
|
@ -394,6 +404,8 @@ func _fresh_water_location_tags(tags: PackedStringArray) -> Array[StringName]:
|
|||
result.append(&"pond")
|
||||
if "river" in tags:
|
||||
result.append(&"river")
|
||||
if "lake" in tags:
|
||||
result.append(&"lake")
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,16 +31,23 @@ gatherable_anchor_root = NodePath("GatherableAnchors")
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("2_generator")
|
||||
catalog = ExtResource("3_catalog")
|
||||
grid_size = Vector2i(15, 13)
|
||||
grid_size = Vector2i(20, 20)
|
||||
generation_seed = 13001
|
||||
generate_on_ready = false
|
||||
build_collision = true
|
||||
force_center_chunk_id = &"chunk_spawn"
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
|
||||
grass_sand_smoothing_enabled = true
|
||||
lake_feature_enabled = true
|
||||
lake_minimum_size = 7
|
||||
lake_maximum_size = 8
|
||||
river_feature_enabled = true
|
||||
river_minimum_length = 7
|
||||
river_maximum_length = 8
|
||||
elevated_cliff_feature_enabled = true
|
||||
elevated_cliff_coastal_feature_required = true
|
||||
required_chunk_weight_multiplier = 64.0
|
||||
maximum_backtracks = 100000
|
||||
maximum_backtracks = 1000
|
||||
|
||||
[node name="WaterBodies" type="Node3D" parent="."]
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@ class_name WaterBodyAuthoring
|
|||
extends Node3D
|
||||
|
||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
const GENERATED_POLYGON_SHAPE_META := &"water_body_generated_polygon_shape"
|
||||
|
||||
@export_group("Surface")
|
||||
## Visible water footprint in meters.
|
||||
|
|
@ -14,6 +15,13 @@ var surface_size: Vector2 = Vector2(10.0, 10.0):
|
|||
set(value):
|
||||
surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1))
|
||||
_sync_owned_nodes()
|
||||
## Optional exact local X/Z coverage. Three or more points replace the box
|
||||
## collision footprint with triangulated, vertically extruded volumes. The
|
||||
## visible PlaneMesh remains controlled by surface_size.
|
||||
@export var surface_polygon := PackedVector2Array():
|
||||
set(value):
|
||||
surface_polygon = value
|
||||
_sync_owned_nodes()
|
||||
## Derive fishing coverage from VisualWater's mesh bounds instead of resizing
|
||||
## the mesh from Surface Size. Use this for imported or shared water surfaces.
|
||||
@export var derive_coverage_from_visual_mesh: bool = false:
|
||||
|
|
@ -97,6 +105,7 @@ func _sync_owned_nodes() -> void:
|
|||
plane_mesh.size = surface_size
|
||||
visual_water.material_override = water_material
|
||||
var surface_footprint: Rect2 = _resolve_surface_footprint(visual_water)
|
||||
var uses_polygon := surface_polygon.size() >= 3
|
||||
|
||||
var fishing_shape_node := (
|
||||
get_node_or_null(fishing_shape_path) as CollisionShape3D
|
||||
|
|
@ -112,9 +121,19 @@ func _sync_owned_nodes() -> void:
|
|||
fishing_region.surface_height_mode = (
|
||||
FishableWaterRegion.SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||
)
|
||||
if fishing_shape_node != null:
|
||||
if fishing_shape_node != null and fishing_region != null:
|
||||
if uses_polygon and _sync_polygon_collision_shapes(
|
||||
fishing_region,
|
||||
fishing_shape_node,
|
||||
fishing_depth,
|
||||
):
|
||||
pass
|
||||
else:
|
||||
_clear_generated_polygon_shapes(fishing_region)
|
||||
var fishing_shape := fishing_shape_node.shape as BoxShape3D
|
||||
if fishing_shape != null:
|
||||
if fishing_shape == null:
|
||||
fishing_shape = BoxShape3D.new()
|
||||
fishing_shape_node.shape = fishing_shape
|
||||
# The authored root is the one water-surface height. Keep the fishable
|
||||
# volume entirely below that plane so visible water and interaction
|
||||
# can be moved together without a second height to tune.
|
||||
|
|
@ -133,18 +152,28 @@ func _sync_owned_nodes() -> void:
|
|||
var recovery_region := (
|
||||
get_node_or_null(recovery_region_path) as Area3D
|
||||
)
|
||||
if recovery_region != null:
|
||||
var recovery_shape_node := (
|
||||
get_node_or_null(recovery_shape_path) as CollisionShape3D
|
||||
)
|
||||
if recovery_region != null and recovery_shape_node != null:
|
||||
if uses_polygon and _sync_polygon_collision_shapes(
|
||||
recovery_region,
|
||||
recovery_shape_node,
|
||||
recovery_depth,
|
||||
):
|
||||
recovery_region.position = Vector3.ZERO
|
||||
else:
|
||||
_clear_generated_polygon_shapes(recovery_region)
|
||||
recovery_region.position = Vector3(
|
||||
surface_footprint.get_center().x,
|
||||
-recovery_depth * 0.5,
|
||||
surface_footprint.get_center().y,
|
||||
)
|
||||
var recovery_shape_node := (
|
||||
get_node_or_null(recovery_shape_path) as CollisionShape3D
|
||||
)
|
||||
if recovery_shape_node != null:
|
||||
recovery_shape_node.position = Vector3.ZERO
|
||||
var recovery_shape := recovery_shape_node.shape as BoxShape3D
|
||||
if recovery_shape != null:
|
||||
if recovery_shape == null:
|
||||
recovery_shape = BoxShape3D.new()
|
||||
recovery_shape_node.shape = recovery_shape
|
||||
recovery_shape.size = Vector3(
|
||||
surface_footprint.size.x,
|
||||
recovery_depth,
|
||||
|
|
@ -155,6 +184,57 @@ func _sync_owned_nodes() -> void:
|
|||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _sync_polygon_collision_shapes(
|
||||
region: CollisionObject3D,
|
||||
primary_shape_node: CollisionShape3D,
|
||||
depth: float,
|
||||
) -> bool:
|
||||
var triangle_indices := Geometry2D.triangulate_polygon(surface_polygon)
|
||||
if triangle_indices.size() < 3:
|
||||
return false
|
||||
_clear_generated_polygon_shapes(region)
|
||||
for offset: int in range(0, triangle_indices.size(), 3):
|
||||
var shape_node := primary_shape_node
|
||||
if offset > 0:
|
||||
shape_node = CollisionShape3D.new()
|
||||
shape_node.name = "GeneratedPolygonShape%d" % floori(
|
||||
float(offset) / 3.0
|
||||
)
|
||||
shape_node.set_meta(GENERATED_POLYGON_SHAPE_META, true)
|
||||
region.add_child(shape_node)
|
||||
shape_node.position = Vector3.ZERO
|
||||
shape_node.disabled = false
|
||||
shape_node.shape = _extruded_triangle_shape(
|
||||
triangle_indices,
|
||||
offset,
|
||||
depth,
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func _extruded_triangle_shape(
|
||||
triangle_indices: PackedInt32Array,
|
||||
offset: int,
|
||||
depth: float,
|
||||
) -> ConvexPolygonShape3D:
|
||||
var points := PackedVector3Array()
|
||||
for point_offset: int in 3:
|
||||
var point := surface_polygon[triangle_indices[offset + point_offset]]
|
||||
points.append(Vector3(point.x, 0.0, point.y))
|
||||
points.append(Vector3(point.x, -depth, point.y))
|
||||
var shape := ConvexPolygonShape3D.new()
|
||||
shape.points = points
|
||||
return shape
|
||||
|
||||
|
||||
func _clear_generated_polygon_shapes(region: CollisionObject3D) -> void:
|
||||
for child: Node in region.get_children():
|
||||
if not child.has_meta(GENERATED_POLYGON_SHAPE_META):
|
||||
continue
|
||||
region.remove_child(child)
|
||||
child.free()
|
||||
|
||||
|
||||
func _resolve_surface_footprint(visual_water: MeshInstance3D) -> Rect2:
|
||||
var authored_footprint := Rect2(-surface_size * 0.5, surface_size)
|
||||
if (
|
||||
|
|
@ -202,7 +282,14 @@ func _get_configuration_warnings() -> PackedStringArray:
|
|||
"VisualWater must provide a PlaneMesh when coverage is authored by size."
|
||||
)
|
||||
var fishing_shape := get_node_or_null(fishing_shape_path) as CollisionShape3D
|
||||
if fishing_shape == null or not fishing_shape.shape is BoxShape3D:
|
||||
if fishing_shape == null:
|
||||
warnings.append("FishingRegion must provide a collision shape.")
|
||||
elif surface_polygon.size() >= 3:
|
||||
if not fishing_shape.shape is ConvexPolygonShape3D:
|
||||
warnings.append(
|
||||
"FishingRegion must provide polygon coverage for Surface Polygon."
|
||||
)
|
||||
elif not fishing_shape.shape is BoxShape3D:
|
||||
warnings.append("FishingRegion must provide a BoxShape3D.")
|
||||
if manage_recovery_coverage:
|
||||
var recovery_region := (
|
||||
|
|
@ -220,7 +307,14 @@ func _get_configuration_warnings() -> PackedStringArray:
|
|||
var recovery_shape := (
|
||||
get_node_or_null(recovery_shape_path) as CollisionShape3D
|
||||
)
|
||||
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
|
||||
if recovery_shape == null:
|
||||
warnings.append("RecoveryRegion must provide a collision shape.")
|
||||
elif surface_polygon.size() >= 3:
|
||||
if not recovery_shape.shape is ConvexPolygonShape3D:
|
||||
warnings.append(
|
||||
"RecoveryRegion must provide polygon coverage for Surface Polygon."
|
||||
)
|
||||
elif not recovery_shape.shape is BoxShape3D:
|
||||
warnings.append("RecoveryRegion must provide a BoxShape3D.")
|
||||
var fishing_region := get_node_or_null(
|
||||
fishing_region_path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue