extends SceneTree const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd") const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd") const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd") const BathymetricTerrainActor = preload( "res://game/world/sites/bathymetric_terrain_actor.gd" ) const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd") const SitePlacement = preload("res://game/domain/sites/site_placement.gd") const DiveSite = preload("res://game/world/sites/dive_site.gd") const SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd") const ReferenceBathymetryAcquisition = preload( "res://game/world/sonar/reference_bathymetry_acquisition.gd" ) const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn") const TEST_COUNT := 24 var _failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await _test_actor_matches_field() await _test_dive_site_scene() var exit_code := 0 if _failures.is_empty(): print("Site terrain integration: %d passed, 0 failed." % TEST_COUNT) else: for failure: String in _failures: push_error(failure) print("Site terrain integration: %d failed." % _failures.size()) exit_code = 1 await process_frame quit(exit_code) func _asymmetric_field() -> TerrainHeightField: var recipe := SiteTerrainBaker.Recipe.new() recipe.columns = 81 recipe.rows = 81 recipe.cell_size_m = 2.0 recipe.origin_x_m = -80.0 recipe.origin_z_m = -80.0 recipe.floor_elevation_m = -90.0 recipe.primitives = [ SiteTerrainBaker.SlopedPlane.new(40.0, 0.0, 0.0, 0.0, 0.0), # Tall bump in the north-west corner only: any front/back flip shows up. SiteTerrainBaker.Blob.new(Vector2(-60.0, -60.0), 10.0, 10.0, 18.0), ] return TerrainHeightField.from_data(SiteTerrainBaker.bake(recipe)) func _test_actor_matches_field() -> void: var field := _asymmetric_field() _expect(field != null, "asymmetric test field bakes") if field == null: return var actor := BathymetricTerrainActor.new() root.add_child(actor) _expect(actor.configure(field).is_empty(), "actor accepts a valid field") await physics_frame await physics_frame _expect(actor.tile_count() == 4, "80 cells per side split into 2 x 2 tiles of 64") var first_tile := actor.get_node("Tiles").get_child(0) as MeshInstance3D var arrays: Array = first_tile.mesh.surface_get_arrays(0) var tile_vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] var tile_indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX] var first_face := Plane( tile_vertices[tile_indices[0]], tile_vertices[tile_indices[1]], tile_vertices[tile_indices[2]] ) _expect(first_face.normal.y > 0.0, "tile triangles are wound clockwise from above (front face up)") var space := actor.get_world_3d().direct_space_state var worst_error := 0.0 for cell: Vector2i in [ Vector2i(0, 0), Vector2i(80, 0), Vector2i(0, 80), Vector2i(80, 80), Vector2i(10, 10), Vector2i(40, 40), Vector2i(64, 20), ]: var expected := field.cell_world_position(cell.x, cell.y) var query := PhysicsRayQueryParameters3D.create( Vector3(expected.x, 50.0, expected.z), Vector3(expected.x, -200.0, expected.z) ) var hit := space.intersect_ray(query) if hit.is_empty(): _failures.append("collision ray at cell %s hits nothing" % cell) continue worst_error = maxf(worst_error, absf(float(hit["position"].y) - expected.y)) _expect(worst_error < 0.05, "collision height equals the field height at vertices (worst %.3f m)" % worst_error) var north_west := _collision_height(space, -60.0, -60.0) var south_east := _collision_height(space, 60.0, 60.0) _expect(north_west > -30.0, "north-west bump is where the field puts it (collision)") _expect(south_east < -39.0, "south-east stays flat (collision)") _expect(absf(north_west - field.elevation_at(-60.0, -60.0)) < 0.05, "bump height matches the field") _expect(actor.sonar_material_at(Vector3(-52.0, -30.0, -60.0)) == SonarMaterial.Value.ROCK, "steep flank reports rock") _expect(actor.sonar_material_at(Vector3(40.0, -40.0, 40.0)) == SonarMaterial.Value.SAND, "flat plain reports sand") _expect(is_equal_approx(actor.sonar_reflectivity_at(Vector3(40.0, -40.0, 40.0)), 0.45), "sand reflectivity from the data") _expect(actor.configure(field).is_empty() and actor.tile_count() == 4, "reconfiguring rebuilds without duplicating tiles") _expect(not actor.configure(null).is_empty(), "null field is refused") actor.queue_free() func _collision_height(space: PhysicsDirectSpaceState3D, x: float, z: float) -> float: var hit := space.intersect_ray( PhysicsRayQueryParameters3D.create(Vector3(x, 50.0, z), Vector3(x, -200.0, z)) ) if hit.is_empty(): return INF return float(hit["position"].y) func _expect(condition: bool, label: String) -> void: if not condition: _failures.append(label) func _test_dive_site_scene() -> void: var definition := load("res://content/sites/vehicle_test.tres") as DiveSiteDefinition _expect(definition != null, "calibration site definition loads") if definition == null: return _expect(definition.validation_error().is_empty(), "calibration site definition is valid") var started_usec := Time.get_ticks_usec() var site := DIVE_SITE_SCENE.instantiate() as DiveSite root.add_child(site) _expect(site.configure(definition).is_empty(), "dive site scene accepts the definition") var configure_ms := float(Time.get_ticks_usec() - started_usec) / 1000.0 _expect(configure_ms < 3000.0, "site configures under the 3 s load budget (%.0f ms)" % configure_ms) await physics_frame await physics_frame _expect(site.terrain().tile_count() > 0, "site builds its terrain tiles") _expect(site.placeholder_count() == 2, "calibration target and beacon are placed") var target := site.get_node_or_null("%Placements/calibration_target") as StaticBody3D _expect(target != null and int(target.get_meta("sonar_material")) == SonarMaterial.Value.METAL, "wreck placeholder is a metal sonar body") _expect(site.spawn_transform().origin.is_equal_approx(Vector3(0.0, -12.0, 12.0)), "spawn comes from the definition") var sensor := Transform3D(Basis(Vector3.UP, 0.0), Vector3(0.0, -12.0, 10.0)) sensor.basis = Basis(sensor.basis.x, sensor.basis.y, -sensor.basis.z) var acquisition := ReferenceBathymetryAcquisition.new(site.get_world_3d()) var request := SonarCaptureRequest.new( &"scan", &"session", sensor, 5.0, 95.0, deg_to_rad(76.0), deg_to_rad(64.0), 49, 31 ) var result = acquisition.capture(request) _expect(result.accepted, "reference acquisition scans the baked terrain") var materials: Dictionary = {} for sample in result.samples: materials[sample.material] = true _expect(materials.has(SonarMaterial.Value.SAND) and materials.has(SonarMaterial.Value.ROCK), "acquisition reports per-cell sand and rock") _expect(materials.has(SonarMaterial.Value.METAL), "acquisition still reports the metal placeholder") site.queue_free()