Improve production cockpit realism
This commit is contained in:
1 parent
7613daee84
commit
5455ae7928
9 files changed
+668
-21
No files matched your search
@@ -19,7 +19,8 @@ production sous `game/` n'en dépend.
|
||||
- Acteur sous-marin de production derrière des contrats de commande et de
|
||||
télémétrie typés.
|
||||
- Habitacle 3D solidaire de la coque : hublot occultant, consoles, commandes,
|
||||
éclairage et emplacement réservé au sonar.
|
||||
éclairage, structure de coque renforcée, conduites, jauges analogiques et
|
||||
emplacement réservé au sonar.
|
||||
- Instruments physiques à valeurs réelles ; le HUD reste réservé aux alertes,
|
||||
à l'éjection et aux états terminaux.
|
||||
- Pause qui suspend les ticks, confirmations de sortie et premier remappage avec
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Statut
|
||||
|
||||
- Date : 10 août 2026.
|
||||
- Dernière révision d'implémentation : 2 octobre 2026.
|
||||
- Dernière révision d'implémentation : 3 octobre 2026.
|
||||
- État : cadrage approuvé, tranches 0, 1 et 1.5 implémentées ; tranche 2 en cours.
|
||||
- Jalon : verticale jouable, après validation des POC A et B.
|
||||
- Durée directrice du plan général : quatre à six mois.
|
||||
@@ -801,6 +801,12 @@ Sortie : parcours de pilotage court avec ressources et retour immédiat lisible.
|
||||
1280 × 720. Cette tranche corrige le graybox initial, qui ne contenait qu'une
|
||||
caméra, un cercle Canvas et des textes superposés.
|
||||
|
||||
Passe de réalisme du 3 octobre 2026 : l'habitacle reçoit des membrures de coque,
|
||||
des conduites de support-vie, des boîtiers de service, des fixations, deux
|
||||
jauges analogiques fonctionnelles, des matériaux métalliques patinés et un
|
||||
éclairage pratique chaud/froid. La lisibilité du hublot et des instruments a
|
||||
été revue nativement en Forward+ à 1280 × 720.
|
||||
|
||||
- Construire une coque intérieure 3D fixe par rapport au véhicule, indépendante
|
||||
du regard de la caméra.
|
||||
- Remplacer le cercle Canvas par un hublot 3D occultant avec cadre, joint, verre,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
class_name CockpitAnalogGauges
|
||||
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 _battery_needle: Node3D
|
||||
var _pressure_needle: Node3D
|
||||
|
||||
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "AnalogGauges"
|
||||
_battery_needle = _add_gauge("BatteryGauge", Vector3(-0.34, 0.48, -1.14), "V")
|
||||
_pressure_needle = _add_gauge("PressureGauge", Vector3(0.34, 0.48, -1.14), "BAR")
|
||||
|
||||
|
||||
func present(battery_ratio: float, pressure_ratio: float) -> void:
|
||||
_set_needle_ratio(_battery_needle, battery_ratio)
|
||||
_set_needle_ratio(_pressure_needle, pressure_ratio)
|
||||
|
||||
|
||||
func battery_needle_degrees() -> float:
|
||||
return 0.0 if _battery_needle == null else _battery_needle.rotation_degrees.z
|
||||
|
||||
|
||||
func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3D:
|
||||
var gauge := Node3D.new()
|
||||
gauge.name = gauge_name
|
||||
gauge.position = position
|
||||
gauge.scale = Vector3(0.78, 0.78, 0.78)
|
||||
add_child(gauge)
|
||||
CockpitMeshFactory.add_box(
|
||||
"MountingPlate",
|
||||
gauge,
|
||||
Vector3(0.0, 0.0, -0.025),
|
||||
Vector3(0.225, 0.225, 0.055),
|
||||
_materials.dark_steel
|
||||
)
|
||||
for screw_position: Vector2 in [
|
||||
Vector2(-0.086, -0.086),
|
||||
Vector2(0.086, -0.086),
|
||||
Vector2(-0.086, 0.086),
|
||||
Vector2(0.086, 0.086),
|
||||
]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"MountingScrew",
|
||||
gauge,
|
||||
Vector3(screw_position.x, screw_position.y, 0.008),
|
||||
0.009,
|
||||
0.012,
|
||||
_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.brass
|
||||
bezel.rotation_degrees.x = 90.0
|
||||
gauge.add_child(bezel)
|
||||
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.ivory
|
||||
face.rotation_degrees.x = 90.0
|
||||
face.position.z = 0.003
|
||||
gauge.add_child(face)
|
||||
var needle_root := Node3D.new()
|
||||
needle_root.name = "Needle"
|
||||
needle_root.position.z = 0.027
|
||||
gauge.add_child(needle_root)
|
||||
CockpitMeshFactory.add_box(
|
||||
"NeedleBlade",
|
||||
needle_root,
|
||||
Vector3(0.0, 0.033, 0.0),
|
||||
Vector3(0.012, 0.064, 0.008),
|
||||
_materials.red_emission
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"NeedleHub",
|
||||
needle_root,
|
||||
Vector3.ZERO,
|
||||
0.018,
|
||||
0.012,
|
||||
_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.045, 0.032)
|
||||
gauge.add_child(label)
|
||||
return needle_root
|
||||
|
||||
|
||||
func _set_needle_ratio(needle: Node3D, ratio: float) -> void:
|
||||
if needle != null:
|
||||
needle.rotation_degrees.z = lerpf(125.0, -125.0, clampf(ratio, 0.0, 1.0))
|
||||
@@ -0,0 +1,311 @@
|
||||
class_name CockpitDetailing
|
||||
extends Node3D
|
||||
|
||||
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
|
||||
const CockpitAnalogGauges = preload("res://game/presentation/cockpit/cockpit_analog_gauges.gd")
|
||||
const 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 _painted_steel: Material
|
||||
var _dark_steel: Material
|
||||
var _brass: Material
|
||||
var _copper: Material
|
||||
var _rubber: Material
|
||||
var _warm_emission: StandardMaterial3D
|
||||
var _analog_gauges: CockpitAnalogGauges
|
||||
var _warm_practical: OmniLight3D
|
||||
var _emergency_light: OmniLight3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_create_materials()
|
||||
_apply_materials_to_existing_cockpit()
|
||||
_build_pressure_hull_ribs()
|
||||
_build_life_support_pipes()
|
||||
_build_overhead_console()
|
||||
_build_panel_hardware()
|
||||
_build_analog_gauges()
|
||||
_build_practical_lights()
|
||||
|
||||
|
||||
func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void:
|
||||
if update == null or update.snapshot == null or telemetry == null:
|
||||
return
|
||||
var snapshot = update.snapshot
|
||||
_analog_gauges.present(
|
||||
snapshot.battery.ratio,
|
||||
clampf(
|
||||
snapshot.hull.exterior_pressure_bar
|
||||
/ maxf(snapshot.hull.certified_pressure_bar, 0.001),
|
||||
0.0,
|
||||
1.0
|
||||
)
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
func pressure_hull_rib_count() -> int:
|
||||
var ribs := get_node_or_null("PressureHullRibs")
|
||||
return 0 if ribs == null else ribs.get_child_count()
|
||||
|
||||
|
||||
func pipe_segment_count() -> int:
|
||||
var pipes := get_node_or_null("LifeSupportPipes")
|
||||
return 0 if pipes == null else pipes.get_child_count()
|
||||
|
||||
|
||||
func battery_needle_degrees() -> float:
|
||||
return 0.0 if _analog_gauges == null else _analog_gauges.battery_needle_degrees()
|
||||
|
||||
|
||||
func _create_materials() -> void:
|
||||
_materials = CockpitMaterialPalette.new()
|
||||
_painted_steel = _materials.painted_steel
|
||||
_dark_steel = _materials.dark_steel
|
||||
_brass = _materials.brass
|
||||
_copper = _materials.copper
|
||||
_rubber = _materials.rubber
|
||||
_warm_emission = _materials.warm_emission
|
||||
|
||||
|
||||
func _apply_materials_to_existing_cockpit() -> void:
|
||||
_apply_material_to_paths(
|
||||
_painted_steel,
|
||||
[
|
||||
"CabinShell/LeftWall",
|
||||
"CabinShell/RightWall",
|
||||
"Dashboard/MainPanel",
|
||||
"LeftConsole",
|
||||
"RightConsole",
|
||||
]
|
||||
)
|
||||
_apply_material_to_paths(
|
||||
_dark_steel,
|
||||
[
|
||||
"CabinShell/Ceiling",
|
||||
"CabinShell/RearBulkhead",
|
||||
"PortholeAssembly/LeftBulkhead",
|
||||
"PortholeAssembly/RightBulkhead",
|
||||
"PortholeAssembly/TopBulkhead",
|
||||
"PortholeAssembly/BottomBulkhead",
|
||||
"ThrottleLever",
|
||||
"VerticalLever",
|
||||
]
|
||||
)
|
||||
_apply_material_to_paths(
|
||||
_brass,
|
||||
[
|
||||
"PortholeAssembly/WindowFrame",
|
||||
"Dashboard/ResourcesFrame",
|
||||
"Dashboard/NavigationFrame",
|
||||
"Dashboard/IntegrityFrame",
|
||||
"ThrottleKnob",
|
||||
"VerticalKnob",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _apply_material_to_paths(material: Material, paths: Array[String]) -> void:
|
||||
var cockpit := get_parent()
|
||||
for path: String in paths:
|
||||
var mesh := cockpit.get_node_or_null(path) as MeshInstance3D
|
||||
if mesh != null:
|
||||
mesh.material_override = material
|
||||
|
||||
|
||||
func _build_pressure_hull_ribs() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "PressureHullRibs"
|
||||
add_child(root)
|
||||
for rib_index in 2:
|
||||
var rib := MeshInstance3D.new()
|
||||
rib.name = "CircularRib%d" % (rib_index + 1)
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 1.00
|
||||
torus.outer_radius = 1.075
|
||||
torus.rings = 48
|
||||
torus.ring_segments = 10
|
||||
rib.mesh = torus
|
||||
rib.material_override = _dark_steel
|
||||
rib.position = Vector3(0.0, 0.22, -0.90 + rib_index * 1.35)
|
||||
rib.rotation_degrees.x = 90.0
|
||||
root.add_child(rib)
|
||||
|
||||
for side: float in [-1.0, 1.0]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"AngledShoulder",
|
||||
root,
|
||||
Vector3(side * 0.88, 0.84, -0.36),
|
||||
Vector3(0.11, 0.62, 1.52),
|
||||
_painted_steel,
|
||||
Vector3(0.0, 0.0, side * -18.0)
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"LowerStringer",
|
||||
root,
|
||||
Vector3(side * 0.91, -0.38, -0.18),
|
||||
Vector3(0.10, 0.12, 1.70),
|
||||
_dark_steel
|
||||
)
|
||||
|
||||
|
||||
func _build_life_support_pipes() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "LifeSupportPipes"
|
||||
add_child(root)
|
||||
for side: float in [-1.0, 1.0]:
|
||||
var x: float = side * 0.83
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"VerticalPipe",
|
||||
root,
|
||||
Vector3(x, 0.33, -1.12),
|
||||
0.027,
|
||||
1.18,
|
||||
_copper
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"ReturnPipe",
|
||||
root,
|
||||
Vector3(side * 0.73, 0.47, -1.09),
|
||||
0.019,
|
||||
0.92,
|
||||
_brass
|
||||
)
|
||||
for clamp_y: float in [-0.04, 0.35, 0.72]:
|
||||
var clamp := MeshInstance3D.new()
|
||||
clamp.name = "PipeClamp"
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 0.027
|
||||
torus.outer_radius = 0.041
|
||||
torus.rings = 16
|
||||
torus.ring_segments = 6
|
||||
clamp.mesh = torus
|
||||
clamp.material_override = _dark_steel
|
||||
clamp.position = Vector3(x, clamp_y, -1.12)
|
||||
root.add_child(clamp)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"PipeJunction",
|
||||
root,
|
||||
Vector3(x, 0.91, -1.12),
|
||||
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.90, -1.08),
|
||||
Vector3(0.66, 0.13, 0.27),
|
||||
_painted_steel,
|
||||
Vector3(-8.0, 0.0, 0.0)
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"WarmLightDiffuser",
|
||||
root,
|
||||
Vector3(0.0, 0.835, -0.89),
|
||||
Vector3(0.38, 0.018, 0.065),
|
||||
_warm_emission,
|
||||
Vector3(-8.0, 0.0, 0.0)
|
||||
)
|
||||
for switch_index in 7:
|
||||
var x := -0.24 + switch_index * 0.08
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"BreakerSwitch",
|
||||
root,
|
||||
Vector3(x, 0.83, -0.88),
|
||||
0.006,
|
||||
0.035,
|
||||
_rubber,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
|
||||
|
||||
func _build_panel_hardware() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "PanelHardware"
|
||||
add_child(root)
|
||||
for x: float in [-0.73, -0.49, -0.24, 0.24, 0.49, 0.73]:
|
||||
for y: float in [-0.18, 0.22]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"PanelFastener",
|
||||
root,
|
||||
Vector3(x, y, -1.105),
|
||||
0.014,
|
||||
0.018,
|
||||
_dark_steel,
|
||||
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_analog_gauges() -> void:
|
||||
_analog_gauges = CockpitAnalogGauges.new(_materials)
|
||||
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)
|
||||
@@ -5,6 +5,9 @@ const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
|
||||
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
|
||||
const CockpitDetailing = preload(
|
||||
"res://game/presentation/cockpit/cockpit_detailing.gd"
|
||||
)
|
||||
|
||||
const COLOR_NORMAL := Color(0.45, 0.89, 0.90, 1.0)
|
||||
const COLOR_ATTENTION := Color(1.0, 0.75, 0.20, 1.0)
|
||||
@@ -17,6 +20,7 @@ const COLOR_CRITICAL := Color(1.0, 0.42, 0.31, 1.0)
|
||||
@onready var _status_label: Label3D = %StatusLabel
|
||||
@onready var _sonar_screen: MeshInstance3D = %SonarScreen
|
||||
@onready var _sonar_placeholder: Label3D = %SonarPlaceholder
|
||||
@onready var _detailing: CockpitDetailing = %CockpitDetailing
|
||||
|
||||
var _sonar_texture: Texture2D
|
||||
|
||||
@@ -65,6 +69,7 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void:
|
||||
maxi(snapshot.alerts.hull, snapshot.alerts.depth)
|
||||
)
|
||||
_status_label.modulate = _alert_color(snapshot.alerts.maximum)
|
||||
_detailing.present(update, telemetry)
|
||||
|
||||
|
||||
func resources_text() -> String:
|
||||
@@ -99,6 +104,10 @@ func sonar_texture() -> Texture2D:
|
||||
return _sonar_texture
|
||||
|
||||
|
||||
func detailing() -> CockpitDetailing:
|
||||
return _detailing
|
||||
|
||||
|
||||
func _return_text(estimate: ReturnEstimate) -> String:
|
||||
match estimate.status:
|
||||
ReturnEstimate.Status.SAFE:
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
[gd_scene load_steps=19 format=3]
|
||||
[gd_scene load_steps=20 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_instrument_panel.gd" id="1_instruments"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_detailing.gd" id="2_detailing"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="SteelMaterial"]
|
||||
albedo_color = Color(0.095, 0.12, 0.12, 1)
|
||||
metallic = 0.82
|
||||
roughness = 0.34
|
||||
albedo_color = Color(0.055, 0.07, 0.07, 1)
|
||||
metallic = 0.76
|
||||
roughness = 0.46
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="PaintedMaterial"]
|
||||
albedo_color = Color(0.085, 0.185, 0.19, 1)
|
||||
metallic = 0.58
|
||||
roughness = 0.48
|
||||
albedo_color = Color(0.045, 0.12, 0.125, 1)
|
||||
metallic = 0.48
|
||||
roughness = 0.61
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="BrassMaterial"]
|
||||
albedo_color = Color(0.36, 0.24, 0.075, 1)
|
||||
metallic = 0.78
|
||||
roughness = 0.31
|
||||
albedo_color = Color(0.27, 0.17, 0.045, 1)
|
||||
metallic = 0.74
|
||||
roughness = 0.43
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="RubberMaterial"]
|
||||
albedo_color = Color(0.018, 0.024, 0.025, 1)
|
||||
@@ -23,10 +24,9 @@ roughness = 0.88
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="GlassMaterial"]
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.12, 0.42, 0.47, 0.09)
|
||||
metallic = 0.12
|
||||
roughness = 0.08
|
||||
albedo_color = Color(0.08, 0.29, 0.32, 0.12)
|
||||
metallic = 0.05
|
||||
roughness = 0.16
|
||||
cull_mode = 2
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="ScreenMaterial"]
|
||||
@@ -103,6 +103,10 @@ radial_segments = 32
|
||||
[node name="CockpitInterior" type="Node3D"]
|
||||
script = ExtResource("1_instruments")
|
||||
|
||||
[node name="CockpitDetailing" type="Node3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("2_detailing")
|
||||
|
||||
[node name="CabinShell" type="Node3D" parent="."]
|
||||
|
||||
[node name="LeftWall" type="MeshInstance3D" parent="CabinShell"]
|
||||
@@ -437,15 +441,17 @@ mesh = SubResource("KnobMesh")
|
||||
material_override = SubResource("RedMaterial")
|
||||
|
||||
[node name="CabinLight" type="OmniLight3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.92, -0.28)
|
||||
light_color = Color(0.42, 0.75, 0.74, 1)
|
||||
light_energy = 2.2
|
||||
light_color = Color(0.36, 0.68, 0.69, 1)
|
||||
light_energy = 1.25
|
||||
shadow_enabled = true
|
||||
omni_range = 3.3
|
||||
|
||||
[node name="PanelLight" type="OmniLight3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.18, -0.63)
|
||||
light_color = Color(0.23, 0.72, 0.76, 1)
|
||||
light_energy = 0.8
|
||||
light_energy = 0.58
|
||||
shadow_enabled = false
|
||||
omni_range = 1.35
|
||||
@@ -0,0 +1,120 @@
|
||||
class_name CockpitMaterialPalette
|
||||
extends RefCounted
|
||||
|
||||
var painted_steel: Material
|
||||
var dark_steel: Material
|
||||
var brass: Material
|
||||
var copper: Material
|
||||
var rubber: Material
|
||||
var ivory: Material
|
||||
var warm_emission: StandardMaterial3D
|
||||
var red_emission: StandardMaterial3D
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
painted_steel = _weathered_material(
|
||||
Color(0.045, 0.12, 0.125),
|
||||
Color(0.16, 0.18, 0.17),
|
||||
0.52,
|
||||
0.58,
|
||||
0.13
|
||||
)
|
||||
dark_steel = _weathered_material(
|
||||
Color(0.045, 0.055, 0.055),
|
||||
Color(0.22, 0.24, 0.22),
|
||||
0.78,
|
||||
0.44,
|
||||
0.08
|
||||
)
|
||||
brass = _weathered_material(
|
||||
Color(0.28, 0.18, 0.045),
|
||||
Color(0.08, 0.16, 0.13),
|
||||
0.74,
|
||||
0.38,
|
||||
0.17
|
||||
)
|
||||
copper = _weathered_material(
|
||||
Color(0.34, 0.12, 0.045),
|
||||
Color(0.055, 0.18, 0.15),
|
||||
0.72,
|
||||
0.42,
|
||||
0.12
|
||||
)
|
||||
rubber = _plain_material(Color(0.012, 0.018, 0.018), 0.0, 0.92)
|
||||
ivory = _plain_material(Color(0.72, 0.69, 0.56), 0.05, 0.63)
|
||||
warm_emission = _emissive_material(Color(0.96, 0.52, 0.18), 2.4)
|
||||
red_emission = _emissive_material(Color(0.92, 0.055, 0.025), 3.6)
|
||||
|
||||
|
||||
func _weathered_material(
|
||||
base_color: Color,
|
||||
worn_color: Color,
|
||||
metallic: float,
|
||||
roughness: float,
|
||||
wear_amount: float
|
||||
) -> ShaderMaterial:
|
||||
var shader := Shader.new()
|
||||
shader.code = """
|
||||
shader_type spatial;
|
||||
render_mode diffuse_burley, specular_schlick_ggx;
|
||||
|
||||
uniform vec4 base_color : source_color;
|
||||
uniform vec4 worn_color : source_color;
|
||||
uniform float metallic_value : hint_range(0.0, 1.0);
|
||||
uniform float roughness_value : hint_range(0.0, 1.0);
|
||||
uniform float wear_amount : hint_range(0.0, 0.4);
|
||||
|
||||
float hash21(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float value_noise(vec2 p) {
|
||||
vec2 cell = floor(p);
|
||||
vec2 local = fract(p);
|
||||
local = local * local * (3.0 - 2.0 * local);
|
||||
float a = hash21(cell);
|
||||
float b = hash21(cell + vec2(1.0, 0.0));
|
||||
float c = hash21(cell + vec2(0.0, 1.0));
|
||||
float d = hash21(cell + vec2(1.0, 1.0));
|
||||
return mix(mix(a, b, local.x), mix(c, d, local.x), local.y);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float broad = value_noise(UV * 11.0);
|
||||
float grain = value_noise(UV * 73.0 + vec2(4.2, 8.7));
|
||||
float wear = smoothstep(0.76, 1.0, broad) * wear_amount;
|
||||
ALBEDO = mix(base_color.rgb * mix(0.78, 1.08, broad), worn_color.rgb, wear);
|
||||
METALLIC = metallic_value;
|
||||
ROUGHNESS = clamp(roughness_value + (grain - 0.5) * 0.22, 0.16, 0.96);
|
||||
}
|
||||
"""
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = shader
|
||||
material.set_shader_parameter("base_color", base_color)
|
||||
material.set_shader_parameter("worn_color", worn_color)
|
||||
material.set_shader_parameter("metallic_value", metallic)
|
||||
material.set_shader_parameter("roughness_value", roughness)
|
||||
material.set_shader_parameter("wear_amount", wear_amount)
|
||||
return material
|
||||
|
||||
|
||||
func _plain_material(
|
||||
color: Color,
|
||||
metallic: float,
|
||||
roughness: float
|
||||
) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = color
|
||||
material.metallic = metallic
|
||||
material.roughness = roughness
|
||||
return material
|
||||
|
||||
|
||||
func _emissive_material(color: Color, energy: float) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = color
|
||||
material.emission_enabled = true
|
||||
material.emission = color
|
||||
material.emission_energy_multiplier = energy
|
||||
return material
|
||||
@@ -0,0 +1,67 @@
|
||||
class_name CockpitMeshFactory
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func add_box(
|
||||
node_name: String,
|
||||
parent: Node3D,
|
||||
position: Vector3,
|
||||
size: Vector3,
|
||||
material: Material,
|
||||
rotation_degrees: Vector3 = Vector3.ZERO
|
||||
) -> MeshInstance3D:
|
||||
var instance := MeshInstance3D.new()
|
||||
instance.name = node_name
|
||||
var mesh := BoxMesh.new()
|
||||
mesh.size = size
|
||||
instance.mesh = mesh
|
||||
instance.material_override = material
|
||||
instance.position = position
|
||||
instance.rotation_degrees = rotation_degrees
|
||||
parent.add_child(instance)
|
||||
return instance
|
||||
|
||||
|
||||
static func add_cylinder(
|
||||
node_name: String,
|
||||
parent: Node3D,
|
||||
position: Vector3,
|
||||
radius: float,
|
||||
height: float,
|
||||
material: Material,
|
||||
rotation_degrees: Vector3 = Vector3.ZERO
|
||||
) -> MeshInstance3D:
|
||||
var instance := MeshInstance3D.new()
|
||||
instance.name = node_name
|
||||
var mesh := CylinderMesh.new()
|
||||
mesh.top_radius = radius
|
||||
mesh.bottom_radius = radius
|
||||
mesh.height = height
|
||||
mesh.radial_segments = 16
|
||||
instance.mesh = mesh
|
||||
instance.material_override = material
|
||||
instance.position = position
|
||||
instance.rotation_degrees = rotation_degrees
|
||||
parent.add_child(instance)
|
||||
return instance
|
||||
|
||||
|
||||
static func add_sphere(
|
||||
node_name: String,
|
||||
parent: Node3D,
|
||||
position: Vector3,
|
||||
radius: float,
|
||||
material: Material
|
||||
) -> MeshInstance3D:
|
||||
var instance := MeshInstance3D.new()
|
||||
instance.name = node_name
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = radius
|
||||
mesh.height = radius * 2.0
|
||||
mesh.radial_segments = 16
|
||||
mesh.rings = 8
|
||||
instance.mesh = mesh
|
||||
instance.material_override = material
|
||||
instance.position = position
|
||||
parent.add_child(instance)
|
||||
return instance
|
||||
@@ -5,6 +5,9 @@ const VerticalSliceRoot = preload(
|
||||
)
|
||||
const MainMenuScreen = preload("res://game/presentation/main_menu/main_menu_screen.gd")
|
||||
const CockpitHud = preload("res://game/presentation/cockpit/cockpit_hud.gd")
|
||||
const CockpitDetailing = preload(
|
||||
"res://game/presentation/cockpit/cockpit_detailing.gd"
|
||||
)
|
||||
const PauseMenu = preload("res://game/presentation/pause/pause_menu.gd")
|
||||
const ControlsSettingsScreen = preload(
|
||||
"res://game/presentation/settings/controls_settings_screen.gd"
|
||||
@@ -13,7 +16,7 @@ const ProductionInputAdapter = preload(
|
||||
"res://game/infrastructure/input/production_input_adapter.gd"
|
||||
)
|
||||
|
||||
const TEST_COUNT := 51
|
||||
const TEST_COUNT := 55
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -76,6 +79,9 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
var actor = composition.submarine_actor()
|
||||
var camera = composition.camera_controller()
|
||||
var instruments = composition.cockpit_instrument_panel()
|
||||
var detailing: CockpitDetailing = (
|
||||
instruments.detailing() if instruments != null else null
|
||||
)
|
||||
var adapter := composition.input_adapter() as ProductionInputAdapter
|
||||
var hud := composition.active_screen() as CockpitHud
|
||||
var sonar_service = composition.sonar_service()
|
||||
@@ -84,16 +90,27 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
_expect(actor != null, "production submarine actor is assembled")
|
||||
_expect(camera != null, "cockpit camera is attached to the submarine")
|
||||
_expect(instruments != null, "physical cockpit instruments are assembled")
|
||||
_expect(detailing != null, "cockpit structural detailing is assembled")
|
||||
_expect(hud != null, "critical HUD overlay is assembled")
|
||||
_expect(sonar_service != null, "production sonar service is assembled")
|
||||
_expect(sonar_display != null, "shared sonar presentation is assembled")
|
||||
_expect(composition.latest_update() != null, "dive session starts with a typed update")
|
||||
if actor == null or camera == null or instruments == null or adapter == null or hud == null:
|
||||
if (
|
||||
actor == null
|
||||
or camera == null
|
||||
or instruments == null
|
||||
or detailing == null
|
||||
or adapter == null
|
||||
or hud == null
|
||||
):
|
||||
return
|
||||
|
||||
_expect(instruments.get_node("%WindowFrame") is MeshInstance3D, "cockpit has a physical porthole frame")
|
||||
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
|
||||
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
|
||||
_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("kWh" in instruments.resources_text() and "kW" in instruments.resources_text(), "battery instrument has exact units")
|
||||
_expect("L/min" in instruments.resources_text(), "oxygen instrument has volume and consumption")
|
||||
_expect("bar" in instruments.integrity_text(), "hull instrument has current and certified pressure")
|
||||
|
||||
Reference in new issue
Block a user