Build validated industrial submarine cockpit
This commit is contained in:
1 parent
dffe790926
commit
fdebbafeae
19 files changed
+906
-277
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user