extends SceneTree const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd") const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd") const VehicleTestRecipe = preload("res://tools/sites/vehicle_test_terrain_recipe.gd") const Zone1Recipe = preload("res://tools/sites/zone1_vigilant_recipe.gd") const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd") const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res" const TEST_COUNT := 36 var _failures: Array[String] = [] func _init() -> void: _test_plane_and_blob() _test_materials_and_determinism() _test_modifiers_and_primitive_materials() _test_zone1_artefact_is_stable() _test_canyon() _test_escarpment() _test_outcrop() _test_boulder_field() if _failures.is_empty(): print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT) quit(0) else: for failure: String in _failures: push_error(failure) print("Site baker unit tests: %d failed." % _failures.size()) quit(1) func _small_recipe() -> SiteTerrainBaker.Recipe: var recipe := SiteTerrainBaker.Recipe.new() recipe.columns = 21 recipe.rows = 21 recipe.cell_size_m = 2.0 recipe.origin_x_m = -20.0 recipe.origin_z_m = -20.0 recipe.floor_elevation_m = -80.0 recipe.rock_slope_degrees = 20.0 return recipe func _test_plane_and_blob() -> void: var recipe := _small_recipe() recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.1, 0.0, 0.0)) var data := SiteTerrainBaker.bake(recipe) _expect(data != null and data.validation_error().is_empty(), "plane recipe bakes valid data") _expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -30.0), "plane depth at the reference point") _expect(is_equal_approx(data.elevations_m[data.index_of(10, 20)], -28.0), "plane slopes along +Z") _expect(data.materials[data.index_of(10, 10)] == SonarMaterial.Value.SAND, "gentle plane is sand") recipe.primitives.append(SiteTerrainBaker.Blob.new(Vector2(0.0, 0.0), 4.0, 4.0, 10.0)) data = SiteTerrainBaker.bake(recipe) _expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -20.0), "blob adds its amplitude at the centre") _expect(data.materials[data.index_of(12, 10)] == SonarMaterial.Value.ROCK, "steep blob flank is rock") func _test_materials_and_determinism() -> void: var recipe := _small_recipe() recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0)) recipe.primitives.append(SiteTerrainBaker.Roughness.new(1933, 0.05, 0.5)) recipe.material_patches.append( SiteTerrainBaker.MaterialPatch.new(Vector2(10.0, 10.0), 3.0, SonarMaterial.Value.ROCK) ) var first := SiteTerrainBaker.bake(recipe) var second := SiteTerrainBaker.bake(recipe) _expect(first.elevations_m == second.elevations_m, "same recipe bakes identical elevations") _expect(first.materials == second.materials, "same recipe bakes identical materials") _expect(first.materials[first.index_of(15, 15)] == SonarMaterial.Value.ROCK, "material patch overrides the slope rule") var calibration := SiteTerrainBaker.bake(VehicleTestRecipe.recipe()) _expect(calibration != null and calibration.validation_error().is_empty(), "calibration recipe bakes valid data") ## Test doubles for the extension points: a modifier that lifts the field by one metre ## and a primitive that forces rock on a flat plane. class Lift extends SiteTerrainBaker.Modifier: func modify(_x_m: float, _z_m: float, elevation_m: float, _gradient: Vector2) -> float: return elevation_m + 1.0 class GradientProbe extends SiteTerrainBaker.Modifier: var gradient_at_centre := Vector2.ZERO func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float: if is_zero_approx(x_m) and is_zero_approx(z_m): gradient_at_centre = gradient return elevation_m class RockDisc extends SiteTerrainBaker.Primitive: func material_at(x_m: float, z_m: float) -> int: if Vector2(x_m, z_m).length() <= 5.0: return SonarMaterial.Value.ROCK return SiteTerrainBaker.NO_MATERIAL func _test_modifiers_and_primitive_materials() -> void: var recipe := _small_recipe() recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.2, 0.0, 0.0, 0.0)) var probe := GradientProbe.new() recipe.modifiers.append(probe) recipe.modifiers.append(Lift.new()) var data := SiteTerrainBaker.bake(recipe) _expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -29.0), "modifier runs after the primitive sum") _expect(probe.gradient_at_centre.is_equal_approx(Vector2(0.2, 0.0)), "modifier receives the local gradient (%s)" % probe.gradient_at_centre) var flat := _small_recipe() flat.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0)) flat.primitives.append(RockDisc.new()) var flat_data := SiteTerrainBaker.bake(flat) _expect(flat_data.materials[flat_data.index_of(10, 10)] == SonarMaterial.Value.ROCK, "primitive material wins over the slope rule") _expect(flat_data.materials[flat_data.index_of(0, 0)] == SonarMaterial.Value.SAND, "primitive material is local") func _test_canyon() -> void: var canyon := SiteTerrainBaker.Canyon.new( PackedVector2Array([Vector2(-20.0, 0.0), Vector2(0.0, 0.0), Vector2(0.0, 20.0)]), 4.0, 10.0, 1.0, 0, 0.0 ) _expect(is_equal_approx(canyon.elevation_delta(-10.0, 0.0), -10.0), "canyon floor sits at depth on the first leg") _expect(is_equal_approx(canyon.elevation_delta(1.5, 10.0), -10.0), "canyon floor is flat across its width on the second leg") _expect(is_equal_approx(canyon.elevation_delta(-10.0, 6.0), 0.0), "canyon stops past half width plus softness") _expect(is_equal_approx(canyon.elevation_delta(15.0, -15.0), 0.0), "canyon is zero far from the path") _expect(is_equal_approx(canyon.elevation_delta(0.0, 20.0), 0.0), "canyon closes at its last vertex") _expect(canyon.elevation_delta(-10.0, 4.0) < -4.0 and canyon.elevation_delta(-10.0, 4.0) > -6.0, "canyon wall is half way down at half width") var rough := SiteTerrainBaker.Canyon.new( PackedVector2Array([Vector2(-200.0, 0.0), Vector2(200.0, 0.0)]), 8.0, 10.0, 2.0, 7, 3.0 ) var wall_values: Dictionary = {} for x in range(-100, 101, 10): wall_values[snappedf(rough.elevation_delta(float(x), 8.0), 0.01)] = true _expect(wall_values.size() > 3, "outline noise makes the canyon wall irregular along the path") func _test_escarpment() -> void: var cliff := SiteTerrainBaker.Escarpment.new( PackedVector2Array([Vector2(-50.0, 0.0), Vector2(50.0, 0.0)]), 10.0, 2.0, 0.5, 0, 0.0 ) _expect(is_equal_approx(cliff.elevation_delta(0.0, -10.0), 0.0), "escarpment leaves the left (north) side alone") _expect(is_equal_approx(cliff.elevation_delta(0.0, 2.0), -10.0), "escarpment drops by drop_m on the right (south) side") _expect(is_equal_approx(cliff.elevation_delta(0.0, 12.0), -15.0), "escarpment keeps falling at slope_beyond") func _test_outcrop() -> void: var mesa := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, true) _expect(is_equal_approx(mesa.elevation_delta(0.0, 0.0), 6.0), "outcrop top is flat at height_m") _expect(is_equal_approx(mesa.elevation_delta(0.0, 8.0), 3.0), "outcrop flank is half way down in the middle of the wall") _expect(is_equal_approx(mesa.elevation_delta(0.0, 25.0), 0.0), "outcrop is zero past its outline") _expect(mesa.material_at(0.0, 0.0) == SonarMaterial.Value.ROCK, "rock_top makes the flat summit rock") _expect(mesa.material_at(0.0, 25.0) == SiteTerrainBaker.NO_MATERIAL, "outcrop leaves the surroundings to the slope rule") var sand_top := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, false) _expect(sand_top.material_at(0.0, 0.0) == SiteTerrainBaker.NO_MATERIAL, "without rock_top the summit follows the slope rule") var pit := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 20.0, -5.0, 3.0, 0, 0.0, false) _expect(is_equal_approx(pit.elevation_delta(0.0, 0.0), -5.0), "negative height digs a flat-bottomed pit") func _test_boulder_field() -> void: var first := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8) var second := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8) _expect(first.boulders() == second.boulders(), "same seed scatters the same boulders") var all_inside := true for boulder in first.boulders(): all_inside = all_inside and Vector2(boulder.x, boulder.y).distance_to(Vector2(50.0, -50.0)) <= 30.0 _expect(all_inside and first.boulders().size() == 20, "boulders stay inside the field disc") var top := first.boulders()[0] _expect(first.elevation_delta(top.x, top.y) >= top.z * 0.8 - 0.001, "a boulder centre is at least its own height") _expect(first.material_at(top.x, top.y) == SonarMaterial.Value.ROCK and first.material_at(200.0, 200.0) == SiteTerrainBaker.NO_MATERIAL, "boulders are rock, the sand around is not forced") var stacked := SiteTerrainBaker.BoulderField.new(Vector2.ZERO, 0.0, 3, 5, 2.0, 4.0, 1.0) _expect(stacked.elevation_delta(0.0, 0.0) <= 4.0 + 0.001, "overlapping boulders combine by maximum, not by sum") func _test_zone1_artefact_is_stable() -> void: var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData _expect(versioned != null, "versioned zone 1 terrain loads") if versioned == null: return var baked := SiteTerrainBaker.bake(Zone1Recipe.recipe()) _expect( baked.elevations_m == versioned.elevations_m and baked.materials == versioned.materials, "zone 1 recipe still bakes the versioned artefact byte for byte" ) func _expect(condition: bool, label: String) -> void: if not condition: _failures.append(label)