Add production bathymetric sonar slice
This commit is contained in:
1 parent
a18e82eb44
commit
7613daee84
28 files changed
+1825
-87
No files matched your search
@@ -0,0 +1,181 @@
|
||||
extends SceneTree
|
||||
|
||||
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||
const SonarScanState = preload("res://game/domain/sonar/sonar_scan_state.gd")
|
||||
const SonarSample = preload("res://game/domain/sonar/sonar_sample.gd")
|
||||
const SonarSampleBuffer = preload(
|
||||
"res://game/domain/sonar/sonar_sample_buffer.gd"
|
||||
)
|
||||
const SonarCaptureRequest = preload(
|
||||
"res://game/application/sonar/sonar_capture_request.gd"
|
||||
)
|
||||
const SonarCaptureSample = preload(
|
||||
"res://game/application/sonar/sonar_capture_sample.gd"
|
||||
)
|
||||
const SonarCaptureResult = preload(
|
||||
"res://game/application/sonar/sonar_capture_result.gd"
|
||||
)
|
||||
const SonarAcquisitionPort = preload(
|
||||
"res://game/application/interfaces/sonar_acquisition_port.gd"
|
||||
)
|
||||
const SonarService = preload("res://game/application/sonar/sonar_service.gd")
|
||||
|
||||
const TEST_COUNT := 32
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
class RecordingAcquisition extends SonarAcquisitionPort:
|
||||
var requests: Array[SonarCaptureRequest] = []
|
||||
|
||||
|
||||
func capture(request: SonarCaptureRequest) -> SonarCaptureResult:
|
||||
requests.append(request)
|
||||
var samples: Array[SonarCaptureSample] = [
|
||||
SonarCaptureSample.new(
|
||||
0, 0, Vector3(-2.0, -7.0, 8.0), Vector3.UP, 0.9, -7.0,
|
||||
SonarMaterial.Value.METAL
|
||||
),
|
||||
SonarCaptureSample.new(
|
||||
0, 1, Vector3(-1.0, -7.0, 8.0), Vector3.UP, 0.8, -7.0,
|
||||
SonarMaterial.Value.METAL
|
||||
),
|
||||
SonarCaptureSample.new(
|
||||
5, 5, Vector3(12.0, -11.0, 24.0), Vector3.UP, 1.0, -11.0,
|
||||
SonarMaterial.Value.METAL
|
||||
),
|
||||
SonarCaptureSample.new(
|
||||
2, 2, Vector3(0.0, -14.0, 16.0), Vector3.UP, 0.6, -14.0,
|
||||
SonarMaterial.Value.ROCK
|
||||
),
|
||||
]
|
||||
return SonarCaptureResult.new(true, samples)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_sample_buffer_contract()
|
||||
_test_service_lifecycle_and_contacts()
|
||||
|
||||
if _failures.is_empty():
|
||||
print("Sonar application unit tests: %d passed, 0 failed." % TEST_COUNT)
|
||||
quit(0)
|
||||
else:
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
print("Sonar application unit tests: %d failed." % _failures.size())
|
||||
quit(1)
|
||||
|
||||
|
||||
func _test_sample_buffer_contract() -> void:
|
||||
var sample := SonarSample.new(
|
||||
Vector3(1.0, -3.0, 8.0),
|
||||
Vector3.UP,
|
||||
0.75,
|
||||
-3.0,
|
||||
SonarMaterial.Value.ROCK,
|
||||
true
|
||||
)
|
||||
_expect(sample.validation_error().is_empty(), "observed typed sample is valid")
|
||||
var buffer := SonarSampleBuffer.new(3, 2)
|
||||
_expect(buffer.set_sample(1, 2, sample).is_empty(), "sample writes inside buffer bounds")
|
||||
_expect(buffer.observed_count() == 1, "buffer counts observed cells")
|
||||
var restored: SonarSample = buffer.sample_at(1, 2)
|
||||
_expect(
|
||||
restored.observed and is_equal_approx(restored.intensity, 0.75),
|
||||
"typed accessor restores aligned sample fields"
|
||||
)
|
||||
_expect(not buffer.sample_at(4, 4).observed, "out-of-range accessor returns unknown sample")
|
||||
_expect(buffer.seal().is_empty(), "valid sample buffer seals")
|
||||
_expect(
|
||||
buffer.set_sample(0, 0, sample) == "sonar sample buffer is sealed",
|
||||
"sealed sample buffer rejects mutation"
|
||||
)
|
||||
_expect(buffer.validation_error().is_empty(), "sealed aligned buffer is publishable")
|
||||
|
||||
|
||||
func _test_service_lifecycle_and_contacts() -> void:
|
||||
var acquisition := RecordingAcquisition.new()
|
||||
var service := SonarService.new(acquisition, &"test-dive")
|
||||
var first = service.request_scan(Transform3D.IDENTITY)
|
||||
_expect(first.accepted, "manual sonar request is accepted while ready")
|
||||
_expect(
|
||||
acquisition.requests.size() == 1
|
||||
and acquisition.requests[0].columns == 49
|
||||
and acquisition.requests[0].rows == 31,
|
||||
"acquisition receives baseline grid dimensions"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(acquisition.requests[0].maximum_range_m, 95.0)
|
||||
and is_equal_approx(rad_to_deg(acquisition.requests[0].horizontal_opening_radians), 76.0),
|
||||
"acquisition receives baseline range and opening"
|
||||
)
|
||||
_expect(first.scan.validation_error().is_empty(), "published sonar scan is valid")
|
||||
_expect(first.scan.samples.observed_count() == 4, "capture becomes a compact sealed buffer")
|
||||
_expect(first.scan.contacts.size() == 2, "separated metal groups become distinct contacts")
|
||||
_expect(
|
||||
first.scan.contacts[0].sample_count == 2
|
||||
and first.scan.contacts[1].sample_count == 1,
|
||||
"adjacent metal samples cluster deterministically"
|
||||
)
|
||||
_expect(service.visible_row_count() == 0, "scan has no immediate visible return")
|
||||
_expect(not service.request_scan(Transform3D.IDENTITY).accepted, "request during reveal is rejected")
|
||||
|
||||
service.advance(0.1)
|
||||
_expect(service.visible_row_count() == 1, "first return appears at four percent")
|
||||
_expect(
|
||||
service.current_scan().state == SonarScanState.Value.REVEALING,
|
||||
"scan remains in reveal after its first return"
|
||||
)
|
||||
service.advance(2.4)
|
||||
_expect(
|
||||
service.current_scan().state == SonarScanState.Value.RECOVERY,
|
||||
"completed reveal enters recovery"
|
||||
)
|
||||
_expect(service.previous_scan() == null, "previous scan is released after replacement")
|
||||
_expect(not service.request_scan(Transform3D.IDENTITY).accepted, "request during recovery is rejected")
|
||||
service.advance(0.35)
|
||||
_expect(service.current_scan().state == SonarScanState.Value.READY, "recovery returns sonar to ready")
|
||||
|
||||
var second = service.request_scan(Transform3D.IDENTITY)
|
||||
_expect(second.accepted, "next manual sonar request is accepted")
|
||||
_expect(
|
||||
service.previous_scan().scan_id == first.scan.scan_id,
|
||||
"one previous scan is retained"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(service.previous_scan_opacity(), 0.52),
|
||||
"previous scan starts at baseline opacity"
|
||||
)
|
||||
service.advance(1.25)
|
||||
_expect(
|
||||
is_equal_approx(service.current_scan().reveal_progress, 0.5),
|
||||
"reveal advances from explicit elapsed time"
|
||||
)
|
||||
_expect(
|
||||
service.previous_scan_opacity() > 0.0
|
||||
and service.previous_scan_opacity() < 0.52,
|
||||
"previous scan fades during replacement"
|
||||
)
|
||||
_expect(
|
||||
service.visible_row_count() > 1 and service.visible_row_count() < 31,
|
||||
"partial reveal exposes only near rows"
|
||||
)
|
||||
service.advance(1.25)
|
||||
_expect(
|
||||
service.current_scan().state == SonarScanState.Value.RECOVERY,
|
||||
"second reveal completes after 2.5 seconds"
|
||||
)
|
||||
_expect(service.previous_scan() == null, "faded previous scan is discarded")
|
||||
service.advance(0.35)
|
||||
_expect(service.current_scan().state == SonarScanState.Value.READY, "second recovery completes")
|
||||
_expect(first.scan.scan_id != second.scan.scan_id, "scan identifiers are unique and ordered")
|
||||
_expect(acquisition.requests.size() == 2, "rejected commands do not invoke acquisition")
|
||||
_expect(
|
||||
is_equal_approx(first.scan.maximum_elevation_m - first.scan.minimum_elevation_m, 7.0),
|
||||
"scan retains a real elevation scale wider than the minimum span"
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(label)
|
||||
Reference in new issue
Block a user