Files
Ransom-s-wake/tests/integration/run_site_terrain_integration.gd
T

107 lines
3.8 KiB
GDScript

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 TEST_COUNT := 12
var _failures: Array[String] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
await _test_actor_matches_field()
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 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)