2032 lines
63 KiB
GDScript
2032 lines
63 KiB
GDScript
extends SceneTree
|
|
|
|
const CATALOG: TerrainChunkCatalog = preload(
|
|
"res://world/generation/chunks/terrain_chunk_catalog.tres"
|
|
)
|
|
const EXPECTED_IDS: Array[String] = [
|
|
"chunk_0000",
|
|
"chunk_0001",
|
|
"chunk_0002",
|
|
"chunk_0003",
|
|
"chunk_0004",
|
|
"chunk_0005",
|
|
"chunk_0006",
|
|
"chunk_0007",
|
|
"chunk_0008",
|
|
"chunk_0009",
|
|
"chunk_0010",
|
|
"chunk_0011",
|
|
"chunk_0012",
|
|
"chunk_0013",
|
|
"chunk_0014",
|
|
"chunk_0015",
|
|
"chunk_0016",
|
|
"chunk_0017",
|
|
"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_0033",
|
|
"chunk_0034",
|
|
"chunk_0035",
|
|
"chunk_0036",
|
|
"chunk_0037",
|
|
"chunk_9999",
|
|
"chunk_9998",
|
|
"chunk_9997",
|
|
"chunk_9996",
|
|
"chunk_9995",
|
|
"chunk_spawn",
|
|
]
|
|
const REQUIRED_IDS: Array[String] = [
|
|
"chunk_spawn",
|
|
"chunk_0001",
|
|
"chunk_0002",
|
|
"chunk_0003",
|
|
"chunk_0004",
|
|
]
|
|
const TEST_SEED := 13001
|
|
const CONNECTOR_STRESS_SEED := 287
|
|
const EXPECTED_SOLVER_VARIANT_COUNT := 35
|
|
const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
|
|
&"chunk_0000": 4,
|
|
&"chunk_0001": 4,
|
|
&"chunk_0002": 4,
|
|
&"chunk_0003": 4,
|
|
&"chunk_0004": 4,
|
|
&"chunk_0005": 4,
|
|
&"chunk_0006": 4,
|
|
&"chunk_0007": 4,
|
|
&"chunk_0008": 4,
|
|
&"chunk_0009": 4,
|
|
&"chunk_0010": 4,
|
|
&"chunk_0011": 4,
|
|
&"chunk_0012": 4,
|
|
&"chunk_0013": 4,
|
|
&"chunk_0014": 4,
|
|
&"chunk_0015": 4,
|
|
&"chunk_0016": 4,
|
|
&"chunk_0017": 4,
|
|
&"chunk_0018": 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_0033": 4,
|
|
&"chunk_0034": 4,
|
|
&"chunk_0035": 4,
|
|
&"chunk_0036": 4,
|
|
&"chunk_0037": 4,
|
|
&"chunk_9999": 4,
|
|
&"chunk_9998": 4,
|
|
&"chunk_9997": 4,
|
|
&"chunk_9996": 4,
|
|
&"chunk_9995": 4,
|
|
&"chunk_spawn": 1,
|
|
}
|
|
|
|
var _failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred(&"_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_validate_catalog()
|
|
_validate_profiles()
|
|
_validate_second_tier_coastal_layout()
|
|
_validate_generation()
|
|
_finish()
|
|
|
|
|
|
func _validate_catalog() -> void:
|
|
for error: String in CATALOG.validation_errors():
|
|
_check(false, error)
|
|
_check(
|
|
CATALOG.definitions.size() == EXPECTED_IDS.size(),
|
|
"Catalog must contain the authored chunks and spawn definition.",
|
|
)
|
|
for stable_id: String in EXPECTED_IDS:
|
|
var definition := CATALOG.definition_for_id(StringName(stable_id))
|
|
_check(definition != null, "Catalog is missing %s." % stable_id)
|
|
if definition == null:
|
|
continue
|
|
_check(
|
|
definition.packed_scene != null,
|
|
"%s must reference an imported GLB." % stable_id,
|
|
)
|
|
var grass := CATALOG.definition_for_id(&"chunk_0000")
|
|
var sand := CATALOG.definition_for_id(&"chunk_0001")
|
|
var beach := CATALOG.definition_for_id(&"chunk_0002")
|
|
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
|
|
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
|
|
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
|
|
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
|
|
var grass_sand_diagonal := CATALOG.definition_for_id(&"chunk_0013")
|
|
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_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_edge_variant := CATALOG.definition_for_id(&"chunk_0037")
|
|
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_bend_c := CATALOG.definition_for_id(&"chunk_0033")
|
|
var river_bend_b := CATALOG.definition_for_id(&"chunk_0034")
|
|
var river_bend_a := CATALOG.definition_for_id(&"chunk_0035")
|
|
var river_bend_d := CATALOG.definition_for_id(&"chunk_0036")
|
|
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(
|
|
(
|
|
grass != null
|
|
and sand != null
|
|
and beach != null
|
|
and grass_ocean_edge != null
|
|
and grass_beach_transition != null
|
|
and grass_ocean_corner != null
|
|
and beach_ocean_corner != null
|
|
and grass_sand_diagonal != null
|
|
and pond != null
|
|
and cliff_sea_edge != null
|
|
and cliff_sea_corner != null
|
|
and cliff_sea_transition_right != null
|
|
and cliff_sea_transition_left != null
|
|
and cliff_beach_transition_right != null
|
|
and cliff_beach_transition_left != null
|
|
and stream_variant != null
|
|
and lake_edge != null
|
|
and lake_edge_variant != 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_bend_a != null
|
|
and river_bend_b != null
|
|
and river_bend_c != null
|
|
and river_bend_d != 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.",
|
|
)
|
|
if (
|
|
grass != null
|
|
and sand != null
|
|
and beach != null
|
|
and grass_ocean_edge != null
|
|
and grass_beach_transition != null
|
|
and grass_ocean_corner != null
|
|
and beach_ocean_corner != null
|
|
and grass_sand_diagonal != null
|
|
and pond != null
|
|
and cliff_sea_edge != null
|
|
and cliff_sea_corner != null
|
|
and cliff_sea_transition_right != null
|
|
and cliff_sea_transition_left != null
|
|
and cliff_beach_transition_right != null
|
|
and cliff_beach_transition_left != null
|
|
and stream_variant != null
|
|
and lake_edge != null
|
|
and lake_edge_variant != 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_bend_a != null
|
|
and river_bend_b != null
|
|
and river_bend_c != null
|
|
and river_bend_d != 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(
|
|
sand.allows_non_water_neighbor(beach),
|
|
"Flat sand must permit an adjacent authored beach.",
|
|
)
|
|
_check(
|
|
sand.allows_non_water_neighbor(grass),
|
|
"Flat sand must permit the inland transition to grass.",
|
|
)
|
|
_check(
|
|
grass.allows_non_water_neighbor(sand),
|
|
"Grass must permit the inland side of a flat-sand transition.",
|
|
)
|
|
_check(
|
|
(
|
|
"sand" in sand.required_neighbor_tags
|
|
and "coast" in sand.required_neighbor_tags
|
|
and sand.maximum_boundary_distance == 1
|
|
),
|
|
(
|
|
"Every flat-sand chunk must remain attached to sand or authored "
|
|
+ "coast within the first inland terrain ring."
|
|
),
|
|
)
|
|
_check(
|
|
spawn.minimum_required_neighbors == 3,
|
|
"Spawn must retain its three-sided safe-grass requirement.",
|
|
)
|
|
_check(
|
|
beach.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The authored beach must descend eastward into ocean before rotation.",
|
|
)
|
|
_check(
|
|
grass_ocean_edge.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The authored grass cliff must face eastward ocean before rotation.",
|
|
)
|
|
_check(
|
|
grass_beach_transition.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The authored grass/beach join must face eastward ocean before rotation.",
|
|
)
|
|
_check(
|
|
grass_beach_transition.minimum_required_neighbors == 2
|
|
and "coast" in grass_beach_transition.required_neighbor_tags,
|
|
"The grass/beach join must remain between two coastline chunks.",
|
|
)
|
|
_check(
|
|
grass_beach_transition.allows_non_water_neighbor_on_edge(
|
|
grass,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
)
|
|
and not grass_beach_transition.allows_non_water_neighbor_on_edge(
|
|
sand,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
),
|
|
"The grass-backed transition edge must accept grass and reject sand.",
|
|
)
|
|
_check(
|
|
grass_beach_transition.allows_non_water_neighbor_on_edge(
|
|
beach,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
)
|
|
and grass_beach_transition.allows_non_water_neighbor_on_edge(
|
|
grass_ocean_edge,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
),
|
|
"The transition's coastline seams must retain their authored roles.",
|
|
)
|
|
_check(
|
|
not grass_ocean_edge.allows_non_water_neighbor_on_edge(
|
|
sand,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
),
|
|
"The straight grass coast's inland edge must reject sand.",
|
|
)
|
|
_check(
|
|
grass_ocean_corner.ocean_facing_edges
|
|
== (
|
|
(1 << int(TerrainChunkTopology.Edge.EAST))
|
|
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
|
|
),
|
|
"The authored grass corner must expose east and south to ocean.",
|
|
)
|
|
_check(
|
|
grass_ocean_corner.minimum_required_neighbors == 2
|
|
and (
|
|
"grass_ocean_edge"
|
|
in grass_ocean_corner.required_neighbor_tags
|
|
),
|
|
"The grass corner must join two straight grass ocean edges.",
|
|
)
|
|
_check(
|
|
grass_ocean_corner.allows_non_water_neighbor_on_edge(
|
|
grass_ocean_edge,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
)
|
|
and grass_ocean_corner.allows_non_water_neighbor_on_edge(
|
|
grass_ocean_edge,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
),
|
|
"Both corner land seams must accept straight grass ocean edges.",
|
|
)
|
|
_check(
|
|
beach_ocean_corner.ocean_facing_edges
|
|
== (
|
|
(1 << int(TerrainChunkTopology.Edge.EAST))
|
|
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
|
|
),
|
|
"The authored beach corner must expose east and south to ocean.",
|
|
)
|
|
_check(
|
|
beach_ocean_corner.minimum_required_neighbors == 2
|
|
and (
|
|
"beach_ocean_edge"
|
|
in beach_ocean_corner.required_neighbor_tags
|
|
),
|
|
"The beach corner must join two straight beach ocean edges.",
|
|
)
|
|
_check(
|
|
grass_sand_diagonal.must_be_interior
|
|
and not grass_sand_diagonal.participates_in_base_solver
|
|
and grass_sand_diagonal.maximum_boundary_distance == 2
|
|
and "mixed_surface" in grass_sand_diagonal.tags,
|
|
(
|
|
"The grass-sand diagonal must remain an inland post-process "
|
|
+ "mixed-surface chunk."
|
|
),
|
|
)
|
|
_check(
|
|
grass_sand_diagonal.surface_tags_for_edge(
|
|
TerrainChunkTopology.Edge.NORTH
|
|
) == PackedStringArray(["sand"])
|
|
and grass_sand_diagonal.surface_tags_for_edge(
|
|
TerrainChunkTopology.Edge.WEST
|
|
) == PackedStringArray(["sand"])
|
|
and grass_sand_diagonal.surface_tags_for_edge(
|
|
TerrainChunkTopology.Edge.EAST
|
|
) == PackedStringArray(["grass"])
|
|
and grass_sand_diagonal.surface_tags_for_edge(
|
|
TerrainChunkTopology.Edge.SOUTH
|
|
) == PackedStringArray(["grass"]),
|
|
"The diagonal's authored edges must retain two sand and two grass sides.",
|
|
)
|
|
var stream := CATALOG.definition_for_id(&"chunk_0003")
|
|
_check(
|
|
pond.water_inlet_edges == pond.water_outlet_edges
|
|
and pond.water_inlet_edges
|
|
== (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
|
|
and stream_variant.water_inlet_edges == stream.water_inlet_edges
|
|
and stream_variant.water_outlet_edges == stream.water_outlet_edges
|
|
and stream_variant.water_surface_size == stream.water_surface_size
|
|
and stream.water_surface_offset.x < 0.0
|
|
and stream_variant.water_surface_offset.x > 0.0
|
|
and is_equal_approx(
|
|
stream_variant.water_surface_offset.y,
|
|
stream.water_surface_offset.y,
|
|
),
|
|
(
|
|
"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_edge_variant.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_edge_variant.water_inlet_edges
|
|
== lake_edge.water_inlet_edges
|
|
and lake_edge_variant.water_outlet_edges
|
|
== lake_edge.water_outlet_edges
|
|
and lake_edge_variant.water_surface_size
|
|
== lake_edge.water_surface_size
|
|
and lake_edge_variant.water_surface_offset
|
|
== lake_edge.water_surface_offset
|
|
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_edge_variant.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_bend_a.participates_in_base_solver
|
|
and not river_bend_b.participates_in_base_solver
|
|
and not river_bend_c.participates_in_base_solver
|
|
and not river_bend_d.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_bend_a.water_inlet_edges == 14
|
|
and river_bend_a.water_outlet_edges == 14
|
|
and river_bend_b.water_inlet_edges == 12
|
|
and river_bend_b.water_outlet_edges == 12
|
|
and river_bend_c.water_inlet_edges == 15
|
|
and river_bend_c.water_outlet_edges == 15
|
|
and river_bend_c.continuous_water_seam_edges == 15
|
|
and river_bend_d.water_inlet_edges == 13
|
|
and river_bend_d.water_outlet_edges == 13
|
|
and river_bend_d.continuous_water_seam_edges == 1
|
|
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_bend_a" in river_bend_a.tags
|
|
and "river_bend_b" in river_bend_b.tags
|
|
and "river_bend_c" in river_bend_c.tags
|
|
and "river_bend_d" in river_bend_d.tags
|
|
and "river" in river_cap.tags
|
|
),
|
|
(
|
|
"The river banks, bend, cliff source, ocean outlet, and legacy "
|
|
+ "caps must remain connected freshwater chunks."
|
|
),
|
|
)
|
|
for cliff_definition: TerrainChunkDefinition in [
|
|
cliff_top,
|
|
cliff_corner,
|
|
cliff_edge,
|
|
cliff_ramp,
|
|
]:
|
|
_check(
|
|
cliff_definition.overlay_only
|
|
and cliff_definition.must_be_interior
|
|
and cliff_definition.base_layer_scene != null
|
|
and cliff_definition.base_layer_mesh_name == &"chunk_0000",
|
|
"Every second-tier piece must overlay level-one grass inland.",
|
|
)
|
|
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,
|
|
]:
|
|
_check(
|
|
coastal_cliff.overlay_only
|
|
and coastal_cliff.base_layer_scene == null
|
|
and not coastal_cliff.must_be_interior
|
|
and coastal_cliff.prefers_map_boundary
|
|
and "coast" in coastal_cliff.tags
|
|
and "elevation_2_sea_edge" in coastal_cliff.tags,
|
|
(
|
|
"Each second-tier sea piece must be a complete coastal "
|
|
+ "overlay without an inland grass base."
|
|
),
|
|
)
|
|
for transition: TerrainChunkDefinition in [
|
|
cliff_sea_transition_right,
|
|
cliff_sea_transition_left,
|
|
]:
|
|
_check(
|
|
transition.overlay_only
|
|
and transition.base_layer_scene != null
|
|
and transition.base_layer_mesh_name == &"chunk_0007"
|
|
and not transition.must_be_interior
|
|
and transition.prefers_map_boundary
|
|
and "transition" in transition.tags,
|
|
(
|
|
"Each raised-coast endpoint must layer the deep corner "
|
|
+ "over an authored level-one coastline."
|
|
),
|
|
)
|
|
_check(
|
|
cliff_sea_transition_right.base_layer_quarter_turns == 3
|
|
and cliff_sea_transition_right.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.SOUTH)),
|
|
"The right coastal transition must align its south-facing base coast.",
|
|
)
|
|
_check(
|
|
cliff_sea_transition_left.base_layer_quarter_turns == 1
|
|
and cliff_sea_transition_left.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The left coastal transition must align its east-facing base coast.",
|
|
)
|
|
for transition: TerrainChunkDefinition in [
|
|
cliff_beach_transition_right,
|
|
cliff_beach_transition_left,
|
|
]:
|
|
_check(
|
|
transition.overlay_only
|
|
and transition.base_layer_scene != null
|
|
and transition.base_layer_mesh_name == &"chunk_0008"
|
|
and not transition.must_be_interior
|
|
and transition.prefers_map_boundary
|
|
and "sand" in transition.tags
|
|
and "transition" in transition.tags,
|
|
(
|
|
"Each raised beach endpoint must close over an authored "
|
|
+ "level-one beach corner."
|
|
),
|
|
)
|
|
_check(
|
|
cliff_beach_transition_right.base_layer_quarter_turns == 3
|
|
and cliff_beach_transition_right.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.SOUTH))
|
|
and cliff_beach_transition_left.base_layer_quarter_turns == 1
|
|
and cliff_beach_transition_left.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The raised beach endpoint corners must face their open coastline.",
|
|
)
|
|
_check(
|
|
cliff_sea_edge.ocean_facing_edges
|
|
== (1 << int(TerrainChunkTopology.Edge.EAST)),
|
|
"The second-tier sea edge must face east before rotation.",
|
|
)
|
|
_check(
|
|
cliff_sea_corner.ocean_facing_edges
|
|
== (
|
|
(1 << int(TerrainChunkTopology.Edge.EAST))
|
|
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
|
|
),
|
|
"The second-tier sea corner must face east and south before rotation.",
|
|
)
|
|
_check(
|
|
cliff_sea_corner.minimum_required_neighbors == 2
|
|
and (
|
|
"elevation_2_sea_edge"
|
|
in cliff_sea_corner.required_neighbor_tags
|
|
),
|
|
"The second-tier sea corner must join two straight sea edges.",
|
|
)
|
|
_check(
|
|
cliff_top.minimum_required_neighbors == 4
|
|
and "elevation_2" in cliff_top.required_neighbor_tags,
|
|
"The second-tier top must be enclosed on all four sides.",
|
|
)
|
|
_check(
|
|
"ramp" in cliff_ramp.tags
|
|
and cliff_ramp.maximum_placements > 0,
|
|
"The second-tier ramp must remain available without being required.",
|
|
)
|
|
_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:
|
|
for definition: TerrainChunkDefinition in CATALOG.definitions:
|
|
var variants := TerrainChunkAnalyzer.create_variants(
|
|
definition,
|
|
CATALOG.chunk_size,
|
|
)
|
|
_check(
|
|
not variants.is_empty(),
|
|
"%s must produce at least one rotation variant."
|
|
% definition.stable_id,
|
|
)
|
|
for variant: TerrainChunkVariant in variants:
|
|
_check(
|
|
variant.edge_profiles.size() == 4,
|
|
"%s must expose four edge profiles." % variant.stable_key(),
|
|
)
|
|
var ocean_edges := variant.rotated_edge_mask(
|
|
definition.ocean_facing_edges
|
|
)
|
|
for edge_index: int in variant.edge_profiles.size():
|
|
var profile := variant.edge_profiles[edge_index]
|
|
_check(
|
|
(
|
|
not profile.points.is_empty()
|
|
or (ocean_edges & (1 << edge_index)) != 0
|
|
),
|
|
(
|
|
"%s has an empty non-ocean edge profile."
|
|
% variant.stable_key()
|
|
),
|
|
)
|
|
|
|
|
|
func _validate_second_tier_coastal_layout() -> void:
|
|
var generator := TerrainChunkGenerator.new()
|
|
generator.catalog = CATALOG
|
|
generator.grid_size = Vector2i(4, 4)
|
|
generator.generate_on_ready = false
|
|
generator.build_collision = false
|
|
generator.force_center_chunk_id = &""
|
|
root.add_child(generator)
|
|
var keys := PackedStringArray()
|
|
for row: int in generator.grid_size.y:
|
|
for column: int in generator.grid_size.x:
|
|
var at_north := row == 0
|
|
var at_east := column == generator.grid_size.x - 1
|
|
var at_south := row == generator.grid_size.y - 1
|
|
var at_west := column == 0
|
|
if at_north and at_west:
|
|
keys.append("chunk_0015@2")
|
|
elif at_north and at_east:
|
|
keys.append("chunk_0015@1")
|
|
elif at_south and at_east:
|
|
keys.append("chunk_0015@0")
|
|
elif at_south and at_west:
|
|
keys.append("chunk_0015@3")
|
|
elif at_north:
|
|
keys.append("chunk_0014@1")
|
|
elif at_east:
|
|
keys.append("chunk_0014@0")
|
|
elif at_south:
|
|
keys.append("chunk_0014@3")
|
|
elif at_west:
|
|
keys.append("chunk_0014@2")
|
|
else:
|
|
keys.append("chunk_0009@0")
|
|
_check(
|
|
generator.generate_from_placement_keys(keys),
|
|
(
|
|
"The second-tier sea edge and corner must form a complete "
|
|
+ "raised coastal perimeter."
|
|
),
|
|
)
|
|
if generator.placement_keys().size() == keys.size():
|
|
_check(
|
|
_all_neighbor_edges_match(generator),
|
|
"Every raised coastal-perimeter seam must match.",
|
|
)
|
|
_check(
|
|
_count_placements(generator.placement_keys(), "chunk_0014") == 8
|
|
and _count_placements(
|
|
generator.placement_keys(), "chunk_0015"
|
|
) == 4,
|
|
"The raised coastal perimeter must use its straight and corner pieces.",
|
|
)
|
|
for record: Dictionary in generator.placement_records():
|
|
var stable_id: StringName = record.get("stable_id", &"")
|
|
if stable_id not in [&"chunk_0014", &"chunk_0015"]:
|
|
continue
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate", Vector2i.ZERO
|
|
)
|
|
var variant := _find_variant(
|
|
generator,
|
|
stable_id,
|
|
int(record.get("rotation_quarters", 0)),
|
|
)
|
|
_check(
|
|
variant != null
|
|
and generator._variant_respects_ocean_boundary(
|
|
variant, coordinate
|
|
),
|
|
"Every second-tier sea opening must face outside the terrain grid.",
|
|
)
|
|
var generated := generator.get_generated_chunks_root()
|
|
if generated != null:
|
|
for child: Node in generated.get_children():
|
|
var stable_id := StringName(
|
|
child.get_meta(&"terrain_chunk_id", &"")
|
|
)
|
|
if stable_id in [&"chunk_0014", &"chunk_0015"]:
|
|
_check(
|
|
child.get_node_or_null("TerrainBaseLayer") == null,
|
|
"Second-tier sea pieces must not render inland grass over ocean.",
|
|
)
|
|
generator.free()
|
|
|
|
|
|
func _validate_generation() -> void:
|
|
var first := _make_generator()
|
|
root.add_child(first)
|
|
var first_solved := first.generate()
|
|
_check(first_solved, "Generator must solve the prototype 9x9 grid.")
|
|
if not first_solved:
|
|
first.free()
|
|
return
|
|
var first_keys := first.placement_keys()
|
|
_check(first_keys.size() == 81, "Generator must place exactly 81 chunks.")
|
|
for stable_id: String in REQUIRED_IDS:
|
|
_check(
|
|
_placements_contain(first_keys, stable_id),
|
|
"Generated diagnostic must contain %s." % stable_id,
|
|
)
|
|
_check(
|
|
_all_neighbor_edges_match(first),
|
|
"Every generated neighboring edge must match.",
|
|
)
|
|
_check(
|
|
_count_placements(first_keys, "chunk_spawn") == 1,
|
|
"Generated layouts must contain exactly one spawn chunk.",
|
|
)
|
|
_check(
|
|
first_keys[40].begins_with("chunk_spawn@"),
|
|
"The spawn chunk must occupy the center cell.",
|
|
)
|
|
_validate_layout_rules(first)
|
|
_validate_authored_rotations(first)
|
|
_validate_generation_summary(first)
|
|
var first_generated_root := first.get_generated_chunks_root()
|
|
_check(
|
|
first.generate(),
|
|
"Repeated generation on one generator must solve.",
|
|
)
|
|
_check(
|
|
first.placement_keys() == first_keys,
|
|
"Repeated generation on one generator must remain deterministic.",
|
|
)
|
|
_check(
|
|
first.get_generated_chunks_root() != first_generated_root,
|
|
"Successful regeneration must replace the generated scene atomically.",
|
|
)
|
|
|
|
var second := _make_generator()
|
|
root.add_child(second)
|
|
var second_solved := second.generate()
|
|
_check(second_solved, "Repeated deterministic generation must solve.")
|
|
if not second_solved:
|
|
first.free()
|
|
second.free()
|
|
return
|
|
_check(
|
|
second.placement_keys() == first_keys,
|
|
"The same seed and catalog must produce the same layout.",
|
|
)
|
|
var restored := _make_generator()
|
|
restored.generation_seed = TEST_SEED + 999
|
|
root.add_child(restored)
|
|
_check(
|
|
restored.generate_from_placement_keys(first_keys),
|
|
"A resolved placement manifest must rebuild successfully.",
|
|
)
|
|
_check(
|
|
restored.placement_keys() == first_keys,
|
|
"A resolved placement manifest must preserve exact rotations.",
|
|
)
|
|
first.free()
|
|
second.free()
|
|
restored.free()
|
|
|
|
for seed_offset: int in range(1, 6):
|
|
var generator := _make_generator()
|
|
generator.generation_seed = TEST_SEED + seed_offset
|
|
root.add_child(generator)
|
|
var solved := generator.generate()
|
|
_check(
|
|
solved,
|
|
"Generator must solve seed %d." % generator.generation_seed,
|
|
)
|
|
if solved:
|
|
var keys := generator.placement_keys()
|
|
for stable_id: String in REQUIRED_IDS:
|
|
_check(
|
|
_placements_contain(keys, stable_id),
|
|
"Seed %d must contain %s."
|
|
% [generator.generation_seed, stable_id],
|
|
)
|
|
_check(
|
|
_all_neighbor_edges_match(generator),
|
|
"Seed %d has a mismatched neighboring edge."
|
|
% generator.generation_seed,
|
|
)
|
|
_validate_layout_rules(generator)
|
|
generator.free()
|
|
|
|
var connector_stress := _make_generator()
|
|
connector_stress.generation_seed = CONNECTOR_STRESS_SEED
|
|
connector_stress.maximum_backtracks = 100
|
|
root.add_child(connector_stress)
|
|
_check(
|
|
connector_stress.generate(),
|
|
"Connector propagation must solve the known stress seed.",
|
|
)
|
|
_check(
|
|
connector_stress._backtrack_count < 100,
|
|
"Connector propagation must reject unsupported branches early.",
|
|
)
|
|
connector_stress.free()
|
|
|
|
|
|
func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
|
|
var counts: Dictionary[StringName, int] = {}
|
|
for variant: TerrainChunkVariant in generator._variants:
|
|
var stable_id := variant.definition.stable_id
|
|
counts[stable_id] = counts.get(stable_id, 0) + 1
|
|
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
|
|
_check(
|
|
counts.get(stable_id, 0) == EXPECTED_VARIANT_COUNTS[stable_id],
|
|
"%s must preserve every explicitly allowed rotation." % stable_id,
|
|
)
|
|
|
|
var stream := CATALOG.definition_for_id(&"chunk_0003")
|
|
var alternate_stream := CATALOG.definition_for_id(&"chunk_0016")
|
|
_check(
|
|
stream != null and alternate_stream != null,
|
|
"Both stream visual definitions must be available.",
|
|
)
|
|
if stream == null or alternate_stream == null:
|
|
return
|
|
for quarter_turns: int in 4:
|
|
var stream_variant := _find_variant(
|
|
generator,
|
|
&"chunk_0003",
|
|
quarter_turns,
|
|
)
|
|
_check(
|
|
stream_variant != null,
|
|
"The stream must support rotation %d." % quarter_turns,
|
|
)
|
|
var alternate_variant := _find_variant(
|
|
generator,
|
|
&"chunk_0016",
|
|
quarter_turns,
|
|
)
|
|
_check(
|
|
alternate_variant != null,
|
|
"The alternate stream must support rotation %d." % quarter_turns,
|
|
)
|
|
if stream_variant == null or alternate_variant == null:
|
|
continue
|
|
var expected_inlet := 1 << int(TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
quarter_turns,
|
|
))
|
|
var expected_outlet := 1 << int(TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
quarter_turns,
|
|
))
|
|
_check(
|
|
stream_variant.rotated_edge_mask(stream.water_inlet_edges)
|
|
== expected_inlet,
|
|
"The rotated stream inlet must follow its authored orientation.",
|
|
)
|
|
_check(
|
|
stream_variant.rotated_edge_mask(stream.water_outlet_edges)
|
|
== expected_outlet,
|
|
"The rotated stream outlet must follow its authored orientation.",
|
|
)
|
|
_check(
|
|
alternate_variant.rotated_edge_mask(
|
|
alternate_stream.water_inlet_edges
|
|
) == expected_inlet
|
|
and alternate_variant.rotated_edge_mask(
|
|
alternate_stream.water_outlet_edges
|
|
) == expected_outlet,
|
|
"The alternate stream connectors must rotate exactly like the original.",
|
|
)
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
_check(
|
|
stream_variant.profile(edge).signature(
|
|
TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION
|
|
) == alternate_variant.profile(edge).signature(
|
|
TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION
|
|
),
|
|
"Both stream visuals must expose identical terrain edge profiles.",
|
|
)
|
|
|
|
var beach := CATALOG.definition_for_id(&"chunk_0002")
|
|
_check(beach != null, "The beach definition must be available.")
|
|
if beach == null:
|
|
return
|
|
for quarter_turns: int in 4:
|
|
var beach_variant := _find_variant(
|
|
generator,
|
|
&"chunk_0002",
|
|
quarter_turns,
|
|
)
|
|
_check(
|
|
beach_variant != null,
|
|
"The beach must support rotation %d." % quarter_turns,
|
|
)
|
|
if beach_variant == null:
|
|
continue
|
|
var expected_ocean_edge := 1 << int(
|
|
TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.EAST,
|
|
quarter_turns,
|
|
)
|
|
)
|
|
_check(
|
|
beach_variant.rotated_edge_mask(beach.ocean_facing_edges)
|
|
== expected_ocean_edge,
|
|
"The rotated beach must keep its low edge facing the ocean.",
|
|
)
|
|
_check(
|
|
not generator._variant_respects_ocean_boundary(
|
|
beach_variant,
|
|
Vector2i(3, 3),
|
|
),
|
|
"A beach slope must never direct its low edge into the map.",
|
|
)
|
|
var boundary_coordinate := Vector2i(3, 3)
|
|
match TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.EAST,
|
|
quarter_turns,
|
|
):
|
|
TerrainChunkTopology.Edge.NORTH:
|
|
boundary_coordinate.y = 0
|
|
TerrainChunkTopology.Edge.EAST:
|
|
boundary_coordinate.x = generator.grid_size.x - 1
|
|
TerrainChunkTopology.Edge.SOUTH:
|
|
boundary_coordinate.y = generator.grid_size.y - 1
|
|
TerrainChunkTopology.Edge.WEST:
|
|
boundary_coordinate.x = 0
|
|
_check(
|
|
generator._variant_respects_ocean_boundary(
|
|
beach_variant,
|
|
boundary_coordinate,
|
|
),
|
|
"A beach slope must be legal when its low edge faces open ocean.",
|
|
)
|
|
|
|
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
|
|
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
|
|
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
|
|
var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
|
|
_check(
|
|
(
|
|
grass_ocean_edge != null
|
|
and grass_beach_transition != null
|
|
and grass_ocean_corner != null
|
|
and beach_ocean_corner != null
|
|
),
|
|
"The authored coastline additions must be available.",
|
|
)
|
|
if (
|
|
grass_ocean_edge == null
|
|
or grass_beach_transition == null
|
|
or grass_ocean_corner == null
|
|
or beach_ocean_corner == null
|
|
):
|
|
return
|
|
var beach_zero := _find_variant(generator, &"chunk_0002", 0)
|
|
var grass_edge_zero := _find_variant(generator, &"chunk_0005", 0)
|
|
var grass_edge_south := _find_variant(generator, &"chunk_0005", 3)
|
|
var transition_zero := _find_variant(generator, &"chunk_0006", 0)
|
|
var corner_zero := _find_variant(generator, &"chunk_0007", 0)
|
|
var beach_corner_zero := _find_variant(generator, &"chunk_0008", 0)
|
|
_check(
|
|
beach_zero != null
|
|
and grass_edge_zero != null
|
|
and grass_edge_south != null
|
|
and transition_zero != null
|
|
and corner_zero != null
|
|
and beach_corner_zero != null,
|
|
"The unrotated coastline variants must be available.",
|
|
)
|
|
if (
|
|
beach_zero != null
|
|
and grass_edge_zero != null
|
|
and grass_edge_south != null
|
|
and transition_zero != null
|
|
and corner_zero != null
|
|
and beach_corner_zero != null
|
|
):
|
|
_check(
|
|
grass_edge_zero.profile(
|
|
TerrainChunkTopology.Edge.EAST
|
|
).points.is_empty(),
|
|
"The grass cliff's authored east edge must remain open to ocean.",
|
|
)
|
|
_check(
|
|
transition_zero.profile(
|
|
TerrainChunkTopology.Edge.NORTH
|
|
).matches(
|
|
beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The transition's north side must match the authored beach.",
|
|
)
|
|
_check(
|
|
transition_zero.profile(
|
|
TerrainChunkTopology.Edge.SOUTH
|
|
).matches(
|
|
grass_edge_zero.profile(TerrainChunkTopology.Edge.NORTH),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The transition's south side must match the grass cliff.",
|
|
)
|
|
_check(
|
|
corner_zero.profile(TerrainChunkTopology.Edge.EAST).points.is_empty()
|
|
and corner_zero.profile(
|
|
TerrainChunkTopology.Edge.SOUTH
|
|
).points.is_empty(),
|
|
"The corner's authored east and south edges must remain open to ocean.",
|
|
)
|
|
_check(
|
|
corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
|
|
grass_edge_zero.profile(TerrainChunkTopology.Edge.SOUTH),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The corner's north side must join a straight grass ocean edge.",
|
|
)
|
|
_check(
|
|
corner_zero.profile(TerrainChunkTopology.Edge.WEST).matches(
|
|
grass_edge_south.profile(TerrainChunkTopology.Edge.EAST),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The corner's west side must join a straight grass ocean edge.",
|
|
)
|
|
_check(
|
|
beach_corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
|
|
beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The beach corner's north side must join a straight beach.",
|
|
)
|
|
var beach_south := _find_variant(generator, &"chunk_0002", 3)
|
|
_check(
|
|
beach_south != null
|
|
and beach_corner_zero.profile(
|
|
TerrainChunkTopology.Edge.WEST
|
|
).matches(
|
|
beach_south.profile(TerrainChunkTopology.Edge.EAST),
|
|
generator.edge_match_tolerance,
|
|
),
|
|
"The beach corner's west side must join a rotated straight beach.",
|
|
)
|
|
var cliff_top_zero := _find_variant(generator, &"chunk_0009", 0)
|
|
var cliff_edge_zero := _find_variant(generator, &"chunk_0011", 0)
|
|
var cliff_edge_south := _find_variant(generator, &"chunk_0011", 3)
|
|
var cliff_sea_edge_zero := _find_variant(generator, &"chunk_0014", 0)
|
|
var cliff_sea_edge_south := _find_variant(generator, &"chunk_0014", 3)
|
|
var cliff_sea_corner_zero := _find_variant(generator, &"chunk_0015", 0)
|
|
_check(
|
|
cliff_top_zero != null
|
|
and cliff_edge_zero != null
|
|
and cliff_edge_south != null
|
|
and cliff_sea_edge_zero != null
|
|
and cliff_sea_edge_south != null
|
|
and cliff_sea_corner_zero != null,
|
|
"The inland and coastal second-tier cliff variants must be available.",
|
|
)
|
|
if (
|
|
cliff_top_zero != null
|
|
and cliff_edge_zero != null
|
|
and cliff_edge_south != null
|
|
and cliff_sea_edge_zero != null
|
|
and cliff_sea_edge_south != null
|
|
and cliff_sea_corner_zero != null
|
|
):
|
|
_check(
|
|
cliff_sea_edge_zero.profile(
|
|
TerrainChunkTopology.Edge.EAST
|
|
).points.is_empty(),
|
|
"The second-tier sea edge must remain open on its ocean side.",
|
|
)
|
|
_check(
|
|
cliff_sea_corner_zero.profile(
|
|
TerrainChunkTopology.Edge.EAST
|
|
).points.is_empty()
|
|
and cliff_sea_corner_zero.profile(
|
|
TerrainChunkTopology.Edge.SOUTH
|
|
).points.is_empty(),
|
|
"The second-tier sea corner must retain both ocean openings.",
|
|
)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
cliff_sea_edge_zero,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
cliff_top_zero,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
),
|
|
"The second-tier sea edge must join the raised grass top inland.",
|
|
)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
cliff_sea_corner_zero,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
cliff_sea_edge_zero,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
cliff_sea_corner_zero,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
cliff_sea_edge_south,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
),
|
|
"The second-tier sea corner must join two straight sea edges.",
|
|
)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
cliff_sea_edge_zero,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
cliff_edge_zero,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
cliff_sea_edge_zero,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
cliff_edge_zero,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
),
|
|
(
|
|
"A coastal cliff edge must transition directly into its "
|
|
+ "inland edge counterpart."
|
|
),
|
|
)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
cliff_sea_corner_zero,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
cliff_edge_zero,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
cliff_sea_corner_zero,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
cliff_edge_south,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
),
|
|
(
|
|
"A coastal cliff corner must transition directly into "
|
|
+ "ordinary inland cliff edges."
|
|
),
|
|
)
|
|
var flat_grass := _find_variant(generator, &"chunk_0000", 0)
|
|
var flat_sand := _find_variant(generator, &"chunk_0001", 0)
|
|
_check(
|
|
flat_grass != null and flat_sand != null,
|
|
"Flat grass and sand variants must be available for seam validation.",
|
|
)
|
|
if flat_grass != null and flat_sand != null:
|
|
for quarter_turns: int in 4:
|
|
var transition := _find_variant(
|
|
generator,
|
|
&"chunk_0006",
|
|
quarter_turns,
|
|
)
|
|
if transition == null:
|
|
continue
|
|
var inland_edge := TerrainChunkTopology.rotated_edge(
|
|
TerrainChunkTopology.Edge.WEST,
|
|
quarter_turns,
|
|
)
|
|
var neighbor_edge := TerrainChunkTopology.opposite_edge(inland_edge)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
transition,
|
|
inland_edge,
|
|
flat_grass,
|
|
neighbor_edge,
|
|
),
|
|
"Every transition rotation must accept grass behind it.",
|
|
)
|
|
_check(
|
|
not generator._edges_are_compatible(
|
|
transition,
|
|
inland_edge,
|
|
flat_sand,
|
|
neighbor_edge,
|
|
),
|
|
"No transition rotation may accept sand behind it.",
|
|
)
|
|
for quarter_turns: int in 4:
|
|
var diagonal := _find_variant(
|
|
generator,
|
|
&"chunk_0013",
|
|
quarter_turns,
|
|
)
|
|
_check(
|
|
diagonal != null,
|
|
"The grass-sand diagonal must support rotation %d."
|
|
% quarter_turns,
|
|
)
|
|
if diagonal == null:
|
|
continue
|
|
for source_edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var source_edge := source_edge_value as TerrainChunkTopology.Edge
|
|
var edge := TerrainChunkTopology.rotated_edge(
|
|
source_edge,
|
|
quarter_turns,
|
|
)
|
|
var neighbor_edge := TerrainChunkTopology.opposite_edge(edge)
|
|
var expects_sand := source_edge in [
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
]
|
|
var expected := flat_sand if expects_sand else flat_grass
|
|
var rejected := flat_grass if expects_sand else flat_sand
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
diagonal,
|
|
edge,
|
|
expected,
|
|
neighbor_edge,
|
|
),
|
|
"Each rotated diagonal edge must accept its authored surface.",
|
|
)
|
|
_check(
|
|
not generator._edges_are_compatible(
|
|
diagonal,
|
|
edge,
|
|
rejected,
|
|
neighbor_edge,
|
|
),
|
|
"No rotated diagonal edge may accept the opposite surface.",
|
|
)
|
|
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)
|
|
var lake_regular_variant_top := _find_variant(generator, &"chunk_0037", 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
|
|
and lake_regular_variant_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
|
|
and lake_regular_variant_top != null
|
|
):
|
|
_check(
|
|
lake_regular_variant_top.topology_signature(0.001)
|
|
== lake_regular_top.topology_signature(0.001),
|
|
"Both regular lake edge visuals must expose identical seams.",
|
|
)
|
|
_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_edge_vertical_left := _find_variant(generator, &"chunk_0024", 1)
|
|
var river_edge_vertical_right := _find_variant(generator, &"chunk_0025", 3)
|
|
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 river_bend_a := _find_variant(generator, &"chunk_0035", 0)
|
|
var river_bend_b := _find_variant(generator, &"chunk_0034", 0)
|
|
var river_bend_c := _find_variant(generator, &"chunk_0033", 2)
|
|
var river_bend_d := _find_variant(generator, &"chunk_0036", 0)
|
|
var beach_east := _find_variant(generator, &"chunk_0002", 0)
|
|
var canonical_bend_specs := generator._river_bend_placement_specs(
|
|
Vector2i(5, 5),
|
|
0,
|
|
)
|
|
var rotated_bend_specs := generator._river_bend_placement_specs(
|
|
Vector2i(5, 5),
|
|
1,
|
|
)
|
|
_check(
|
|
(
|
|
canonical_bend_specs.size() == 4
|
|
and canonical_bend_specs[2]["id"] == &"chunk_0033"
|
|
and int(canonical_bend_specs[2]["turns"]) == 2
|
|
and rotated_bend_specs.size() == 4
|
|
and rotated_bend_specs[2]["id"] == &"chunk_0033"
|
|
and int(rotated_bend_specs[2]["turns"]) == 3
|
|
),
|
|
"River bend C must retain its authored half-turn at every bend rotation.",
|
|
)
|
|
_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_edge_vertical_left != null
|
|
and river_edge_vertical_right != null
|
|
and river_source_right != null
|
|
and river_source_left != null
|
|
and river_outlet_right != null
|
|
and river_outlet_left != null
|
|
and river_bend_a != null
|
|
and river_bend_b != null
|
|
and river_bend_c != null
|
|
and river_bend_d != null
|
|
and beach_east != null
|
|
),
|
|
(
|
|
"Every authored river-bank, bend, 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_edge_vertical_left != null
|
|
and river_edge_vertical_right != null
|
|
and river_source_right != null
|
|
and river_source_left != null
|
|
and river_outlet_right != null
|
|
and river_outlet_left != null
|
|
and river_bend_a != null
|
|
and river_bend_b != null
|
|
and river_bend_c != null
|
|
and river_bend_d != 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_bend_a,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
river_bend_b,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_bend_a,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
river_bend_c,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_bend_b,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
river_bend_d,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_bend_c,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
river_bend_d,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
),
|
|
"The four authored river-bend quadrants must form one continuous turn.",
|
|
)
|
|
_check(
|
|
generator._edges_are_compatible(
|
|
river_edge_top,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
river_bend_a,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_edge_variant_bottom,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
river_bend_c,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_bend_c,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
river_edge_vertical_left,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
)
|
|
and generator._edges_are_compatible(
|
|
river_bend_d,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
river_edge_vertical_right,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
),
|
|
"The bend must connect both incoming and outgoing authored 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",
|
|
&"chunk_0007",
|
|
&"chunk_0008",
|
|
&"chunk_0014",
|
|
&"chunk_0015",
|
|
&"chunk_9999",
|
|
&"chunk_9998",
|
|
&"chunk_9997",
|
|
&"chunk_9996",
|
|
]:
|
|
for quarter_turns: int in 4:
|
|
var coast_variant := _find_variant(
|
|
generator,
|
|
stable_id,
|
|
quarter_turns,
|
|
)
|
|
_check(
|
|
coast_variant != null,
|
|
"%s must support rotation %d." % [stable_id, quarter_turns],
|
|
)
|
|
if coast_variant == null:
|
|
continue
|
|
_check(
|
|
not generator._variant_respects_ocean_boundary(
|
|
coast_variant,
|
|
Vector2i(3, 3),
|
|
),
|
|
"%s must never place its ocean edge inside the map." % stable_id,
|
|
)
|
|
var coast_definition := CATALOG.definition_for_id(stable_id)
|
|
if coast_definition == null:
|
|
continue
|
|
var boundary_coordinate := Vector2i(3, 3)
|
|
var ocean_edges := coast_variant.rotated_edge_mask(
|
|
coast_definition.ocean_facing_edges
|
|
)
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
if (ocean_edges & (1 << edge_value)) == 0:
|
|
continue
|
|
match edge_value as TerrainChunkTopology.Edge:
|
|
TerrainChunkTopology.Edge.NORTH:
|
|
boundary_coordinate.y = 0
|
|
TerrainChunkTopology.Edge.EAST:
|
|
boundary_coordinate.x = generator.grid_size.x - 1
|
|
TerrainChunkTopology.Edge.SOUTH:
|
|
boundary_coordinate.y = generator.grid_size.y - 1
|
|
TerrainChunkTopology.Edge.WEST:
|
|
boundary_coordinate.x = 0
|
|
_check(
|
|
generator._variant_respects_ocean_boundary(
|
|
coast_variant,
|
|
boundary_coordinate,
|
|
),
|
|
"Each coast rotation must fit its matching open map boundary.",
|
|
)
|
|
|
|
|
|
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
|
|
var summary := generator._build_summary()
|
|
_check(
|
|
int(summary.get("variant_count", 0)) == 170,
|
|
"The current catalog must expose all 170 authored rotations.",
|
|
)
|
|
_check(
|
|
int(summary.get("solver_variant_count", 0))
|
|
== EXPECTED_SOLVER_VARIANT_COUNT,
|
|
"Equivalent visual rotations must share one solver constraint.",
|
|
)
|
|
_check(
|
|
int(summary.get("adjacent_repeat_edges", -1))
|
|
== generator._count_adjacent_repeat_edges(),
|
|
"The summary must report adjacent repeated chunk definitions.",
|
|
)
|
|
_check(
|
|
str(summary.get("layout_fingerprint", ""))
|
|
== generator.placement_fingerprint(),
|
|
"The summary must identify the exact resolved layout.",
|
|
)
|
|
_check(
|
|
generator.placement_fingerprint().length() == 64,
|
|
"The resolved layout fingerprint must be SHA-256.",
|
|
)
|
|
var variant_counts: Dictionary = summary.get("variant_counts", {})
|
|
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
|
|
_check(
|
|
int(variant_counts.get(stable_id, 0))
|
|
== EXPECTED_VARIANT_COUNTS[stable_id],
|
|
"The summary must report %s rotation candidates." % stable_id,
|
|
)
|
|
|
|
|
|
func _find_variant(
|
|
generator: TerrainChunkGenerator,
|
|
stable_id: StringName,
|
|
quarter_turns: int,
|
|
) -> TerrainChunkVariant:
|
|
for variant: TerrainChunkVariant in generator._variants:
|
|
if (
|
|
variant.definition.stable_id == stable_id
|
|
and variant.quarter_turns == quarter_turns
|
|
):
|
|
return variant
|
|
return null
|
|
|
|
|
|
func _make_generator() -> TerrainChunkGenerator:
|
|
var generator := TerrainChunkGenerator.new()
|
|
generator.catalog = CATALOG
|
|
generator.grid_size = Vector2i(9, 9)
|
|
generator.generation_seed = TEST_SEED
|
|
generator.generate_on_ready = false
|
|
generator.build_collision = false
|
|
generator.force_center_chunk_id = &"chunk_spawn"
|
|
generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS)
|
|
generator.grass_sand_smoothing_enabled = true
|
|
generator.maximum_backtracks = 100000
|
|
return generator
|
|
|
|
|
|
func _placements_contain(keys: PackedStringArray, stable_id: String) -> bool:
|
|
for key: String in keys:
|
|
if key.begins_with(stable_id + "@"):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _count_placements(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 _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
|
|
for index: int in generator._placements.size():
|
|
var coordinate := Vector2i(
|
|
index % generator.grid_size.x,
|
|
index / generator.grid_size.x,
|
|
)
|
|
var current: TerrainChunkVariant = generator._placements[index]
|
|
if coordinate.x > 0:
|
|
var west: TerrainChunkVariant = generator._placements[index - 1]
|
|
if not generator._edges_are_compatible(
|
|
current,
|
|
TerrainChunkTopology.Edge.WEST,
|
|
west,
|
|
TerrainChunkTopology.Edge.EAST,
|
|
):
|
|
return false
|
|
if coordinate.y > 0:
|
|
var north: TerrainChunkVariant = (
|
|
generator._placements[index - generator.grid_size.x]
|
|
)
|
|
if not generator._edges_are_compatible(
|
|
current,
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
north,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|
_check(
|
|
generator._neighbor_requirement_validation_error(
|
|
generator._placements
|
|
).is_empty(),
|
|
"Every generated chunk must satisfy its authored neighbor count.",
|
|
)
|
|
_check(
|
|
generator._walkable_connectivity_validation_error(
|
|
generator._placements
|
|
).is_empty(),
|
|
"All walkable generated terrain must remain connected to spawn.",
|
|
)
|
|
var center := Vector2i(
|
|
generator.grid_size.x / 2,
|
|
generator.grid_size.y / 2,
|
|
)
|
|
var safe_spawn_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
center
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
var neighbor := generator._placements[
|
|
neighbor_coordinate.y * generator.grid_size.x
|
|
+ neighbor_coordinate.x
|
|
]
|
|
if "spawn_safe" in neighbor.definition.tags:
|
|
safe_spawn_neighbors += 1
|
|
_check(
|
|
safe_spawn_neighbors >= 3,
|
|
"At least three cardinal spawn neighbors must be safe flat grass.",
|
|
)
|
|
for index: int in generator._placements.size():
|
|
var placement := generator._placements[index]
|
|
var coordinate := Vector2i(
|
|
index % generator.grid_size.x,
|
|
index / generator.grid_size.x,
|
|
)
|
|
if "coast" in placement.definition.tags:
|
|
_check(
|
|
generator._variant_respects_ocean_boundary(
|
|
placement,
|
|
coordinate,
|
|
),
|
|
"Every coastline chunk must direct its ocean edge outside the grid.",
|
|
)
|
|
var ocean_edges := placement.rotated_edge_mask(
|
|
placement.definition.ocean_facing_edges
|
|
)
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
var outside := not generator._coordinate_is_inside_grid(
|
|
neighbor_coordinate
|
|
)
|
|
_check(
|
|
((ocean_edges & (1 << edge_value)) != 0) == outside,
|
|
"Every map-boundary edge must use authored coastline geometry.",
|
|
)
|
|
if placement.definition.stable_id == &"chunk_0006":
|
|
var touches_beach := false
|
|
var touches_grass_edge := false
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := generator._placements[
|
|
neighbor_coordinate.y * generator.grid_size.x
|
|
+ neighbor_coordinate.x
|
|
]
|
|
touches_beach = (
|
|
touches_beach
|
|
or neighbor.definition.stable_id == &"chunk_0002"
|
|
)
|
|
touches_grass_edge = (
|
|
touches_grass_edge
|
|
or neighbor.definition.stable_id == &"chunk_0005"
|
|
)
|
|
_check(
|
|
touches_beach and touches_grass_edge,
|
|
"Every grass/beach transition must join both authored edge types.",
|
|
)
|
|
if placement.definition.stable_id == &"chunk_0007":
|
|
var straight_edge_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := generator._placements[
|
|
neighbor_coordinate.y * generator.grid_size.x
|
|
+ neighbor_coordinate.x
|
|
]
|
|
if neighbor.definition.stable_id == &"chunk_0005":
|
|
straight_edge_neighbors += 1
|
|
_check(
|
|
straight_edge_neighbors == 2,
|
|
"Every grass corner must join two straight grass ocean edges.",
|
|
)
|
|
if placement.definition.stable_id == &"chunk_0008":
|
|
var straight_beach_neighbors := 0
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := generator._placements[
|
|
neighbor_coordinate.y * generator.grid_size.x
|
|
+ neighbor_coordinate.x
|
|
]
|
|
if neighbor.definition.stable_id == &"chunk_0002":
|
|
straight_beach_neighbors += 1
|
|
_check(
|
|
straight_beach_neighbors == 2,
|
|
"Every beach corner must join two straight beach ocean edges.",
|
|
)
|
|
if placement.definition.stable_id != &"chunk_0001":
|
|
continue
|
|
var touches_sand_or_coast := false
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var neighbor_coordinate := (
|
|
coordinate
|
|
+ TerrainChunkTopology.grid_offset(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
)
|
|
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
|
|
continue
|
|
var neighbor := generator._placements[
|
|
neighbor_coordinate.y * generator.grid_size.x
|
|
+ neighbor_coordinate.x
|
|
]
|
|
if (
|
|
"sand" in neighbor.definition.tags
|
|
or "coast" in neighbor.definition.tags
|
|
):
|
|
touches_sand_or_coast = true
|
|
_check(
|
|
touches_sand_or_coast,
|
|
"Every flat-sand chunk must remain attached to sand or coast.",
|
|
)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
_failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if _failures.is_empty():
|
|
print("Terrain chunk generator validation: PASS")
|
|
quit(0)
|
|
return
|
|
for failure: String in _failures:
|
|
printerr("Terrain chunk generator validation: ", failure)
|
|
quit(1)
|