diff --git a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md index f8fea55..c73a16e 100644 --- a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md +++ b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md @@ -816,10 +816,11 @@ binnacle solidaire du tableau de bord, hors du hublot. La façade du tableau est structurée par des rails, séparateurs, vis et banques de commutateurs physiques. Passe du 4 octobre 2026 : les quatre volumes rectangulaires autour du hublot -sont remplacés par douze panneaux annulaires bombés, séparés par des joints fins -et ceinturés par un raidisseur. Les cadrans utilisent des graduations physiques -et les témoins colorés provisoires sont remplacés par trois voyants encastrés ; -un seul voyant reflète l'état d'alerte courant. +sont remplacés par une cloison annulaire bombée continue, serrée entre deux +raidisseurs métalliques et boulonnée. Le tableau reprend la hiérarchie de la +référence `design.jpg` : quatre cadrans principaux de tailles variées, petits +afficheurs de valeurs exactes et trois voyants encastrés dont un seul reflète +l'état d'alerte courant. - Construire une coque intérieure 3D fixe par rapport au véhicule, indépendante du regard de la caméra. diff --git a/game/presentation/cockpit/cockpit_analog_gauges.gd b/game/presentation/cockpit/cockpit_analog_gauges.gd index 0808b55..be2d5dc 100644 --- a/game/presentation/cockpit/cockpit_analog_gauges.gd +++ b/game/presentation/cockpit/cockpit_analog_gauges.gd @@ -6,8 +6,10 @@ const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd") var _materials: CockpitMaterialPalette +var _oxygen_needle: Node3D var _battery_needle: Node3D -var _pressure_needle: Node3D +var _hull_needle: Node3D +var _depth_needle: Node3D var _indicator_materials: Array[StandardMaterial3D] = [] var _active_indicator_count := 0 @@ -15,29 +17,49 @@ var _active_indicator_count := 0 func _init(materials: CockpitMaterialPalette) -> void: _materials = materials name = "AnalogGauges" - _build_binnacle() + _build_instrument_cluster() _build_indicator_bank() + _oxygen_needle = _add_gauge( + "OxygenGauge", + Vector3(-0.50, 0.12, 0.145), + "%", + 0.72 + ) _battery_needle = _add_gauge( "BatteryGauge", - Vector3(-0.095, 0.315, 0.145), - "%" + Vector3(-0.22, 0.12, 0.145), + "%", + 0.72 ) - _pressure_needle = _add_gauge( - "PressureGauge", - Vector3(0.095, 0.315, 0.145), - "BAR" + _hull_needle = _add_gauge( + "HullGauge", + Vector3(0.05, 0.12, 0.145), + "%", + 0.56 ) - _add_binnacle_label("CHARGE BAT.", Vector3(-0.095, 0.235, 0.160)) - _add_binnacle_label("PRESSION COQUE", Vector3(0.095, 0.235, 0.160)) + _depth_needle = _add_gauge( + "DepthGauge", + Vector3(0.40, 0.12, 0.145), + "%", + 0.90 + ) + _add_cluster_label("OXYGÈNE", Vector3(-0.50, 0.025, 0.170)) + _add_cluster_label("BATTERIE", Vector3(-0.22, 0.025, 0.170)) + _add_cluster_label("COQUE", Vector3(0.05, 0.045, 0.170)) + _add_cluster_label("PROFONDEUR", Vector3(0.40, 0.005, 0.170)) func present( + oxygen_ratio: float, battery_ratio: float, - pressure_ratio: float, + hull_ratio: float, + depth_ratio: float, alert_level: DiveAlertLevel.Value ) -> void: + _set_needle_ratio(_oxygen_needle, oxygen_ratio) _set_needle_ratio(_battery_needle, battery_ratio) - _set_needle_ratio(_pressure_needle, pressure_ratio) + _set_needle_ratio(_hull_needle, hull_ratio) + _set_needle_ratio(_depth_needle, depth_ratio) _update_indicator_bank(alert_level) @@ -49,23 +71,27 @@ func indicator_count() -> int: return _indicator_materials.size() +func gauge_count() -> int: + return 4 + + func active_indicator_count() -> int: return _active_indicator_count -func _build_binnacle() -> void: +func _build_instrument_cluster() -> void: CockpitMeshFactory.add_box( - "BinnacleHousing", + "InstrumentClusterHousing", self, - Vector3(0.0, 0.290, 0.095), - Vector3(0.40, 0.180, 0.105), + Vector3(-0.04, 0.105, 0.095), + Vector3(1.26, 0.255, 0.105), _materials.overhead_steel ) CockpitMeshFactory.add_box( - "BinnacleFoot", + "InstrumentClusterFoot", self, - Vector3(0.0, 0.205, 0.075), - Vector3(0.47, 0.040, 0.14), + Vector3(-0.04, -0.025, 0.075), + Vector3(1.32, 0.040, 0.14), _materials.dark_steel ) @@ -81,11 +107,11 @@ func _build_indicator_bank() -> void: ] var captions: Array[String] = ["SYS", "ATTN", "ALRM"] for indicator_index in colors.size(): - var x := -0.055 + indicator_index * 0.055 + var y := 0.165 - indicator_index * 0.050 CockpitMeshFactory.add_cylinder( "%sBezel" % captions[indicator_index], root, - Vector3(x, 0.198, 0.156), + Vector3(0.205, y, 0.156), 0.016, 0.012, _materials.brass, @@ -96,7 +122,7 @@ func _build_indicator_bank() -> void: CockpitMeshFactory.add_cylinder( "%sLens" % captions[indicator_index], root, - Vector3(x, 0.198, 0.165), + Vector3(0.205, y, 0.165), 0.009, 0.008, material, @@ -108,7 +134,7 @@ func _build_indicator_bank() -> void: label.font_size = 8 label.pixel_size = 0.00042 label.modulate = Color(0.70, 0.67, 0.56) - label.position = Vector3(x, 0.177, 0.168) + label.position = Vector3(0.235, y, 0.168) root.add_child(label) @@ -136,7 +162,7 @@ func _update_indicator_bank(alert_level: DiveAlertLevel.Value) -> void: _active_indicator_count = 1 -func _add_binnacle_label(caption: String, position: Vector3) -> void: +func _add_cluster_label(caption: String, position: Vector3) -> void: var label := Label3D.new() label.name = "%sLabel" % caption.to_pascal_case().replace(" ", "") label.text = caption @@ -147,18 +173,23 @@ func _add_binnacle_label(caption: String, position: Vector3) -> void: add_child(label) -func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3D: +func _add_gauge( + gauge_name: String, + position: Vector3, + caption: String, + gauge_scale: float +) -> Node3D: var gauge := Node3D.new() gauge.name = gauge_name gauge.position = position - gauge.scale = Vector3(0.62, 0.62, 0.62) + gauge.scale = Vector3(gauge_scale, gauge_scale, gauge_scale) add_child(gauge) CockpitMeshFactory.add_box( "MountingPlate", gauge, Vector3(0.0, 0.0, -0.025), Vector3(0.225, 0.225, 0.055), - _materials.dark_steel + _materials.overhead_steel ) for screw_position: Vector2 in [ Vector2(-0.086, -0.086), @@ -182,7 +213,7 @@ func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3 bezel_mesh.height = 0.035 bezel_mesh.radial_segments = 32 bezel.mesh = bezel_mesh - bezel.material_override = _materials.brass + bezel.material_override = _materials.brushed_steel bezel.rotation_degrees.x = 90.0 gauge.add_child(bezel) var face := MeshInstance3D.new() diff --git a/game/presentation/cockpit/cockpit_dashboard_detailing.gd b/game/presentation/cockpit/cockpit_dashboard_detailing.gd index e63e6a8..07359c9 100644 --- a/game/presentation/cockpit/cockpit_dashboard_detailing.gd +++ b/game/presentation/cockpit/cockpit_dashboard_detailing.gd @@ -64,8 +64,8 @@ func _build_structural_frame() -> void: CockpitMeshFactory.add_box( "ScreenSeparator", self, - Vector3(x, 0.015, 0.118), - Vector3(0.038, 0.39, 0.048), + Vector3(x, -0.075, 0.118), + Vector3(0.038, 0.16, 0.048), _materials.brass ) for screw_position: Vector2 in [ diff --git a/game/presentation/cockpit/cockpit_detailing.gd b/game/presentation/cockpit/cockpit_detailing.gd index c9123a7..c5a39a4 100644 --- a/game/presentation/cockpit/cockpit_detailing.gd +++ b/game/presentation/cockpit/cockpit_detailing.gd @@ -14,7 +14,6 @@ const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh var _materials: CockpitMaterialPalette var _painted_steel: Material var _dark_steel: Material -var _overhead_steel: Material var _brass: Material var _copper: Material var _analog_gauges: CockpitAnalogGauges @@ -39,10 +38,12 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: return var snapshot = update.snapshot _analog_gauges.present( + snapshot.oxygen.ratio, snapshot.battery.ratio, + snapshot.hull.ratio, clampf( - snapshot.hull.exterior_pressure_bar - / maxf(snapshot.hull.certified_pressure_bar, 0.001), + snapshot.depth.current_m + / maxf(snapshot.depth.certified_m, 0.001), 0.0, 1.0 ), @@ -89,7 +90,6 @@ func _create_materials() -> void: _materials = CockpitMaterialPalette.new() _painted_steel = _materials.painted_steel _dark_steel = _materials.dark_steel - _overhead_steel = _materials.overhead_steel _brass = _materials.brass _copper = _materials.copper @@ -116,7 +116,7 @@ func _apply_materials_to_existing_cockpit() -> void: ] ) _apply_material_to_paths( - _overhead_steel, + _materials.overhead_steel, [ "CabinShell/Ceiling", "PortholeAssembly/TopBulkhead", @@ -240,7 +240,7 @@ func _build_overhead_console() -> void: root, Vector3(0.0, 0.94, -1.06), Vector3(0.30, 0.09, 0.12), - _overhead_steel, + _materials.overhead_steel, Vector3(-8.0, 0.0, 0.0) ) CockpitMeshFactory.add_box( diff --git a/game/presentation/cockpit/cockpit_instrument_panel.gd b/game/presentation/cockpit/cockpit_instrument_panel.gd index c5d45b4..45c2d98 100644 --- a/game/presentation/cockpit/cockpit_instrument_panel.gd +++ b/game/presentation/cockpit/cockpit_instrument_panel.gd @@ -30,20 +30,16 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: return var snapshot = update.snapshot _resources_label.text = ( - "BATTERIE\n%.1f / %.1f kWh · %.0f %%\n%.0f kW\n\nOXYGÈNE\n%.1f / %.1f L · %.0f %%\n%.1f L/min" + "BAT %.1f kWh · O₂ %.0f %%\n%.0f kW · %.1f L/min" % [ snapshot.battery.remaining_kwh, - snapshot.battery.maximum_kwh, - snapshot.battery.ratio * 100.0, - snapshot.battery.current_power_kw, - snapshot.oxygen.remaining_l, - snapshot.oxygen.maximum_l, snapshot.oxygen.ratio * 100.0, + snapshot.battery.current_power_kw, snapshot.oxygen.consumption_l_per_min, ] ) _navigation_label.text = ( - "PROFONDEUR\n%.1f m\nMAX. CONSEILLÉ %.1f m\n\nVITESSE\n%.2f m/s" + "PROF. %.1f / %.1f m\nVIT. %.2f m/s" % [ snapshot.depth.current_m, snapshot.depth.certified_m, @@ -51,10 +47,8 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: ] ) _integrity_label.text = ( - "COQUE\n%.1f / %.1f · %.0f %%\n\nPRESSION\n%.2f / %.2f bar" + "COQUE %.0f %% · %.2f bar\nLIMITE %.2f bar" % [ - snapshot.hull.current_integrity, - snapshot.hull.maximum_integrity, snapshot.hull.ratio * 100.0, snapshot.hull.exterior_pressure_bar, snapshot.hull.certified_pressure_bar, diff --git a/game/presentation/cockpit/cockpit_interior.tscn b/game/presentation/cockpit/cockpit_interior.tscn index a4d0f03..9f5e380 100644 --- a/game/presentation/cockpit/cockpit_interior.tscn +++ b/game/presentation/cockpit/cockpit_interior.tscn @@ -267,82 +267,82 @@ mesh = SubResource("UnitBoxMesh") material_override = SubResource("PaintedMaterial") [node name="ResourcesFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(-0.5, 0.03, 0.088) -scale = Vector3(0.45, 0.31, 0.025) +position = Vector3(-0.5, -0.075, 0.088) +scale = Vector3(0.45, 0.115, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="ResourcesScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(-0.5, 0.03, 0.102) -scale = Vector3(0.41, 0.27, 1) +position = Vector3(-0.5, -0.075, 0.102) +scale = Vector3(0.41, 0.085, 1) mesh = SubResource("ScreenQuadMesh") material_override = SubResource("ScreenMaterial") [node name="ResourcesLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(-0.5, 0.03, 0.108) -pixel_size = 0.00115 -font_size = 20 +position = Vector3(-0.5, -0.075, 0.108) +pixel_size = 0.00082 +font_size = 14 outline_size = 2 -text = "BATTERIE\n— / — kWh\n— kW\n\nOXYGÈNE\n— / — L\n— L/min" +text = "BAT — kWh · O₂ — %\n— kW · — L/min" modulate = Color(0.45, 0.89, 0.9, 1) [node name="NavigationFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0, 0.03, 0.088) -scale = Vector3(0.4, 0.31, 0.025) +position = Vector3(0, -0.075, 0.088) +scale = Vector3(0.4, 0.115, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="NavigationScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0, 0.03, 0.102) -scale = Vector3(0.36, 0.27, 1) +position = Vector3(0, -0.075, 0.102) +scale = Vector3(0.36, 0.085, 1) mesh = SubResource("ScreenQuadMesh") material_override = SubResource("ScreenMaterial") [node name="NavigationLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(0, 0.03, 0.108) -pixel_size = 0.00115 -font_size = 20 +position = Vector3(0, -0.075, 0.108) +pixel_size = 0.00082 +font_size = 14 outline_size = 2 -text = "PROFONDEUR\n— m\nMAX. — m\n\nVITESSE\n— m/s" +text = "PROF. — / — m\nVIT. — m/s" modulate = Color(0.45, 0.89, 0.9, 1) [node name="IntegrityFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0.5, 0.03, 0.088) -scale = Vector3(0.45, 0.31, 0.025) +position = Vector3(0.5, -0.075, 0.088) +scale = Vector3(0.45, 0.115, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="IntegrityScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0.5, 0.03, 0.102) -scale = Vector3(0.41, 0.27, 1) +position = Vector3(0.5, -0.075, 0.102) +scale = Vector3(0.41, 0.085, 1) mesh = SubResource("ScreenQuadMesh") material_override = SubResource("ScreenMaterial") [node name="IntegrityLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(0.5, 0.03, 0.108) -pixel_size = 0.00115 -font_size = 20 +position = Vector3(0.5, -0.075, 0.108) +pixel_size = 0.00082 +font_size = 14 outline_size = 2 -text = "COQUE\n— / —\n\nPRESSION\n— / — bar" +text = "COQUE — % · — bar\nLIMITE — bar" modulate = Color(0.45, 0.89, 0.9, 1) [node name="ReturnLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(0.32, 0.205, 0.108) -pixel_size = 0.00082 -font_size = 15 +position = Vector3(0.61, 0.245, 0.17) +pixel_size = 0.00062 +font_size = 12 outline_size = 2 text = "RETOUR · INCONNU" modulate = Color(1, 0.75, 0.2, 1) [node name="StatusLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(-0.34, 0.205, 0.108) -pixel_size = 0.00082 -font_size = 15 +position = Vector3(-0.61, 0.245, 0.17) +pixel_size = 0.00062 +font_size = 12 outline_size = 2 text = "SYSTÈMES · NORMAL" modulate = Color(0.45, 0.89, 0.9, 1) @@ -413,16 +413,16 @@ text = "SONAR\nMODULE EN ATTENTE" modulate = Color(0.34, 0.6, 0.6, 1) [node name="EmergencyDial" type="MeshInstance3D" parent="."] -position = Vector3(-0.6, 0.39, -1.15) -scale = Vector3(0.38, 0.38, 0.38) +position = Vector3(-0.7, 0.32, -1.15) +scale = Vector3(0.18, 0.18, 0.18) rotation_degrees = Vector3(90, 0, 0) mesh = SubResource("DialMesh") material_override = SubResource("RedMaterial") [node name="EmergencyLabel" type="Label3D" parent="."] -position = Vector3(-0.6, 0.3, -1.125) -pixel_size = 0.0009 -font_size = 15 +position = Vector3(-0.7, 0.275, -1.125) +pixel_size = 0.00072 +font_size = 12 outline_size = 2 text = "ÉJECTION · X" modulate = Color(1, 0.48, 0.38, 1) diff --git a/game/presentation/cockpit/cockpit_material_palette.gd b/game/presentation/cockpit/cockpit_material_palette.gd index e9a36e9..1fc7300 100644 --- a/game/presentation/cockpit/cockpit_material_palette.gd +++ b/game/presentation/cockpit/cockpit_material_palette.gd @@ -11,6 +11,7 @@ const GAUGE_FACE_TEXTURE_PATH := ( var painted_steel: Material var dark_steel: Material var overhead_steel: StandardMaterial3D +var brushed_steel: StandardMaterial3D var brass: Material var copper: Material var rubber: Material @@ -43,6 +44,7 @@ func _init() -> void: 0.56, Vector3(1.6, 1.6, 1.6) ) + brushed_steel = _plain_material(Color(0.27, 0.30, 0.29), 0.82, 0.30) brass = _weathered_material( Color(0.28, 0.18, 0.045), Color(0.08, 0.16, 0.13), diff --git a/game/presentation/cockpit/cockpit_pressure_bulkhead.gd b/game/presentation/cockpit/cockpit_pressure_bulkhead.gd index 7ae1aa0..06b4ac6 100644 --- a/game/presentation/cockpit/cockpit_pressure_bulkhead.gd +++ b/game/presentation/cockpit/cockpit_pressure_bulkhead.gd @@ -8,9 +8,10 @@ const CockpitMeshFactory = preload( "res://game/presentation/cockpit/cockpit_mesh_factory.gd" ) -const PANEL_COUNT := 12 -const ANGULAR_STEPS := 4 -const RADIAL_STEPS := 3 +const PANEL_COUNT := 1 +const FASTENER_COUNT := 16 +const ANGULAR_STEPS := 64 +const RADIAL_STEPS := 4 const INNER_RADIUS := 0.72 const OUTER_RADIUS := 1.11 @@ -21,6 +22,7 @@ var _panel_count := 0 func _ready() -> void: _materials = CockpitMaterialPalette.new() _hide_rectangular_whitebox() + _style_existing_frame() _build_curved_panels() _build_outer_rib() _build_fasteners() @@ -45,12 +47,20 @@ func _hide_rectangular_whitebox() -> void: whitebox.visible = false +func _style_existing_frame() -> void: + var frame := get_parent().get_node_or_null("WindowFrame") as MeshInstance3D + if frame != null: + frame.material_override = _materials.brushed_steel + for child: Node in get_parent().get_children(): + if child is MeshInstance3D and child.name.begins_with("Bolt"): + (child as MeshInstance3D).material_override = _materials.dark_steel + + func _build_curved_panels() -> void: var panel_angle := TAU / float(PANEL_COUNT) - var seam_half_width := deg_to_rad(0.22) for panel_index in PANEL_COUNT: - var start_angle := panel_index * panel_angle + seam_half_width - var end_angle := (panel_index + 1) * panel_angle - seam_half_width + var start_angle := panel_index * panel_angle + var end_angle := (panel_index + 1) * panel_angle var panel := MeshInstance3D.new() panel.name = "PressurePanel%02d" % (panel_index + 1) panel.mesh = _create_curved_panel_mesh(start_angle, end_angle) @@ -128,11 +138,23 @@ func _build_outer_rib() -> void: rib.rotation_degrees.x = 90.0 rib.position.z = 0.025 add_child(rib) + var inner_rib := MeshInstance3D.new() + inner_rib.name = "InnerPressureRib" + var inner_torus := TorusMesh.new() + inner_torus.inner_radius = 0.695 + inner_torus.outer_radius = 0.755 + inner_torus.rings = 64 + inner_torus.ring_segments = 12 + inner_rib.mesh = inner_torus + inner_rib.material_override = _materials.brushed_steel + inner_rib.rotation_degrees.x = 90.0 + inner_rib.position.z = 0.075 + add_child(inner_rib) func _build_fasteners() -> void: - for fastener_index in PANEL_COUNT: - var angle := (fastener_index + 0.5) * TAU / float(PANEL_COUNT) + for fastener_index in FASTENER_COUNT: + var angle := (fastener_index + 0.5) * TAU / float(FASTENER_COUNT) CockpitMeshFactory.add_cylinder( "OuterFastener", self, diff --git a/tests/integration/run_vertical_slice_foundation.gd b/tests/integration/run_vertical_slice_foundation.gd index 14c4863..b4838b4 100644 --- a/tests/integration/run_vertical_slice_foundation.gd +++ b/tests/integration/run_vertical_slice_foundation.gd @@ -19,7 +19,7 @@ const ProductionInputAdapter = preload( "res://game/infrastructure/input/production_input_adapter.gd" ) -const TEST_COUNT := 64 +const TEST_COUNT := 65 var _failures: Array[String] = [] @@ -131,8 +131,9 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: _expect(detailing.textures_are_loaded(), "cockpit loads its overhead and 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(pressure_bulkhead.panel_count() == 12, "porthole bulkhead uses twelve curved pressure panels") + _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("kWh" in instruments.resources_text() and "kW" in instruments.resources_text(), "battery instrument has exact units")