diff --git a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md index c73a16e..1406e72 100644 --- a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md +++ b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md @@ -63,6 +63,8 @@ hors reprise après échec. Ces nombres seront corrigés par playtest. ### Plongée - POV intérieur et hublot circulaire. +- Direction visuelle du cockpit alignée sur + `docs/visual_targets/cockpit-objective-v1.jpg`, sans exigence pixel perfect. - Pilotage inertiel avec retour immédiat des commandes. - Oxygène, batterie électrique, profondeur, pression et intégrité. - Alarmes texte, couleur et son. @@ -817,10 +819,10 @@ structurée par des rails, séparateurs, vis et banques de commutateurs physique Passe du 4 octobre 2026 : les quatre volumes rectangulaires autour du hublot 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. +raidisseurs métalliques et boulonnée. La cible approuvée +`docs/visual_targets/cockpit-objective-v1.jpg` fixe la hiérarchie : quatre +cadrans principaux de tailles variées, petits afficheurs de valeurs exactes, +sonar bathymétrique séparé, équipements latéraux et lampes navales en cage. - Construire une coque intérieure 3D fixe par rapport au véhicule, indépendante du regard de la caméra. diff --git a/docs/visual_targets/README.md b/docs/visual_targets/README.md new file mode 100644 index 0000000..d70ef07 --- /dev/null +++ b/docs/visual_targets/README.md @@ -0,0 +1,26 @@ +# Cibles visuelles validées + +Ce dossier contient les références visuelles explicitement approuvées pour la +production. Elles cadrent une intention et une hiérarchie visuelle ; elles ne +constituent pas une exigence de reproduction pixel perfect. + +## Cockpit du mini-sous-marin + +`cockpit-objective-v1.jpg` est la cible validée le 4 octobre 2026 pour la vue +intérieure du sous-marin. Elle a été générée spécifiquement pour Ransom's Wake +avec le générateur d'images OpenAI intégré, à partir de la direction artistique +fournie par le projet. + +La scène de production doit en conserver les caractéristiques principales : + +- grand hublot circulaire cerclé d'acier boulonné ; +- tableau inférieur dense avec quatre cadrans analogiques lisibles ; +- sonar bathymétrique séparé sur le côté droit ; +- réservoirs, vannes et commandes de survie sur le côté gauche ; +- boîtiers électriques, gaines et câbles sur le côté droit ; +- deux lampes navales chaudes protégées par une cage ; +- acier peint usé, métal brossé, laiton vieilli et caoutchouc sombre. + +Les valeurs exactes restent produites par les contrats de télémétrie du jeu. +La présentation peut simplifier le nombre de petites pièces pour garantir la +lisibilité à 720p et les performances de la verticale. diff --git a/docs/visual_targets/cockpit-objective-v1.jpg b/docs/visual_targets/cockpit-objective-v1.jpg new file mode 100644 index 0000000..2d0b94d Binary files /dev/null and b/docs/visual_targets/cockpit-objective-v1.jpg differ diff --git a/game/presentation/cockpit/assets/README.md b/game/presentation/cockpit/assets/README.md index 4bf2589..c0cc8bf 100644 --- a/game/presentation/cockpit/assets/README.md +++ b/game/presentation/cockpit/assets/README.md @@ -20,3 +20,18 @@ hub, glass, text, numbers, logo or watermark. Both source outputs were resized to 1024 × 1024 and stripped of metadata for runtime use. The original generated outputs remain in the local Codex image generation store and are not required by the game. + +## Dedicated instrument faces + +`oxygen_gauge_face_albedo_v2.png`, `battery_gauge_face_albedo_v2.png`, +`hull_gauge_face_albedo_v2.png` and `depth_gauge_face_albedo_v2.png` were +generated on 4 October 2026 with the same built-in tool. The approved cockpit +target and then the oxygen face were used only as project-owned style +references. + +Prompt summary: orthographic mechanical submarine gauges in aged ivory enamel, +with exact French labels and units, dense numerical scales, restrained patina, +and green, amber and red operating sectors. The generated needles, bezels and +glass were explicitly excluded because those parts are modeled and animated in +Godot. The source outputs were resized to 1024 × 1024 and given a circular alpha +mask for flat, distortion-free mounting inside the physical bezels. diff --git a/game/presentation/cockpit/assets/battery_gauge_face_albedo_v2.png b/game/presentation/cockpit/assets/battery_gauge_face_albedo_v2.png new file mode 100644 index 0000000..c22a1fe Binary files /dev/null and b/game/presentation/cockpit/assets/battery_gauge_face_albedo_v2.png differ diff --git a/game/presentation/cockpit/assets/depth_gauge_face_albedo_v2.png b/game/presentation/cockpit/assets/depth_gauge_face_albedo_v2.png new file mode 100644 index 0000000..bd43d15 Binary files /dev/null and b/game/presentation/cockpit/assets/depth_gauge_face_albedo_v2.png differ diff --git a/game/presentation/cockpit/assets/hull_gauge_face_albedo_v2.png b/game/presentation/cockpit/assets/hull_gauge_face_albedo_v2.png new file mode 100644 index 0000000..ba4b7d4 Binary files /dev/null and b/game/presentation/cockpit/assets/hull_gauge_face_albedo_v2.png differ diff --git a/game/presentation/cockpit/assets/oxygen_gauge_face_albedo_v2.png b/game/presentation/cockpit/assets/oxygen_gauge_face_albedo_v2.png new file mode 100644 index 0000000..34a3c02 Binary files /dev/null and b/game/presentation/cockpit/assets/oxygen_gauge_face_albedo_v2.png differ diff --git a/game/presentation/cockpit/cockpit_analog_gauges.gd b/game/presentation/cockpit/cockpit_analog_gauges.gd index be2d5dc..03a1efb 100644 --- a/game/presentation/cockpit/cockpit_analog_gauges.gd +++ b/game/presentation/cockpit/cockpit_analog_gauges.gd @@ -13,7 +13,6 @@ var _depth_needle: Node3D var _indicator_materials: Array[StandardMaterial3D] = [] var _active_indicator_count := 0 - func _init(materials: CockpitMaterialPalette) -> void: _materials = materials name = "AnalogGauges" @@ -21,33 +20,28 @@ func _init(materials: CockpitMaterialPalette) -> void: _build_indicator_bank() _oxygen_needle = _add_gauge( "OxygenGauge", - Vector3(-0.50, 0.12, 0.145), - "%", - 0.72 + Vector3(-0.52, 0.12, 0.145), + 0.94, + _materials.oxygen_gauge_face ) _battery_needle = _add_gauge( "BatteryGauge", Vector3(-0.22, 0.12, 0.145), - "%", - 0.72 + 0.94, + _materials.battery_gauge_face ) _hull_needle = _add_gauge( "HullGauge", - Vector3(0.05, 0.12, 0.145), - "%", - 0.56 + Vector3(0.09, 0.12, 0.145), + 0.86, + _materials.hull_gauge_face ) _depth_needle = _add_gauge( "DepthGauge", - Vector3(0.40, 0.12, 0.145), - "%", - 0.90 + Vector3(0.47, 0.12, 0.145), + 1.12, + _materials.depth_gauge_face ) - _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, @@ -66,15 +60,12 @@ func present( func battery_needle_degrees() -> float: return 0.0 if _battery_needle == null else _battery_needle.rotation_degrees.z - func indicator_count() -> int: return _indicator_materials.size() - func gauge_count() -> int: return 4 - func active_indicator_count() -> int: return _active_indicator_count @@ -84,18 +75,17 @@ func _build_instrument_cluster() -> void: "InstrumentClusterHousing", self, Vector3(-0.04, 0.105, 0.095), - Vector3(1.26, 0.255, 0.105), + Vector3(1.42, 0.31, 0.105), _materials.overhead_steel ) CockpitMeshFactory.add_box( "InstrumentClusterFoot", self, Vector3(-0.04, -0.025, 0.075), - Vector3(1.32, 0.040, 0.14), + Vector3(1.48, 0.040, 0.14), _materials.dark_steel ) - func _build_indicator_bank() -> void: var root := Node3D.new() root.name = "IndicatorBank" @@ -111,7 +101,7 @@ func _build_indicator_bank() -> void: CockpitMeshFactory.add_cylinder( "%sBezel" % captions[indicator_index], root, - Vector3(0.205, y, 0.156), + Vector3(0.68, y, 0.156), 0.016, 0.012, _materials.brass, @@ -122,7 +112,7 @@ func _build_indicator_bank() -> void: CockpitMeshFactory.add_cylinder( "%sLens" % captions[indicator_index], root, - Vector3(0.205, y, 0.165), + Vector3(0.68, y, 0.165), 0.009, 0.008, material, @@ -134,10 +124,9 @@ 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(0.235, y, 0.168) + label.position = Vector3(0.71, y, 0.168) root.add_child(label) - func _indicator_material(color: Color) -> StandardMaterial3D: var material := StandardMaterial3D.new() material.albedo_color = color * 0.12 @@ -148,7 +137,6 @@ func _indicator_material(color: Color) -> StandardMaterial3D: material.emission_energy_multiplier = 0.04 return material - func _update_indicator_bank(alert_level: DiveAlertLevel.Value) -> void: var active_index := 0 if alert_level >= DiveAlertLevel.Value.CRITICAL: @@ -162,22 +150,11 @@ func _update_indicator_bank(alert_level: DiveAlertLevel.Value) -> void: _active_indicator_count = 1 -func _add_cluster_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, - gauge_scale: float + gauge_scale: float, + face_material: Material ) -> Node3D: var gauge := Node3D.new() gauge.name = gauge_name @@ -206,42 +183,40 @@ func _add_gauge( _materials.brass, Vector3(90.0, 0.0, 0.0) ) - var bezel := MeshInstance3D.new() - var bezel_mesh := CylinderMesh.new() - bezel_mesh.top_radius = 0.095 - bezel_mesh.bottom_radius = 0.095 - bezel_mesh.height = 0.035 - bezel_mesh.radial_segments = 32 - bezel.mesh = bezel_mesh - bezel.material_override = _materials.brushed_steel - bezel.rotation_degrees.x = 90.0 - gauge.add_child(bezel) + _add_bezel(gauge, 0.108, 0.046, _materials.dark_steel) + _add_bezel(gauge, 0.099, 0.041, _materials.brushed_steel) var face := MeshInstance3D.new() - var face_mesh := CylinderMesh.new() - face_mesh.top_radius = 0.078 - face_mesh.bottom_radius = 0.078 - face_mesh.height = 0.039 - face_mesh.radial_segments = 32 - face.mesh = face_mesh - face.material_override = _materials.gauge_face - face.rotation_degrees.x = 90.0 - face.position.z = 0.003 + var printed_face := QuadMesh.new() + printed_face.size = Vector2(0.178, 0.178) + face.mesh = printed_face + face.position.z = 0.031 + face.material_override = face_material gauge.add_child(face) - _add_dial_ticks(gauge) - _add_scale_label(gauge, "0", Vector3(-0.052, -0.018, 0.028)) - _add_scale_label(gauge, "50", Vector3(0.0, 0.052, 0.028)) - _add_scale_label(gauge, "100", Vector3(0.052, -0.018, 0.028)) var needle_root := Node3D.new() needle_root.name = "Needle" - needle_root.position.z = 0.027 + needle_root.position.z = 0.038 gauge.add_child(needle_root) CockpitMeshFactory.add_box( "NeedleBlade", needle_root, - Vector3(0.0, 0.034, 0.0), - Vector3(0.008, 0.068, 0.006), + Vector3(0.0, 0.030, 0.0), + Vector3(0.006, 0.060, 0.006), + _materials.dark_steel + ) + CockpitMeshFactory.add_box( + "NeedleTip", + needle_root, + Vector3(0.0, 0.061, 0.001), + Vector3(0.004, 0.024, 0.006), _materials.needle_paint ) + CockpitMeshFactory.add_box( + "NeedleCounterweight", + needle_root, + Vector3(0.0, -0.014, -0.001), + Vector3(0.013, 0.030, 0.007), + _materials.dark_steel + ) CockpitMeshFactory.add_cylinder( "NeedleHub", needle_root, @@ -251,57 +226,38 @@ func _add_gauge( _materials.dark_steel, Vector3(90.0, 0.0, 0.0) ) - var label := Label3D.new() - label.name = "Caption" - label.text = caption - label.font_size = 12 - label.pixel_size = 0.0007 - label.modulate = Color(0.11, 0.12, 0.10) - label.position = Vector3(0.0, -0.044, 0.032) - gauge.add_child(label) var glass := MeshInstance3D.new() glass.name = "GaugeGlass" var glass_mesh := CylinderMesh.new() - glass_mesh.top_radius = 0.081 - glass_mesh.bottom_radius = 0.081 + glass_mesh.top_radius = 0.089 + glass_mesh.bottom_radius = 0.089 glass_mesh.height = 0.004 glass_mesh.radial_segments = 40 glass.mesh = glass_mesh glass.material_override = _materials.gauge_glass glass.rotation_degrees.x = 90.0 - glass.position.z = 0.036 + glass.position.z = 0.050 glass.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF gauge.add_child(glass) return needle_root -func _add_dial_ticks(gauge: Node3D) -> void: - for tick_index in 11: - var ratio := float(tick_index) / 10.0 - var angle_degrees := lerpf(125.0, -125.0, ratio) - var angle_radians := deg_to_rad(angle_degrees) - var direction := Vector2(-sin(angle_radians), cos(angle_radians)) - var is_major := tick_index % 5 == 0 - var tick_length := 0.015 if is_major else 0.009 - var tick := CockpitMeshFactory.add_box( - "MajorTick" if is_major else "MinorTick", - gauge, - Vector3(direction.x * 0.064, direction.y * 0.064, 0.029), - Vector3(0.004 if is_major else 0.0025, tick_length, 0.003), - _materials.dark_steel - ) - tick.rotation_degrees.z = angle_degrees - - -func _add_scale_label(gauge: Node3D, text: String, position: Vector3) -> void: - var label := Label3D.new() - label.name = "Scale%s" % text - label.text = text - label.font_size = 11 - label.pixel_size = 0.00052 - label.modulate = Color(0.09, 0.085, 0.065) - label.position = position - gauge.add_child(label) +func _add_bezel( + gauge: Node3D, + radius: float, + height: float, + material: Material +) -> void: + var bezel := MeshInstance3D.new() + var bezel_mesh := CylinderMesh.new() + bezel_mesh.top_radius = radius + bezel_mesh.bottom_radius = radius + bezel_mesh.height = height + bezel_mesh.radial_segments = 48 + bezel.mesh = bezel_mesh + bezel.material_override = material + bezel.rotation_degrees.x = 90.0 + gauge.add_child(bezel) func _set_needle_ratio(needle: Node3D, ratio: float) -> void: diff --git a/game/presentation/cockpit/cockpit_dashboard_detailing.gd b/game/presentation/cockpit/cockpit_dashboard_detailing.gd index 07359c9..36e7061 100644 --- a/game/presentation/cockpit/cockpit_dashboard_detailing.gd +++ b/game/presentation/cockpit/cockpit_dashboard_detailing.gd @@ -23,18 +23,25 @@ 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 hide_placeholder_indicators(cockpit: Node3D) -> void: +func hide_obsolete_elements(cockpit: Node3D) -> void: for indicator_name: String in ["CyanIndicator", "AmberIndicator", "RedIndicator"]: var indicator := cockpit.get_node_or_null(indicator_name) as MeshInstance3D if indicator != null: indicator.visible = false + for readout_name: String in [ + "ResourcesFrame", + "ResourcesScreen", + "ResourcesLabel", + "NavigationFrame", + "NavigationScreen", + "NavigationLabel", + "IntegrityFrame", + "IntegrityScreen", + "IntegrityLabel", + ]: + var readout := cockpit.get_node_or_null("Dashboard/%s" % readout_name) as Node3D + if readout != null: + readout.visible = false func _build_structural_frame() -> void: @@ -60,14 +67,6 @@ func _build_structural_frame() -> void: 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.075, 0.118), - Vector3(0.038, 0.16, 0.048), - _materials.brass - ) for screw_position: Vector2 in [ Vector2(-0.72, -0.18), Vector2(-0.72, 0.23), diff --git a/game/presentation/cockpit/cockpit_detailing.gd b/game/presentation/cockpit/cockpit_detailing.gd index c5a39a4..7374145 100644 --- a/game/presentation/cockpit/cockpit_detailing.gd +++ b/game/presentation/cockpit/cockpit_detailing.gd @@ -10,6 +10,15 @@ const CockpitDashboardDetailing = preload( ) const CockpitMaterialPalette = preload("res://game/presentation/cockpit/cockpit_material_palette.gd") const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh_factory.gd") +const CockpitSideEquipment = preload( + "res://game/presentation/cockpit/cockpit_side_equipment.gd" +) +const CockpitSonarConsole = preload( + "res://game/presentation/cockpit/cockpit_sonar_console.gd" +) +const CockpitPracticalLighting = preload( + "res://game/presentation/cockpit/cockpit_practical_lighting.gd" +) var _materials: CockpitMaterialPalette var _painted_steel: Material @@ -18,8 +27,9 @@ var _brass: Material var _copper: Material var _analog_gauges: CockpitAnalogGauges var _dashboard_detailing: CockpitDashboardDetailing -var _warm_practical: OmniLight3D -var _emergency_light: OmniLight3D +var _side_equipment: CockpitSideEquipment +var _sonar_console: CockpitSonarConsole +var _practical_lighting: CockpitPracticalLighting func _ready() -> void: @@ -27,10 +37,9 @@ func _ready() -> void: _apply_materials_to_existing_cockpit() _build_pressure_hull_ribs() _build_life_support_pipes() - _build_overhead_console() _build_panel_hardware() _build_dashboard_instrumentation() - _build_practical_lights() + _build_equipment_assemblies() func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: @@ -50,10 +59,7 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: snapshot.alerts.maximum ) var alert_level: DiveAlertLevel.Value = snapshot.alerts.maximum - _emergency_light.visible = alert_level >= DiveAlertLevel.Value.CRITICAL - _warm_practical.light_energy = ( - 0.28 if alert_level >= DiveAlertLevel.Value.CRITICAL else 0.72 - ) + _practical_lighting.present(alert_level) func pressure_hull_rib_count() -> int: @@ -74,7 +80,10 @@ func textures_are_loaded() -> bool: return ( _materials != null and _materials.overhead_steel.albedo_texture != null - and _materials.gauge_face.albedo_texture != null + and _materials.oxygen_gauge_face.albedo_texture != null + and _materials.battery_gauge_face.albedo_texture != null + and _materials.hull_gauge_face.albedo_texture != null + and _materials.depth_gauge_face.albedo_texture != null ) @@ -82,6 +91,24 @@ func dashboard_control_count() -> int: return 0 if _dashboard_detailing == null else _dashboard_detailing.control_count() +func side_equipment_count() -> int: + if _side_equipment == null: + return 0 + return _side_equipment.tank_count() + _side_equipment.junction_box_count() + + +func side_control_count() -> int: + return 0 if _side_equipment == null else _side_equipment.control_count() + + +func sonar_control_count() -> int: + return 0 if _sonar_console == null else _sonar_console.control_count() + + +func practical_light_count() -> int: + return 0 if _practical_lighting == null else _practical_lighting.practical_light_count() + + func gauges_are_dashboard_mounted() -> bool: return _analog_gauges != null and _analog_gauges.get_parent() == get_parent().get_node("Dashboard") @@ -219,58 +246,13 @@ func _build_life_support_pipes() -> void: 0.052, _copper ) - - CockpitMeshFactory.add_cylinder( - "OverheadCrossPipe", - root, - Vector3(0.0, 0.93, -1.12), - 0.031, - 1.82, - _copper, - Vector3(0.0, 0.0, 90.0) - ) - - -func _build_overhead_console() -> void: - var root := Node3D.new() - root.name = "OverheadConsole" - add_child(root) - CockpitMeshFactory.add_box( - "BreakerHousing", - root, - Vector3(0.0, 0.94, -1.06), - Vector3(0.30, 0.09, 0.12), - _materials.overhead_steel, - Vector3(-8.0, 0.0, 0.0) - ) - CockpitMeshFactory.add_box( - "WarmLightDiffuser", - root, - Vector3(0.0, 0.895, -0.985), - Vector3(0.13, 0.010, 0.026), - _materials.warm_emission, - Vector3(-8.0, 0.0, 0.0) - ) - for guard_index in 5: - var guard_x := -0.05 + guard_index * 0.025 - CockpitMeshFactory.add_box( - "LampGuard", + CockpitMeshFactory.add_cylinder_between( + "UpperBranchPipe", root, - Vector3(guard_x, 0.896, -0.968), - Vector3(0.005, 0.045, 0.008), - _dark_steel, - Vector3(-8.0, 0.0, 0.0) - ) - for switch_index in 5: - var x := -0.10 + switch_index * 0.05 - CockpitMeshFactory.add_cylinder( - "BreakerSwitch", - root, - Vector3(x, 0.965, -0.985), - 0.004, - 0.018, - _materials.rubber, - Vector3(90.0, 0.0, 0.0) + Vector3(x, 0.84, -1.12), + Vector3(side * 0.72, 0.77, -1.10), + 0.019, + _brass ) @@ -290,23 +272,6 @@ func _build_panel_hardware() -> void: Vector3(90.0, 0.0, 0.0) ) - for side: float in [-1.0, 1.0]: - CockpitMeshFactory.add_box( - "ServiceBox", - root, - Vector3(side * 0.89, 0.08, -1.10), - Vector3(0.18, 0.34, 0.19), - _painted_steel, - Vector3(0.0, side * -12.0, 0.0) - ) - var label := Label3D.new() - label.name = "ServicePlate" - label.text = "O₂" if side < 0.0 else "ELEC" - label.font_size = 16 - label.pixel_size = 0.0009 - label.modulate = Color(0.72, 0.69, 0.56) - label.position = Vector3(side * 0.89, 0.10, -0.99) - root.add_child(label) func _build_dashboard_instrumentation() -> void: @@ -315,36 +280,17 @@ func _build_dashboard_instrumentation() -> void: return _dashboard_detailing = CockpitDashboardDetailing.new(_materials) dashboard.add_child(_dashboard_detailing) - _dashboard_detailing.apply_screen_materials(dashboard) - _dashboard_detailing.hide_placeholder_indicators(get_parent()) + _dashboard_detailing.hide_obsolete_elements(get_parent()) _analog_gauges = CockpitAnalogGauges.new(_materials) dashboard.add_child(_analog_gauges) -func _build_practical_lights() -> void: - _warm_practical = OmniLight3D.new() - _warm_practical.name = "WarmPracticalLight" - _warm_practical.position = Vector3(0.0, 0.82, -0.50) - _warm_practical.light_color = Color(1.0, 0.52, 0.22) - _warm_practical.light_energy = 0.58 - _warm_practical.omni_range = 2.2 - _warm_practical.shadow_enabled = false - add_child(_warm_practical) - - var cool_fill := OmniLight3D.new() - cool_fill.name = "CoolRearFill" - cool_fill.position = Vector3(0.0, 0.42, 0.35) - cool_fill.light_color = Color(0.24, 0.46, 0.47) - cool_fill.light_energy = 0.48 - cool_fill.omni_range = 3.0 - cool_fill.shadow_enabled = false - add_child(cool_fill) - - _emergency_light = OmniLight3D.new() - _emergency_light.name = "EmergencyLight" - _emergency_light.position = Vector3(-0.58, 0.44, -0.83) - _emergency_light.light_color = Color(1.0, 0.04, 0.015) - _emergency_light.light_energy = 1.4 - _emergency_light.omni_range = 2.4 - _emergency_light.visible = false - add_child(_emergency_light) +func _build_equipment_assemblies() -> void: + _side_equipment = CockpitSideEquipment.new(_materials) + add_child(_side_equipment) + var sonar_monitor := get_parent().get_node_or_null("SonarMonitor") as Node3D + if sonar_monitor != null: + _sonar_console = CockpitSonarConsole.new(_materials) + sonar_monitor.add_child(_sonar_console) + _practical_lighting = CockpitPracticalLighting.new(_materials) + add_child(_practical_lighting) diff --git a/game/presentation/cockpit/cockpit_interior.tscn b/game/presentation/cockpit/cockpit_interior.tscn index 9f5e380..fcfe6e1 100644 --- a/game/presentation/cockpit/cockpit_interior.tscn +++ b/game/presentation/cockpit/cockpit_interior.tscn @@ -267,67 +267,67 @@ mesh = SubResource("UnitBoxMesh") material_override = SubResource("PaintedMaterial") [node name="ResourcesFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(-0.5, -0.075, 0.088) -scale = Vector3(0.45, 0.115, 0.025) +position = Vector3(-0.49, -0.115, 0.14) +scale = Vector3(0.34, 0.065, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="ResourcesScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(-0.5, -0.075, 0.102) -scale = Vector3(0.41, 0.085, 1) +position = Vector3(-0.49, -0.115, 0.155) +scale = Vector3(0.305, 0.044, 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.075, 0.108) -pixel_size = 0.00082 -font_size = 14 +position = Vector3(-0.49, -0.115, 0.163) +pixel_size = 0.00055 +font_size = 10 outline_size = 2 text = "BAT — kWh · O₂ — %\n— kW · — L/min" -modulate = Color(0.45, 0.89, 0.9, 1) +modulate = Color(0.76, 0.74, 0.6, 1) [node name="NavigationFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0, -0.075, 0.088) -scale = Vector3(0.4, 0.115, 0.025) +position = Vector3(0, -0.115, 0.14) +scale = Vector3(0.30, 0.065, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="NavigationScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0, -0.075, 0.102) -scale = Vector3(0.36, 0.085, 1) +position = Vector3(0, -0.115, 0.155) +scale = Vector3(0.265, 0.044, 1) mesh = SubResource("ScreenQuadMesh") material_override = SubResource("ScreenMaterial") [node name="NavigationLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true -position = Vector3(0, -0.075, 0.108) -pixel_size = 0.00082 -font_size = 14 +position = Vector3(0, -0.115, 0.163) +pixel_size = 0.00055 +font_size = 10 outline_size = 2 text = "PROF. — / — m\nVIT. — m/s" -modulate = Color(0.45, 0.89, 0.9, 1) +modulate = Color(0.76, 0.74, 0.6, 1) [node name="IntegrityFrame" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0.5, -0.075, 0.088) -scale = Vector3(0.45, 0.115, 0.025) +position = Vector3(0.49, -0.115, 0.14) +scale = Vector3(0.34, 0.065, 0.025) mesh = SubResource("UnitBoxMesh") material_override = SubResource("BrassMaterial") [node name="IntegrityScreen" type="MeshInstance3D" parent="Dashboard"] -position = Vector3(0.5, -0.075, 0.102) -scale = Vector3(0.41, 0.085, 1) +position = Vector3(0.49, -0.115, 0.155) +scale = Vector3(0.305, 0.044, 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.075, 0.108) -pixel_size = 0.00082 -font_size = 14 +position = Vector3(0.49, -0.115, 0.163) +pixel_size = 0.00055 +font_size = 10 outline_size = 2 text = "COQUE — % · — bar\nLIMITE — bar" -modulate = Color(0.45, 0.89, 0.9, 1) +modulate = Color(0.76, 0.74, 0.6, 1) [node name="ReturnLabel" type="Label3D" parent="Dashboard"] unique_name_in_owner = true @@ -388,24 +388,24 @@ mesh = SubResource("KnobMesh") material_override = SubResource("BrassMaterial") [node name="SonarMonitor" type="Node3D" parent="."] -position = Vector3(0.72, 0.4, -1.24) +position = Vector3(0.74, 0.42, -1.18) rotation_degrees = Vector3(-5, -22, -2) [node name="Housing" type="MeshInstance3D" parent="SonarMonitor"] -scale = Vector3(0.32, 0.24, 0.08) +scale = Vector3(0.5, 0.34, 0.1) mesh = SubResource("UnitBoxMesh") -material_override = SubResource("RubberMaterial") +material_override = SubResource("PaintedMaterial") [node name="SonarScreen" type="MeshInstance3D" parent="SonarMonitor"] unique_name_in_owner = true -position = Vector3(0, 0.025, 0.06) -scale = Vector3(0.265, 0.165, 1) +position = Vector3(-0.025, 0.012, 0.06) +scale = Vector3(0.36, 0.235, 1) mesh = SubResource("ScreenQuadMesh") material_override = SubResource("ScreenMaterial") [node name="SonarPlaceholder" type="Label3D" parent="SonarMonitor"] unique_name_in_owner = true -position = Vector3(0, 0.025, 0.068) +position = Vector3(-0.025, 0.012, 0.068) pixel_size = 0.0009 font_size = 16 outline_size = 2 diff --git a/game/presentation/cockpit/cockpit_material_palette.gd b/game/presentation/cockpit/cockpit_material_palette.gd index 1fc7300..b3907f4 100644 --- a/game/presentation/cockpit/cockpit_material_palette.gd +++ b/game/presentation/cockpit/cockpit_material_palette.gd @@ -4,8 +4,17 @@ extends RefCounted const NAVAL_STEEL_TEXTURE_PATH := ( "res://game/presentation/cockpit/assets/naval_painted_steel_albedo.png" ) -const GAUGE_FACE_TEXTURE_PATH := ( - "res://game/presentation/cockpit/assets/analog_gauge_face_albedo.png" +const OXYGEN_GAUGE_FACE_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/oxygen_gauge_face_albedo_v2.png" +) +const BATTERY_GAUGE_FACE_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/battery_gauge_face_albedo_v2.png" +) +const HULL_GAUGE_FACE_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/hull_gauge_face_albedo_v2.png" +) +const DEPTH_GAUGE_FACE_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/depth_gauge_face_albedo_v2.png" ) var painted_steel: Material @@ -15,11 +24,22 @@ var brushed_steel: StandardMaterial3D var brass: Material var copper: Material var rubber: Material -var gauge_face: StandardMaterial3D +var oxygen_gauge_face: StandardMaterial3D +var battery_gauge_face: StandardMaterial3D +var hull_gauge_face: StandardMaterial3D +var depth_gauge_face: StandardMaterial3D var gauge_glass: StandardMaterial3D var needle_paint: StandardMaterial3D var instrument_screen: ShaderMaterial var warm_emission: StandardMaterial3D +var safety_yellow: StandardMaterial3D +var signal_red: StandardMaterial3D +var indicator_green: StandardMaterial3D +var indicator_amber: StandardMaterial3D +var indicator_red: StandardMaterial3D +var sonar_grid: StandardMaterial3D +var sonar_trace: StandardMaterial3D +var warm_glass: StandardMaterial3D func _init() -> void: @@ -39,12 +59,12 @@ func _init() -> void: ) overhead_steel = _textured_material( _load_texture(NAVAL_STEEL_TEXTURE_PATH), - Color(0.64, 0.69, 0.68), + Color(0.88, 0.91, 0.88), 0.64, 0.56, Vector3(1.6, 1.6, 1.6) ) - brushed_steel = _plain_material(Color(0.27, 0.30, 0.29), 0.82, 0.30) + brushed_steel = _plain_material(Color(0.42, 0.44, 0.42), 0.82, 0.30) brass = _weathered_material( Color(0.28, 0.18, 0.045), Color(0.08, 0.16, 0.13), @@ -60,16 +80,22 @@ func _init() -> void: 0.12 ) rubber = _plain_material(Color(0.012, 0.018, 0.018), 0.0, 0.92) - gauge_face = _textured_material( - _load_texture(GAUGE_FACE_TEXTURE_PATH), - Color(0.78, 0.74, 0.64), - 0.02, - 0.72 - ) + oxygen_gauge_face = _dedicated_gauge_material(OXYGEN_GAUGE_FACE_TEXTURE_PATH) + battery_gauge_face = _dedicated_gauge_material(BATTERY_GAUGE_FACE_TEXTURE_PATH) + hull_gauge_face = _dedicated_gauge_material(HULL_GAUGE_FACE_TEXTURE_PATH) + depth_gauge_face = _dedicated_gauge_material(DEPTH_GAUGE_FACE_TEXTURE_PATH) 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) + safety_yellow = _plain_material(Color(0.48, 0.31, 0.035), 0.48, 0.53) + signal_red = _plain_material(Color(0.42, 0.025, 0.015), 0.38, 0.48) + indicator_green = _emissive_material(Color(0.05, 0.82, 0.24), 2.8) + indicator_amber = _emissive_material(Color(1.0, 0.43, 0.035), 2.8) + indicator_red = _emissive_material(Color(0.9, 0.045, 0.018), 2.8) + sonar_grid = _emissive_material(Color(0.02, 0.20, 0.22), 0.55) + sonar_trace = _emissive_material(Color(0.12, 0.78, 0.78), 1.8) + warm_glass = _warm_glass_material() func _weathered_material( @@ -161,6 +187,17 @@ func _load_texture(resource_path: String) -> ImageTexture: return ImageTexture.create_from_image(image) +func _dedicated_gauge_material(resource_path: String) -> StandardMaterial3D: + var material := _textured_material( + _load_texture(resource_path), + Color(0.94, 0.92, 0.86), + 0.02, + 0.66 + ) + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + return material + + func _glass_material() -> StandardMaterial3D: var material := StandardMaterial3D.new() material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA @@ -203,3 +240,14 @@ func _emissive_material(color: Color, energy: float) -> StandardMaterial3D: material.emission = color material.emission_energy_multiplier = energy return material + + +func _warm_glass_material() -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + material.albedo_color = Color(1.0, 0.48, 0.12, 0.72) + material.roughness = 0.18 + material.emission_enabled = true + material.emission = Color(1.0, 0.28, 0.045) + material.emission_energy_multiplier = 2.6 + return material diff --git a/game/presentation/cockpit/cockpit_mesh_factory.gd b/game/presentation/cockpit/cockpit_mesh_factory.gd index dea27c4..0e204e1 100644 --- a/game/presentation/cockpit/cockpit_mesh_factory.gd +++ b/game/presentation/cockpit/cockpit_mesh_factory.gd @@ -46,6 +46,28 @@ static func add_cylinder( return instance +static func add_cylinder_between( + node_name: String, + parent: Node3D, + start: Vector3, + end: Vector3, + radius: float, + material: Material +) -> MeshInstance3D: + var direction := end - start + var instance := add_cylinder( + node_name, + parent, + (start + end) * 0.5, + radius, + direction.length(), + material + ) + if not direction.is_zero_approx(): + instance.quaternion = Quaternion(Vector3.UP, direction.normalized()) + return instance + + static func add_sphere( node_name: String, parent: Node3D, diff --git a/game/presentation/cockpit/cockpit_practical_lighting.gd b/game/presentation/cockpit/cockpit_practical_lighting.gd new file mode 100644 index 0000000..77814cd --- /dev/null +++ b/game/presentation/cockpit/cockpit_practical_lighting.gd @@ -0,0 +1,102 @@ +class_name CockpitPracticalLighting +extends Node3D + +const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd") +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 _warm_lights: Array[OmniLight3D] = [] +var _emergency_light: OmniLight3D + + +func _init(materials: CockpitMaterialPalette) -> void: + _materials = materials + name = "PracticalLighting" + _add_caged_lamp(-0.76, -8.0) + _add_caged_lamp(0.76, 8.0) + _add_fill_light() + _add_emergency_light() + + +func present(alert_level: DiveAlertLevel.Value) -> void: + _emergency_light.visible = alert_level >= DiveAlertLevel.Value.CRITICAL + for light: OmniLight3D in _warm_lights: + light.light_energy = 0.22 if alert_level >= DiveAlertLevel.Value.CRITICAL else 0.48 + + +func practical_light_count() -> int: + return _warm_lights.size() + + +func _add_caged_lamp(x: float, roll_degrees: float) -> void: + var root := Node3D.new() + root.name = "CagedLamp" + root.position = Vector3(x, 0.88, -1.16) + root.rotation_degrees.z = roll_degrees + add_child(root) + CockpitMeshFactory.add_box( + "LampHousing", + root, + Vector3(0.0, 0.0, -0.025), + Vector3(0.14, 0.11, 0.055), + _materials.dark_steel + ) + CockpitMeshFactory.add_sphere( + "WarmBulb", + root, + Vector3(0.0, 0.0, 0.04), + 0.038, + _materials.warm_glass + ) + for x_offset: float in [-0.048, -0.024, 0.0, 0.024, 0.048]: + CockpitMeshFactory.add_box( + "LampGuard", + root, + Vector3(x_offset, 0.0, 0.095), + Vector3(0.007, 0.10, 0.007), + _materials.brushed_steel + ) + for y_offset: float in [-0.04, 0.04]: + CockpitMeshFactory.add_box( + "LampGuardRail", + root, + Vector3(0.0, y_offset, 0.095), + Vector3(0.115, 0.007, 0.007), + _materials.brushed_steel + ) + var light := OmniLight3D.new() + light.name = "WarmPracticalLight" + light.position = Vector3(0.0, -0.03, 0.16) + light.light_color = Color(1.0, 0.55, 0.26) + light.light_energy = 0.48 + light.omni_range = 1.75 + light.shadow_enabled = false + root.add_child(light) + _warm_lights.append(light) + + +func _add_fill_light() -> void: + var cool_fill := OmniLight3D.new() + cool_fill.name = "CoolRearFill" + cool_fill.position = Vector3(0.0, 0.42, 0.35) + cool_fill.light_color = Color(0.24, 0.46, 0.47) + cool_fill.light_energy = 0.36 + cool_fill.omni_range = 3.0 + cool_fill.shadow_enabled = false + add_child(cool_fill) + + +func _add_emergency_light() -> void: + _emergency_light = OmniLight3D.new() + _emergency_light.name = "EmergencyLight" + _emergency_light.position = Vector3(-0.58, 0.44, -0.83) + _emergency_light.light_color = Color(1.0, 0.04, 0.015) + _emergency_light.light_energy = 1.4 + _emergency_light.omni_range = 2.4 + _emergency_light.visible = false + add_child(_emergency_light) diff --git a/game/presentation/cockpit/cockpit_pressure_bulkhead.gd b/game/presentation/cockpit/cockpit_pressure_bulkhead.gd index 06b4ac6..a712b7d 100644 --- a/game/presentation/cockpit/cockpit_pressure_bulkhead.gd +++ b/game/presentation/cockpit/cockpit_pressure_bulkhead.gd @@ -26,6 +26,7 @@ func _ready() -> void: _build_curved_panels() _build_outer_rib() _build_fasteners() + _build_clamp_plates() func panel_count() -> int: @@ -53,7 +54,7 @@ func _style_existing_frame() -> void: 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 + (child as MeshInstance3D).material_override = _materials.brass func _build_curved_panels() -> void: @@ -155,12 +156,37 @@ func _build_outer_rib() -> void: func _build_fasteners() -> void: for fastener_index in FASTENER_COUNT: var angle := (fastener_index + 0.5) * TAU / float(FASTENER_COUNT) + var fastener_position := Vector3( + cos(angle) * 0.96, + sin(angle) * 0.96, + 0.12 + ) CockpitMeshFactory.add_cylinder( "OuterFastener", self, - Vector3(cos(angle) * 0.96, sin(angle) * 0.96, 0.105), - 0.014, + fastener_position, + 0.020, 0.018, _materials.brass, Vector3(90.0, 0.0, 0.0) ) + CockpitMeshFactory.add_sphere( + "FastenerCap", + self, + fastener_position + Vector3(0.0, 0.0, 0.012), + 0.017, + _materials.brushed_steel + ) + + +func _build_clamp_plates() -> void: + for plate_index in 12: + var angle := (plate_index + 0.5) * TAU / 12.0 + var plate := CockpitMeshFactory.add_box( + "PressureClamp", + self, + Vector3(cos(angle) * 0.88, sin(angle) * 0.88, 0.105), + Vector3(0.11, 0.055, 0.028), + _materials.brushed_steel + ) + plate.rotation_degrees.z = rad_to_deg(angle) + 90.0 diff --git a/game/presentation/cockpit/cockpit_side_equipment.gd b/game/presentation/cockpit/cockpit_side_equipment.gd new file mode 100644 index 0000000..90b9bf7 --- /dev/null +++ b/game/presentation/cockpit/cockpit_side_equipment.gd @@ -0,0 +1,320 @@ +class_name CockpitSideEquipment +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 _tank_count := 0 +var _junction_box_count := 0 +var _control_count := 0 + + +func _init(materials: CockpitMaterialPalette) -> void: + _materials = materials + name = "SideEquipment" + _build_left_life_support_bank() + _build_right_electrical_bank() + _build_cable_looms() + _build_control_rails() + + +func tank_count() -> int: + return _tank_count + + +func junction_box_count() -> int: + return _junction_box_count + + +func control_count() -> int: + return _control_count + + +func _build_left_life_support_bank() -> void: + var root := Node3D.new() + root.name = "LifeSupportBank" + add_child(root) + for tank_data: Vector3 in [ + Vector3(-0.78, 0.55, -1.17), + Vector3(-0.68, 0.47, -1.19), + ]: + _add_tank(root, tank_data) + + _add_service_box(root, Vector3(-0.61, 0.42, -1.14), "O₂", -10.0) + _add_indicator_column(root, Vector3(-0.55, 0.39, -1.055)) + _add_valve_wheel(root, Vector3(-0.66, 0.12, -1.08)) + + CockpitMeshFactory.add_cylinder_between( + "ManifoldPipe", + root, + Vector3(-0.78, 0.24, -1.13), + Vector3(-0.67, 0.09, -1.11), + 0.018, + _materials.copper + ) + CockpitMeshFactory.add_cylinder_between( + "ManifoldReturn", + root, + Vector3(-0.68, 0.25, -1.15), + Vector3(-0.59, 0.02, -1.10), + 0.014, + _materials.brass + ) + + +func _add_tank(root: Node3D, tank_position: Vector3) -> void: + CockpitMeshFactory.add_cylinder( + "OxygenTank", + root, + tank_position, + 0.055, + 0.43, + _materials.safety_yellow + ) + CockpitMeshFactory.add_sphere( + "TankTop", + root, + tank_position + Vector3(0.0, 0.215, 0.0), + 0.055, + _materials.safety_yellow + ) + CockpitMeshFactory.add_sphere( + "TankBottom", + root, + tank_position - Vector3(0.0, 0.215, 0.0), + 0.055, + _materials.safety_yellow + ) + for offset_y: float in [-0.13, 0.13]: + var strap := MeshInstance3D.new() + strap.name = "TankStrap" + var torus := TorusMesh.new() + torus.inner_radius = 0.054 + torus.outer_radius = 0.066 + torus.rings = 20 + torus.ring_segments = 6 + strap.mesh = torus + strap.material_override = _materials.dark_steel + strap.position = tank_position + Vector3(0.0, offset_y, 0.0) + root.add_child(strap) + CockpitMeshFactory.add_cylinder( + "TankValve", + root, + tank_position + Vector3(0.0, 0.29, 0.0), + 0.017, + 0.075, + _materials.brass + ) + _tank_count += 1 + + +func _build_right_electrical_bank() -> void: + var root := Node3D.new() + root.name = "ElectricalBank" + add_child(root) + _add_service_box(root, Vector3(0.72, 0.65, -1.17), "ELEC", 11.0) + _add_service_box(root, Vector3(0.73, 0.36, -1.16), "POMPES", 8.0) + _add_vent(root, Vector3(0.705, 0.69, -1.095)) + _add_vent(root, Vector3(0.715, 0.40, -1.085)) + _add_knob(root, Vector3(0.66, 0.55, -1.08)) + _add_knob(root, Vector3(0.68, 0.25, -1.07)) + _add_indicator_column(root, Vector3(0.795, 0.49, -1.075)) + + for pipe_x: float in [0.64, 0.82]: + CockpitMeshFactory.add_cylinder( + "ElectricalConduit", + root, + Vector3(pipe_x, 0.70, -1.20), + 0.017, + 0.58, + _materials.copper + ) + + +func _add_service_box( + root: Node3D, + box_position: Vector3, + caption: String, + yaw_degrees: float +) -> void: + CockpitMeshFactory.add_box( + "JunctionBox", + root, + box_position, + Vector3(0.17, 0.23, 0.12), + _materials.overhead_steel, + Vector3(0.0, yaw_degrees, 0.0) + ) + var label := Label3D.new() + label.name = "%sPlate" % caption.to_pascal_case() + label.text = caption + label.font_size = 12 + label.pixel_size = 0.00065 + label.modulate = Color(0.13, 0.16, 0.14) + label.position = box_position + Vector3(0.0, 0.075, 0.067) + root.add_child(label) + for corner: Vector2 in [ + Vector2(-0.065, -0.085), + Vector2(-0.065, 0.085), + Vector2(0.065, -0.085), + Vector2(0.065, 0.085), + ]: + CockpitMeshFactory.add_cylinder( + "BoxFastener", + root, + box_position + Vector3(corner.x, corner.y, 0.065), + 0.008, + 0.01, + _materials.dark_steel, + Vector3(90.0, 0.0, 0.0) + ) + _junction_box_count += 1 + + +func _add_vent(root: Node3D, center: Vector3) -> void: + for row in 5: + CockpitMeshFactory.add_box( + "VentSlit", + root, + center + Vector3(0.0, (row - 2) * 0.018, 0.0), + Vector3(0.085, 0.007, 0.009), + _materials.dark_steel + ) + + +func _add_knob(root: Node3D, knob_position: Vector3) -> void: + CockpitMeshFactory.add_cylinder( + "ControlKnob", + root, + knob_position, + 0.027, + 0.026, + _materials.rubber, + Vector3(90.0, 0.0, 0.0) + ) + _control_count += 1 + + +func _add_indicator_column(root: Node3D, center: Vector3) -> void: + var indicator_materials: Array[StandardMaterial3D] = [ + _materials.indicator_green, + _materials.indicator_amber, + _materials.indicator_red, + ] + for index in 3: + CockpitMeshFactory.add_box( + "PilotLightBezel", + root, + center + Vector3(0.0, (1 - index) * 0.055, -0.004), + Vector3(0.038, 0.038, 0.018), + _materials.dark_steel + ) + CockpitMeshFactory.add_sphere( + "PilotLight", + root, + center + Vector3(0.0, (1 - index) * 0.055, 0.010), + 0.013, + indicator_materials[index] + ) + _control_count += 1 + + +func _add_valve_wheel(root: Node3D, wheel_position: Vector3) -> void: + var wheel := MeshInstance3D.new() + wheel.name = "ValveWheel" + var torus := TorusMesh.new() + torus.inner_radius = 0.034 + torus.outer_radius = 0.046 + torus.rings = 20 + torus.ring_segments = 7 + wheel.mesh = torus + wheel.material_override = _materials.signal_red + wheel.position = wheel_position + wheel.rotation_degrees.x = 90.0 + root.add_child(wheel) + for angle_degrees: float in [0.0, 45.0, 90.0, 135.0]: + CockpitMeshFactory.add_box( + "ValveSpoke", + root, + wheel_position + Vector3(0.0, 0.0, 0.002), + Vector3(0.075, 0.007, 0.008), + _materials.signal_red, + Vector3(0.0, 0.0, angle_degrees) + ) + _control_count += 1 + + +func _build_cable_looms() -> void: + var root := Node3D.new() + root.name = "CableLooms" + add_child(root) + var left_points: Array[Vector3] = [ + Vector3(-0.82, 0.93, -1.15), + Vector3(-0.72, 0.87, -1.16), + Vector3(-0.64, 0.73, -1.14), + Vector3(-0.61, 0.58, -1.12), + ] + var right_points: Array[Vector3] = [ + Vector3(0.84, 0.91, -1.15), + Vector3(0.75, 0.86, -1.16), + Vector3(0.68, 0.73, -1.14), + Vector3(0.65, 0.59, -1.12), + ] + _add_cable_path(root, left_points) + _add_cable_path(root, right_points) + + +func _add_cable_path(root: Node3D, points: Array[Vector3]) -> void: + for point_index in points.size() - 1: + CockpitMeshFactory.add_cylinder_between( + "ArmouredCable", + root, + points[point_index], + points[point_index + 1], + 0.012, + _materials.rubber + ) + + +func _build_control_rails() -> void: + var root := Node3D.new() + root.name = "ControlRails" + add_child(root) + CockpitMeshFactory.add_cylinder_between( + "LowerSteelRail", + root, + Vector3(-0.82, -0.34, -1.12), + Vector3(0.82, -0.34, -1.12), + 0.026, + _materials.brushed_steel + ) + CockpitMeshFactory.add_cylinder_between( + "LeftGrip", + root, + Vector3(-0.58, -0.34, -1.11), + Vector3(-0.18, -0.34, -1.11), + 0.034, + _materials.rubber + ) + CockpitMeshFactory.add_cylinder_between( + "RightGrip", + root, + Vector3(0.18, -0.34, -1.11), + Vector3(0.58, -0.34, -1.11), + 0.034, + _materials.rubber + ) + for x: float in [-0.82, 0.82]: + CockpitMeshFactory.add_cylinder_between( + "RailSupport", + root, + Vector3(x, -0.34, -1.12), + Vector3(x, -0.20, -1.08), + 0.022, + _materials.brushed_steel + ) diff --git a/game/presentation/cockpit/cockpit_sonar_console.gd b/game/presentation/cockpit/cockpit_sonar_console.gd new file mode 100644 index 0000000..39a6b57 --- /dev/null +++ b/game/presentation/cockpit/cockpit_sonar_console.gd @@ -0,0 +1,163 @@ +class_name CockpitSonarConsole +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 = "SonarConsoleDetailing" + _build_bezel() + _build_graticule() + _build_control_strip() + _build_mounting_hardware() + + +func control_count() -> int: + return _control_count + + +func _build_bezel() -> void: + CockpitMeshFactory.add_box( + "UpperBezel", + self, + Vector3(-0.025, 0.151, 0.073), + Vector3(0.41, 0.034, 0.035), + _materials.brushed_steel + ) + CockpitMeshFactory.add_box( + "LowerBezel", + self, + Vector3(-0.025, -0.126, 0.073), + Vector3(0.41, 0.034, 0.035), + _materials.brushed_steel + ) + for x: float in [-0.225, 0.175]: + CockpitMeshFactory.add_box( + "SideBezel", + self, + Vector3(x, 0.012, 0.073), + Vector3(0.035, 0.31, 0.035), + _materials.brushed_steel + ) + var label := Label3D.new() + label.name = "SonarPlate" + label.text = "SONAR · BATHYMÉTRIE" + label.font_size = 12 + label.pixel_size = 0.00062 + label.modulate = Color(0.7, 0.73, 0.64) + label.position = Vector3(-0.04, 0.156, 0.097) + add_child(label) + + +func _build_control_strip() -> void: + CockpitMeshFactory.add_box( + "ControlStrip", + self, + Vector3(0.235, 0.012, 0.072), + Vector3(0.075, 0.31, 0.04), + _materials.dark_steel + ) + var lamps: Array[StandardMaterial3D] = [ + _materials.indicator_amber, + _materials.indicator_amber, + _materials.indicator_green, + ] + for index in 3: + var y := 0.095 - index * 0.073 + CockpitMeshFactory.add_box( + "SonarLampBezel", + self, + Vector3(0.235, y, 0.101), + Vector3(0.044, 0.044, 0.018), + _materials.brass + ) + CockpitMeshFactory.add_sphere( + "SonarLamp", + self, + Vector3(0.235, y, 0.116), + 0.013, + lamps[index] + ) + _control_count += 1 + CockpitMeshFactory.add_cylinder( + "SonarGainKnob", + self, + Vector3(0.235, -0.102, 0.108), + 0.024, + 0.026, + _materials.rubber, + Vector3(90.0, 0.0, 0.0) + ) + _control_count += 1 + + +func _build_graticule() -> void: + for x: float in [-0.145, -0.085, -0.025, 0.035, 0.095]: + CockpitMeshFactory.add_box( + "SonarGridVertical", + self, + Vector3(x, 0.012, 0.076), + Vector3(0.0015, 0.215, 0.002), + _materials.sonar_grid + ) + for y: float in [-0.075, -0.025, 0.025, 0.075]: + CockpitMeshFactory.add_box( + "SonarGridHorizontal", + self, + Vector3(-0.025, y, 0.076), + Vector3(0.33, 0.0015, 0.002), + _materials.sonar_grid + ) + var trace_points: Array[Vector3] = [ + Vector3(-0.185, -0.035, 0.080), + Vector3(-0.135, -0.020, 0.080), + Vector3(-0.085, -0.055, 0.080), + Vector3(-0.030, -0.005, 0.080), + Vector3(0.025, -0.030, 0.080), + Vector3(0.075, 0.030, 0.080), + Vector3(0.125, 0.075, 0.080), + ] + for point_index in trace_points.size() - 1: + CockpitMeshFactory.add_cylinder_between( + "SonarReferenceTrace", + self, + trace_points[point_index], + trace_points[point_index + 1], + 0.0025, + _materials.sonar_trace + ) + + +func _build_mounting_hardware() -> void: + for corner: Vector2 in [ + Vector2(-0.205, -0.105), + Vector2(-0.205, 0.13), + Vector2(0.195, -0.105), + Vector2(0.195, 0.13), + ]: + CockpitMeshFactory.add_cylinder( + "SonarFastener", + self, + Vector3(corner.x, corner.y, 0.103), + 0.008, + 0.012, + _materials.brass, + Vector3(90.0, 0.0, 0.0) + ) + CockpitMeshFactory.add_cylinder_between( + "LeftSonarHandle", + self, + Vector3(-0.255, -0.09, 0.06), + Vector3(-0.255, 0.11, 0.06), + 0.013, + _materials.brass + ) diff --git a/tests/integration/run_vertical_slice_foundation.gd b/tests/integration/run_vertical_slice_foundation.gd index b4838b4..d47bd11 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 := 65 +const TEST_COUNT := 69 var _failures: Array[String] = [] @@ -128,9 +128,13 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: _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(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.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.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")