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
+4
-1
@@ -15,6 +15,9 @@ Suites de production actuelles :
|
||||
pression, collisions, alertes et états terminaux ;
|
||||
- `unit/run_vehicle_application_tests.gd` : validation et séquençage des
|
||||
commandes de pilotage et de caméra ;
|
||||
- `unit/run_sonar_application_tests.gd` : buffer compact scellé, validation des
|
||||
captures, contacts séparés et temporalité de révélation/récupération ;
|
||||
- `integration/run_vertical_slice_foundation.gd` : composition, mouvement
|
||||
inertiel, caméra indépendante, cockpit 3D fixe, instruments physiques, couche
|
||||
d'alerte, pause des ticks et remappage avec conflit.
|
||||
d'alerte, sonar bathymétrique partagé, pause des ticks et remappage avec
|
||||
conflit.
|
||||
@@ -13,7 +13,7 @@ const ProductionInputAdapter = preload(
|
||||
"res://game/infrastructure/input/production_input_adapter.gd"
|
||||
)
|
||||
|
||||
const TEST_COUNT := 40
|
||||
const TEST_COUNT := 51
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -78,11 +78,15 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
var instruments = composition.cockpit_instrument_panel()
|
||||
var adapter := composition.input_adapter() as ProductionInputAdapter
|
||||
var hud := composition.active_screen() as CockpitHud
|
||||
var sonar_service = composition.sonar_service()
|
||||
var sonar_display = composition.sonar_display()
|
||||
_expect(composition.active_world() != null, "vehicle test world is assembled")
|
||||
_expect(actor != null, "production submarine actor is assembled")
|
||||
_expect(camera != null, "cockpit camera is attached to the submarine")
|
||||
_expect(instruments != null, "physical cockpit instruments are assembled")
|
||||
_expect(hud != null, "critical HUD overlay is assembled")
|
||||
_expect(sonar_service != null, "production sonar service is assembled")
|
||||
_expect(sonar_display != null, "shared sonar presentation is assembled")
|
||||
_expect(composition.latest_update() != null, "dive session starts with a typed update")
|
||||
if actor == null or camera == null or instruments == null or adapter == null or hud == null:
|
||||
return
|
||||
@@ -94,6 +98,24 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
_expect("L/min" in instruments.resources_text(), "oxygen instrument has volume and consumption")
|
||||
_expect("bar" in instruments.integrity_text(), "hull instrument has current and certified pressure")
|
||||
_expect(not hud.alert_overlay_visible(), "normal telemetry stays on physical instruments only")
|
||||
_expect(
|
||||
instruments.sonar_texture() == sonar_display.sonar_texture(),
|
||||
"physical and expanded sonar views share one texture"
|
||||
)
|
||||
|
||||
adapter.sonar_scan_requested.emit()
|
||||
var first_scan = sonar_service.current_scan()
|
||||
_expect(first_scan != null, "manual sonar command publishes a typed scan")
|
||||
_expect(first_scan.samples.observed_count() > 1000, "reference acquisition captures dense bathymetry")
|
||||
_expect(first_scan.contacts.size() >= 1, "metal calibration target becomes a distinct contact")
|
||||
_expect("RELEVÉ EN COURS" in sonar_display.status_text(), "sonar display reports progressive reveal")
|
||||
for frame in 8:
|
||||
await physics_frame
|
||||
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
|
||||
adapter.sonar_view_toggle_requested.emit()
|
||||
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
|
||||
adapter.sonar_view_toggle_requested.emit()
|
||||
_expect(not sonar_display.is_expanded(), "expanded sonar returns to cockpit view")
|
||||
|
||||
var start_position: Vector3 = actor.global_position
|
||||
var starting_battery: float = composition.latest_update().snapshot.battery.remaining_kwh
|
||||
@@ -130,6 +152,7 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
_expect(instruments.global_transform.is_equal_approx(cockpit_transform), "cockpit remains fixed while the player looks around")
|
||||
|
||||
var paused_sequence: int = composition.latest_update().snapshot.tick_sequence
|
||||
var paused_sonar_progress: float = sonar_service.current_scan().reveal_progress
|
||||
adapter.pause_requested.emit()
|
||||
await process_frame
|
||||
_expect(paused, "pause menu suspends the scene tree")
|
||||
@@ -141,13 +164,17 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
composition.latest_update().snapshot.tick_sequence == paused_sequence,
|
||||
"dive ticks are frozen while paused"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(sonar_service.current_scan().reveal_progress, paused_sonar_progress),
|
||||
"sonar reveal is frozen while paused"
|
||||
)
|
||||
|
||||
var controls_button := pause_menu.get_node("%ControlsButton") as Button
|
||||
controls_button.pressed.emit()
|
||||
await process_frame
|
||||
var controls := composition.active_overlay() as ControlsSettingsScreen
|
||||
_expect(controls != null, "controls settings open over the paused dive")
|
||||
_expect(adapter.binding_snapshots().size() == 9, "all gameplay keyboard bindings are exposed")
|
||||
_expect(adapter.binding_snapshots().size() == 11, "all gameplay keyboard bindings are exposed")
|
||||
|
||||
var rebind_results: Array = []
|
||||
adapter.rebind_result_available.connect(
|
||||
@@ -186,7 +213,10 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
adapter.pause_requested.emit()
|
||||
await process_frame
|
||||
_expect(not paused, "second escape resumes the dive")
|
||||
await physics_frame
|
||||
for frame in 3:
|
||||
await physics_frame
|
||||
if composition.latest_update().snapshot.tick_sequence > paused_sequence:
|
||||
break
|
||||
_expect(
|
||||
composition.latest_update().snapshot.tick_sequence > paused_sequence,
|
||||
"fixed dive ticks resume after pause"
|
||||
|
||||
@@ -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