Build validated industrial submarine cockpit

This commit is contained in:
chindit committed 2026-10-04 10:39:56 +02:00
1 parent dffe790926
commit fdebbafeae
19 files changed
+906 -277

No files matched your search

+6 -4
View File
@@ -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.
+26
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -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:
@@ -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),
+52 -106
View File
@@ -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)
+30 -30
View File
@@ -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
@@ -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
@@ -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,
@@ -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)
@@ -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
@@ -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
)
@@ -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
)
@@ -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")