diff --git a/README.md b/README.md index 2308a84..4a3277e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ production sous `game/` n'en dépend. télémétrie typés. - Habitacle 3D solidaire de la coque : hublot occultant, consoles, commandes, éclairage, structure de coque renforcée, conduites, jauges analogiques et - emplacement réservé au sonar. + tableau d'instruments structuré, et emplacement réservé au sonar. - Instruments physiques à valeurs réelles ; le HUD reste réservé aux alertes, à l'éjection et aux états terminaux. - Pause qui suspend les ticks, confirmations de sortie et premier remappage avec diff --git a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md index 12f1733..f0bb514 100644 --- a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md +++ b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md @@ -811,6 +811,9 @@ Complément du 3 octobre 2026 : le plafond, le boîtier supérieur et les faces jauges utilisent des textures bitmap dédiées. Les cadrans reçoivent en plus des graduations, une aiguille peinte et une vitre distincte ; le plafonnier est réduit et protégé par une grille pour conserver des proportions crédibles. +Les jauges de charge batterie et de pression coque sont regroupées dans un +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. - 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 47de666..cf5e866 100644 --- a/game/presentation/cockpit/cockpit_analog_gauges.gd +++ b/game/presentation/cockpit/cockpit_analog_gauges.gd @@ -12,8 +12,19 @@ var _pressure_needle: Node3D func _init(materials: CockpitMaterialPalette) -> void: _materials = materials name = "AnalogGauges" - _battery_needle = _add_gauge("BatteryGauge", Vector3(-0.34, 0.48, -1.14), "BAT") - _pressure_needle = _add_gauge("PressureGauge", Vector3(0.34, 0.48, -1.14), "BAR") + _build_binnacle() + _battery_needle = _add_gauge( + "BatteryGauge", + Vector3(-0.095, 0.315, 0.145), + "%" + ) + _pressure_needle = _add_gauge( + "PressureGauge", + Vector3(0.095, 0.315, 0.145), + "BAR" + ) + _add_binnacle_label("CHARGE BAT.", Vector3(-0.095, 0.235, 0.160)) + _add_binnacle_label("PRESSION COQUE", Vector3(0.095, 0.235, 0.160)) func present(battery_ratio: float, pressure_ratio: float) -> void: @@ -25,11 +36,39 @@ func battery_needle_degrees() -> float: return 0.0 if _battery_needle == null else _battery_needle.rotation_degrees.z +func _build_binnacle() -> void: + CockpitMeshFactory.add_box( + "BinnacleHousing", + self, + Vector3(0.0, 0.290, 0.095), + Vector3(0.40, 0.180, 0.105), + _materials.overhead_steel + ) + CockpitMeshFactory.add_box( + "BinnacleFoot", + self, + Vector3(0.0, 0.205, 0.075), + Vector3(0.47, 0.040, 0.14), + _materials.dark_steel + ) + + +func _add_binnacle_label(caption: String, position: Vector3) -> void: + var label := Label3D.new() + label.name = "%sLabel" % caption.to_pascal_case().replace(" ", "") + label.text = caption + label.font_size = 12 + label.pixel_size = 0.00055 + label.modulate = Color(0.76, 0.72, 0.60) + label.position = position + add_child(label) + + func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3D: var gauge := Node3D.new() gauge.name = gauge_name gauge.position = position - gauge.scale = Vector3(0.78, 0.78, 0.78) + gauge.scale = Vector3(0.62, 0.62, 0.62) add_child(gauge) CockpitMeshFactory.add_box( "MountingPlate", diff --git a/game/presentation/cockpit/cockpit_dashboard_detailing.gd b/game/presentation/cockpit/cockpit_dashboard_detailing.gd new file mode 100644 index 0000000..dffae64 --- /dev/null +++ b/game/presentation/cockpit/cockpit_dashboard_detailing.gd @@ -0,0 +1,114 @@ +class_name CockpitDashboardDetailing +extends Node3D + +const CockpitMaterialPalette = preload( + "res://game/presentation/cockpit/cockpit_material_palette.gd" +) +const CockpitMeshFactory = preload( + "res://game/presentation/cockpit/cockpit_mesh_factory.gd" +) + +var _materials: CockpitMaterialPalette +var _control_count := 0 + + +func _init(materials: CockpitMaterialPalette) -> void: + _materials = materials + name = "DashboardDetailing" + _build_structural_frame() + _build_switch_banks() + + +func control_count() -> int: + return _control_count + + +func apply_screen_materials(dashboard: Node3D) -> void: + for screen_name: String in ["ResourcesScreen", "NavigationScreen", "IntegrityScreen"]: + var screen := dashboard.get_node_or_null(screen_name) as MeshInstance3D + if screen != null: + screen.material_override = _materials.instrument_screen + + +func _build_structural_frame() -> void: + CockpitMeshFactory.add_box( + "TopRail", + self, + Vector3(0.0, 0.255, 0.115), + Vector3(1.50, 0.055, 0.055), + _materials.dark_steel + ) + CockpitMeshFactory.add_box( + "BottomRail", + self, + Vector3(0.0, -0.205, 0.115), + Vector3(1.50, 0.050, 0.055), + _materials.dark_steel + ) + for x: float in [-0.755, 0.755]: + CockpitMeshFactory.add_box( + "SideRail", + self, + Vector3(x, 0.025, 0.115), + Vector3(0.050, 0.46, 0.055), + _materials.dark_steel + ) + for x: float in [-0.238, 0.238]: + CockpitMeshFactory.add_box( + "ScreenSeparator", + self, + Vector3(x, 0.015, 0.118), + Vector3(0.038, 0.39, 0.048), + _materials.brass + ) + for screw_position: Vector2 in [ + Vector2(-0.72, -0.18), + Vector2(-0.72, 0.23), + Vector2(0.72, -0.18), + Vector2(0.72, 0.23), + Vector2(-0.24, -0.18), + Vector2(0.24, -0.18), + ]: + CockpitMeshFactory.add_cylinder( + "PanelScrew", + self, + Vector3(screw_position.x, screw_position.y, 0.151), + 0.010, + 0.010, + _materials.brass, + Vector3(90.0, 0.0, 0.0) + ) + + +func _build_switch_banks() -> void: + _add_switch_bank(-0.59, "AUX") + _add_switch_bank(0.59, "POMPES") + + +func _add_switch_bank(center_x: float, caption: String) -> void: + CockpitMeshFactory.add_box( + "SwitchPlate", + self, + Vector3(center_x, -0.205, 0.145), + Vector3(0.25, 0.060, 0.020), + _materials.overhead_steel + ) + for offset: float in [-0.075, -0.025, 0.025, 0.075]: + CockpitMeshFactory.add_cylinder( + "ToggleSwitch", + self, + Vector3(center_x + offset, -0.195, 0.171), + 0.006, + 0.030, + _materials.dark_steel, + Vector3(90.0, 0.0, 0.0) + ) + _control_count += 1 + var label := Label3D.new() + label.name = "%sLabel" % caption.to_pascal_case() + label.text = caption + label.font_size = 10 + label.pixel_size = 0.00055 + label.modulate = Color(0.72, 0.69, 0.56) + label.position = Vector3(center_x, -0.235, 0.165) + add_child(label) diff --git a/game/presentation/cockpit/cockpit_detailing.gd b/game/presentation/cockpit/cockpit_detailing.gd index ce023cb..fa904a7 100644 --- a/game/presentation/cockpit/cockpit_detailing.gd +++ b/game/presentation/cockpit/cockpit_detailing.gd @@ -5,6 +5,9 @@ const DiveUpdate = preload("res://game/domain/dive/dive_update.gd") const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd") const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd") const CockpitAnalogGauges = preload("res://game/presentation/cockpit/cockpit_analog_gauges.gd") +const CockpitDashboardDetailing = preload( + "res://game/presentation/cockpit/cockpit_dashboard_detailing.gd" +) const CockpitMaterialPalette = preload("res://game/presentation/cockpit/cockpit_material_palette.gd") const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh_factory.gd") @@ -14,9 +17,8 @@ var _dark_steel: Material var _overhead_steel: Material var _brass: Material var _copper: Material -var _rubber: Material -var _warm_emission: StandardMaterial3D var _analog_gauges: CockpitAnalogGauges +var _dashboard_detailing: CockpitDashboardDetailing var _warm_practical: OmniLight3D var _emergency_light: OmniLight3D @@ -28,7 +30,7 @@ func _ready() -> void: _build_life_support_pipes() _build_overhead_console() _build_panel_hardware() - _build_analog_gauges() + _build_dashboard_instrumentation() _build_practical_lights() @@ -74,6 +76,14 @@ func textures_are_loaded() -> bool: ) +func dashboard_control_count() -> int: + return 0 if _dashboard_detailing == null else _dashboard_detailing.control_count() + + +func gauges_are_dashboard_mounted() -> bool: + return _analog_gauges != null and _analog_gauges.get_parent() == get_parent().get_node("Dashboard") + + func _create_materials() -> void: _materials = CockpitMaterialPalette.new() _painted_steel = _materials.painted_steel @@ -81,8 +91,6 @@ func _create_materials() -> void: _overhead_steel = _materials.overhead_steel _brass = _materials.brass _copper = _materials.copper - _rubber = _materials.rubber - _warm_emission = _materials.warm_emission func _apply_materials_to_existing_cockpit() -> void: @@ -91,7 +99,6 @@ func _apply_materials_to_existing_cockpit() -> void: [ "CabinShell/LeftWall", "CabinShell/RightWall", - "Dashboard/MainPanel", "LeftConsole", "RightConsole", ] @@ -112,6 +119,7 @@ func _apply_materials_to_existing_cockpit() -> void: [ "CabinShell/Ceiling", "PortholeAssembly/TopBulkhead", + "Dashboard/MainPanel", ] ) _apply_material_to_paths( @@ -125,8 +133,6 @@ func _apply_materials_to_existing_cockpit() -> void: "VerticalKnob", ] ) - - func _apply_material_to_paths(material: Material, paths: Array[String]) -> void: var cockpit := get_parent() for path: String in paths: @@ -241,7 +247,7 @@ func _build_overhead_console() -> void: root, Vector3(0.0, 0.895, -0.985), Vector3(0.13, 0.010, 0.026), - _warm_emission, + _materials.warm_emission, Vector3(-8.0, 0.0, 0.0) ) for guard_index in 5: @@ -262,7 +268,7 @@ func _build_overhead_console() -> void: Vector3(x, 0.965, -0.985), 0.004, 0.018, - _rubber, + _materials.rubber, Vector3(90.0, 0.0, 0.0) ) @@ -302,9 +308,15 @@ func _build_panel_hardware() -> void: root.add_child(label) -func _build_analog_gauges() -> void: +func _build_dashboard_instrumentation() -> void: + var dashboard := get_parent().get_node_or_null("Dashboard") as Node3D + if dashboard == null: + return + _dashboard_detailing = CockpitDashboardDetailing.new(_materials) + dashboard.add_child(_dashboard_detailing) + _dashboard_detailing.apply_screen_materials(dashboard) _analog_gauges = CockpitAnalogGauges.new(_materials) - add_child(_analog_gauges) + dashboard.add_child(_analog_gauges) func _build_practical_lights() -> void: diff --git a/game/presentation/cockpit/cockpit_material_palette.gd b/game/presentation/cockpit/cockpit_material_palette.gd index b6a40eb..e9a36e9 100644 --- a/game/presentation/cockpit/cockpit_material_palette.gd +++ b/game/presentation/cockpit/cockpit_material_palette.gd @@ -17,6 +17,7 @@ var rubber: Material var gauge_face: StandardMaterial3D var gauge_glass: StandardMaterial3D var needle_paint: StandardMaterial3D +var instrument_screen: ShaderMaterial var warm_emission: StandardMaterial3D @@ -65,6 +66,7 @@ func _init() -> void: ) gauge_glass = _glass_material() needle_paint = _plain_material(Color(0.68, 0.055, 0.022), 0.12, 0.44) + instrument_screen = _screen_material() warm_emission = _emissive_material(Color(0.96, 0.52, 0.18), 2.4) @@ -167,6 +169,30 @@ func _glass_material() -> StandardMaterial3D: return material +func _screen_material() -> ShaderMaterial: + var shader := Shader.new() + shader.code = """ +shader_type spatial; +render_mode diffuse_burley, specular_schlick_ggx; + +void fragment() { + vec2 centered = UV - vec2(0.5); + float vignette = 1.0 - smoothstep(0.28, 0.72, length(centered)); + float scanline = 0.94 + 0.06 * sin(UV.y * 720.0); + float edge = mix(0.68, 1.0, vignette); + vec3 phosphor = vec3(0.025, 0.105, 0.11) * edge * scanline; + ALBEDO = phosphor; + EMISSION = phosphor * 0.38; + METALLIC = 0.08; + ROUGHNESS = 0.18; + SPECULAR = 0.82; +} +""" + var material := ShaderMaterial.new() + material.shader = shader + return material + + func _emissive_material(color: Color, energy: float) -> StandardMaterial3D: var material := StandardMaterial3D.new() material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED diff --git a/tests/integration/run_vertical_slice_foundation.gd b/tests/integration/run_vertical_slice_foundation.gd index e7a40b7..e81ab7e 100644 --- a/tests/integration/run_vertical_slice_foundation.gd +++ b/tests/integration/run_vertical_slice_foundation.gd @@ -16,7 +16,7 @@ const ProductionInputAdapter = preload( "res://game/infrastructure/input/production_input_adapter.gd" ) -const TEST_COUNT := 56 +const TEST_COUNT := 58 var _failures: Array[String] = [] @@ -112,6 +112,8 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: _expect(detailing.pipe_segment_count() >= 13, "cockpit exposes a complete life-support pipe run") _expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot") _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("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")