feat(cockpit): replace procedural cockpit with twelve scripted models
Quality Gate / Quality Gate (pull_request) Successful in 9m4s

Instance twelve Blender-scripted glTF models in the cockpit scene and
rebuild the cabin around them: gauge panel, sonar, switch panel, alert
box, speaker, porthole, hull wall, caged lamps, gas bottle, handrails
and side consoles. Presentation only; no domain, application or world
rule changes beyond enabling glow in the test environment.

- Needles, alert lamps and sonar screen are driven by the existing
  snapshots; the alert box now carries SYS/ATTN/ALRM.
- Depth dial reads percent of certified depth.
- Sonar display restyled as a turquoise holographic relief with a
  shader grid and height scale; SubViewport matches the glass (960x604).
- Cabin lighting retuned: warm pooled lamp light, cool porthole rim,
  lamp shadows, dial backlight.
- Procedural stand-ins removed (bulkhead, tanks, consoles, levers,
  indicator columns, placeholder pipe run, dial face PNGs).
- Tests adapted where nodes disappeared; one assertion removed
  (pipe_segment_count) until the pipe kit is placed.
- Model .glb files are committed without LFS; textures Godot extracts
  next to them at import are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Opus 5.5 committed 2026-10-06 21:36:57 +02:00
1 parent 912123087f
commit 2f9b7c0894
45 files changed
+1353 -1603

No files matched your search

@@ -11,6 +11,17 @@ const CockpitDetailing = preload(
const CockpitPressureBulkhead = preload(
"res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd"
)
const CockpitAlertBox = preload("res://game/presentation/cockpit/cockpit_alert_box.gd")
const CockpitAnalogGauges = preload(
"res://game/presentation/cockpit/cockpit_analog_gauges.gd"
)
const CockpitSonarConsole = preload(
"res://game/presentation/cockpit/cockpit_sonar_console.gd"
)
const CockpitSwitchPanel = preload(
"res://game/presentation/cockpit/cockpit_switch_panel.gd"
)
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const PauseMenu = preload("res://game/presentation/pause/pause_menu.gd")
const ControlsSettingsScreen = preload(
"res://game/presentation/settings/controls_settings_screen.gd"
@@ -19,7 +30,7 @@ const ProductionInputAdapter = preload(
"res://game/infrastructure/input/production_input_adapter.gd"
)
const TEST_COUNT := 78
const TEST_COUNT := 91
var _failures: Array[String] = []
@@ -91,7 +102,17 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
else null
)
var analog_gauges := (
instruments.get_node_or_null("Dashboard/AnalogGauges")
instruments.get_node_or_null("Dashboard/AnalogGauges") as CockpitAnalogGauges
if instruments != null
else null
)
var alert_box := (
instruments.get_node_or_null("AlertBox") as CockpitAlertBox
if instruments != null
else null
)
var switch_panel := (
instruments.get_node_or_null("SwitchPanel") as CockpitSwitchPanel
if instruments != null
else null
)
@@ -106,6 +127,8 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
_expect(detailing != null, "cockpit structural detailing is assembled")
_expect(pressure_bulkhead != null, "curved pressure bulkhead is assembled")
_expect(analog_gauges != null, "dashboard analog instruments are assembled")
_expect(alert_box != null, "alert box is assembled")
_expect(switch_panel != null, "switch panel is 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")
@@ -117,17 +140,19 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
or detailing == null
or pressure_bulkhead == null
or analog_gauges == null
or alert_box == null
or switch_panel == null
or adapter == null
or hud == null
):
return
_expect(instruments.get_node("%WindowFrame") is MeshInstance3D, "cockpit has a physical porthole frame")
_expect(pressure_bulkhead.porthole_frame() is MeshInstance3D, "cockpit has a physical porthole frame")
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
var sonar_monitor := instruments.get_node("SonarMonitor") as Node3D
var sonar_monitor := instruments.get_node("SonarMonitor") as CockpitSonarConsole
var sonar_screen := instruments.get_node("%SonarScreen") as MeshInstance3D
var sonar_housing := instruments.get_node("SonarMonitor/Housing") as MeshInstance3D
var sonar_housing := sonar_monitor.housing()
var cockpit_camera := camera.get_node("Camera") as Camera3D
var electrical_bank := detailing.get_node("SideEquipment/ElectricalBank") as Node3D
_expect(sonar_monitor.position.x > 0.5, "cockpit sonar is mounted on the right side")
@@ -147,21 +172,39 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
instruments.find_children("SonarReferenceTrace", "MeshInstance3D", true).is_empty(),
"cockpit sonar does not invent a decorative profile"
)
_expect(detailing.pressure_hull_rib_count() >= 6, "cockpit exposes reinforced pressure-hull structure")
_expect(detailing.pipe_segment_count() >= 13, "cockpit exposes a complete life-support pipe run")
_expect(
_shell_surrounds(pressure_bulkhead.hull_shell(), cockpit_camera.global_position),
"pressure hull shell surrounds the pilot"
)
_expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot")
_expect(detailing.textures_are_loaded(), "cockpit loads its dedicated analog gauge textures")
_expect(detailing.gauges_are_dashboard_mounted(), "analog gauges are mounted on the dashboard")
_expect(detailing.dashboard_control_count() >= 8, "dashboard exposes physical auxiliary controls")
_expect(detailing.side_equipment_count() >= 5, "cockpit sidewalls expose tanks and junction boxes")
_expect(detailing.side_control_count() >= 9, "cockpit sidewalls expose valves and pilot controls")
_expect(detailing.side_equipment_count() >= 3, "cockpit sidewalls expose a gas bottle and junction boxes")
_expect(detailing.side_control_count() >= 3, "cockpit sidewalls expose a valve and pilot controls")
_expect(detailing.sonar_control_count() >= 4, "sonar console exposes physical lamps and gain control")
_expect(detailing.practical_light_count() == 2, "cockpit uses two caged practical lights")
_expect(pressure_bulkhead.panel_count() == 1, "porthole bulkhead uses one continuous curved shell")
_expect(pressure_bulkhead.rectangular_whitebox_is_hidden(), "porthole rectangular whitebox is hidden")
_expect(analog_gauges.gauge_count() == 4, "dashboard exposes four primary analog instruments")
_expect(analog_gauges.indicator_count() == 3, "instrument binnacle exposes three inset pilot lights")
_expect(analog_gauges.active_indicator_count() == 1, "exactly one pilot light reflects the alert state")
_expect(alert_box.indicator_count() == 3, "alert box exposes three pilot lights")
_expect(alert_box.active_indicator_count() == 1, "exactly one pilot light reflects the alert state")
_expect(
alert_box.active_indicator_name() == "Lamp_Alerte",
"normal telemetry lights the green system lamp"
)
_expect(
analog_gauges.pilot_light_count() == 6 and analog_gauges.lit_pilot_light_count() == 0,
"gauge panel pilot lights rest dark without per-instrument data"
)
_expect(
switch_panel.control_count() == 10 and switch_panel.controls_are_at_rest(),
"decorative switches stay in their rest pose"
)
_expect(
_sonar_texture_matches_glass(instruments.sonar_texture(), sonar_screen),
"sonar render target has the aspect ratio of the monitor glass"
)
_expect("kWh" in instruments.resources_text() and "kW" in instruments.resources_text(), "battery instrument has exact units")
_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")
@@ -184,6 +227,13 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
_expect(sonar_display.built_triangle_count() >= 90, "sonar renders a dense relief surface, not a line")
_expect(sonar_display.built_grid_segment_count() > 0, "sonar overlays a perspective grid on the relief")
var scale_texts: PackedStringArray = sonar_display.depth_scale_texts()
_expect(
scale_texts.size() >= 2
and scale_texts[0] == "%.0f m" % first_scan.maximum_elevation_m
and scale_texts[scale_texts.size() - 1] == "%.0f m" % first_scan.minimum_elevation_m,
"sonar height scale shows the real elevation span of the scan"
)
adapter.sonar_view_toggle_requested.emit()
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
adapter.sonar_view_toggle_requested.emit()
@@ -294,6 +344,38 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
"fixed dive ticks resume after pause"
)
analog_gauges.present(0.0, 0.5, 1.0, 0.25)
_expect(
is_equal_approx(analog_gauges.needle_degrees("Needle_Oxygen"), 120.0),
"empty gauge needle rests at the lower-left end of the printed scale"
)
_expect(
is_zero_approx(analog_gauges.needle_degrees("Needle_Battery")),
"half-scale gauge needle points straight up"
)
_expect(
is_equal_approx(analog_gauges.needle_degrees("Needle_Hull"), -120.0),
"full gauge needle reaches the lower-right end clockwise"
)
_expect(
is_equal_approx(analog_gauges.needle_degrees("Needle_Depth"), 60.0),
"depth needle uses the same printed sweep"
)
_expect(
_alert_lamp_for(alert_box, DiveAlertLevel.Value.NORMAL) == "Lamp_Alerte",
"normal level lights only the system lamp"
)
_expect(
_alert_lamp_for(alert_box, DiveAlertLevel.Value.ATTENTION) == "Lamp_Attention"
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.RETURN) == "Lamp_Attention",
"attention and return levels light only the attention lamp"
)
_expect(
_alert_lamp_for(alert_box, DiveAlertLevel.Value.CRITICAL) == "Lamp_Danger"
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.FAILURE) == "Lamp_Danger",
"critical and failure levels light only the danger lamp"
)
func _pressed_key(keycode: Key) -> InputEventKey:
var event := InputEventKey.new()
@@ -327,20 +409,61 @@ func _sonar_screen_is_ahead_of_bank(
return true
## The imported housing is an arbitrary mesh, so the clearance is measured on
## its triangles: looking straight at the glass, nothing of the housing may sit
## level with or in front of the picture.
func _sonar_screen_clears_housing(
sonar_screen: MeshInstance3D,
sonar_housing: MeshInstance3D
) -> bool:
if sonar_screen == null or sonar_housing == null:
if sonar_screen == null or sonar_housing == null or sonar_housing.mesh == null:
return false
var housing_box := sonar_housing.mesh as BoxMesh
if housing_box == null:
return false
var housing_front_z := (
sonar_housing.position.z
+ housing_box.size.z * sonar_housing.scale.z * 0.5
var glass := sonar_screen.mesh.get_aabb()
var screen_to_housing := (
sonar_housing.global_transform.affine_inverse() * sonar_screen.global_transform
)
return sonar_screen.position.z >= housing_front_z + 0.005
var outward := (screen_to_housing.basis * Vector3.BACK).normalized()
var triangles := sonar_housing.mesh.get_faces()
if triangles.is_empty():
return false
for column in 5:
for row in 3:
var on_glass := Vector3(
lerpf(glass.position.x, glass.end.x, 0.1 + 0.2 * column),
lerpf(glass.position.y, glass.end.y, 0.1 + 0.4 * row),
glass.position.z - 0.005
)
var origin := screen_to_housing * on_glass
for index in range(0, triangles.size(), 3):
if Geometry3D.ray_intersects_triangle(
origin,
outward,
triangles[index],
triangles[index + 1],
triangles[index + 2]
) != null:
return false
return true
func _shell_surrounds(shell: MeshInstance3D, point: Vector3) -> bool:
if shell == null or shell.mesh == null:
return false
var local := shell.global_transform.affine_inverse() * point
return shell.mesh.get_aabb().grow(-0.05).has_point(local)
func _sonar_texture_matches_glass(texture: Texture2D, sonar_screen: MeshInstance3D) -> bool:
if texture == null or sonar_screen == null:
return false
var glass := sonar_screen.mesh.get_aabb().size
var texture_ratio := float(texture.get_width()) / float(texture.get_height())
return absf(texture_ratio - glass.x / glass.y) < 0.01
func _alert_lamp_for(alert_box: CockpitAlertBox, level: DiveAlertLevel.Value) -> String:
alert_box.present(level)
return alert_box.active_indicator_name() if alert_box.active_indicator_count() == 1 else ""
func _binding_for(adapter: ProductionInputAdapter, action_id: StringName) -> String: