feat(cockpit): replace procedural cockpit with twelve scripted models
Quality Gate / Quality Gate (pull_request) Successful in 9m4s
Quality Gate / Quality Gate (pull_request) Successful in 9m4s
Instance twelve Blender-scripted glTF models in the cockpit scene and rebuild the cabin around them: gauge panel, sonar, switch panel, alert box, speaker, porthole, hull wall, caged lamps, gas bottle, handrails and side consoles. Presentation only; no domain, application or world rule changes beyond enabling glow in the test environment. - Needles, alert lamps and sonar screen are driven by the existing snapshots; the alert box now carries SYS/ATTN/ALRM. - Depth dial reads percent of certified depth. - Sonar display restyled as a turquoise holographic relief with a shader grid and height scale; SubViewport matches the glass (960x604). - Cabin lighting retuned: warm pooled lamp light, cool porthole rim, lamp shadows, dial backlight. - Procedural stand-ins removed (bulkhead, tanks, consoles, levers, indicator columns, placeholder pipe run, dial face PNGs). - Tests adapted where nodes disappeared; one assertion removed (pipe_segment_count) until the pipe kit is placed. - Model .glb files are committed without LFS; textures Godot extracts next to them at import are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
912123087f
commit
2f9b7c0894
45 files changed
+1353
-1603
No files matched your search
@@ -1,265 +1,101 @@
|
||||
class_name CockpitAnalogGauges
|
||||
extends Node3D
|
||||
|
||||
const CockpitMaterialPalette = preload("res://game/presentation/cockpit/cockpit_material_palette.gd")
|
||||
## Drives the imported gauge panel model: four needles and its six pilot lights.
|
||||
## The model scene is instanced by `cockpit_interior.tscn`; this script only
|
||||
## rotates needles and sets emission, it never rebuilds geometry.
|
||||
|
||||
const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh_factory.gd")
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _oxygen_needle: Node3D
|
||||
var _battery_needle: Node3D
|
||||
var _hull_needle: Node3D
|
||||
var _depth_needle: Node3D
|
||||
var _indicator_materials: Array[StandardMaterial3D] = []
|
||||
var _active_indicator_count := 0
|
||||
## Printed dial sweep: ratio 0 points lower-left, ratio 1 lower-right, clockwise.
|
||||
const NEEDLE_DEGREES_AT_ZERO := 120.0
|
||||
const NEEDLE_DEGREES_AT_FULL := -120.0
|
||||
## The snapshot carries no per-instrument lamp state, so the panel pilot lights
|
||||
## stay dark; the alert level is reported by `CockpitAlertBox`.
|
||||
const PILOT_LIGHT_REST_ENERGY := 0.0
|
||||
## Scales the dial backlight baked in the model (1.0 = as exported), so the
|
||||
## paper reads as lit ivory without outshining the cabin's warm lamps.
|
||||
const DIAL_BACKLIGHT_ENERGY := 0.7
|
||||
|
||||
const NEEDLE_NAMES: Array[String] = [
|
||||
"Needle_Oxygen",
|
||||
"Needle_Battery",
|
||||
"Needle_Hull",
|
||||
"Needle_Depth",
|
||||
]
|
||||
|
||||
var _needles: Dictionary[String, Node3D] = {}
|
||||
var _dials: MeshInstance3D
|
||||
var _pilot_light_materials: Array[BaseMaterial3D] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for needle_name: String in NEEDLE_NAMES:
|
||||
var needle := find_child(needle_name, true, false) as Node3D
|
||||
if needle == null:
|
||||
push_error("Gauge panel model has no %s node" % needle_name)
|
||||
continue
|
||||
_needles[needle_name] = needle
|
||||
_dials = find_child("GaugeDials", true, false) as MeshInstance3D
|
||||
var dial_material := CockpitMeshFactory.own_surface_material(_dials)
|
||||
if dial_material != null:
|
||||
dial_material.emission_energy_multiplier = DIAL_BACKLIGHT_ENERGY
|
||||
for light: Node in find_children("Light_*", "MeshInstance3D", true, false):
|
||||
var material := CockpitMeshFactory.own_surface_material(light as MeshInstance3D)
|
||||
if material != null:
|
||||
material.emission_energy_multiplier = PILOT_LIGHT_REST_ENERGY
|
||||
_pilot_light_materials.append(material)
|
||||
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "AnalogGauges"
|
||||
_build_instrument_cluster()
|
||||
_build_indicator_bank()
|
||||
_oxygen_needle = _add_gauge(
|
||||
"OxygenGauge",
|
||||
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.94,
|
||||
_materials.battery_gauge_face
|
||||
)
|
||||
_hull_needle = _add_gauge(
|
||||
"HullGauge",
|
||||
Vector3(0.09, 0.12, 0.145),
|
||||
0.86,
|
||||
_materials.hull_gauge_face
|
||||
)
|
||||
_depth_needle = _add_gauge(
|
||||
"DepthGauge",
|
||||
Vector3(0.47, 0.12, 0.145),
|
||||
1.12,
|
||||
_materials.depth_gauge_face
|
||||
)
|
||||
|
||||
func present(
|
||||
oxygen_ratio: float,
|
||||
battery_ratio: float,
|
||||
hull_ratio: float,
|
||||
depth_ratio: float,
|
||||
alert_level: DiveAlertLevel.Value
|
||||
depth_ratio: float
|
||||
) -> void:
|
||||
_set_needle_ratio(_oxygen_needle, oxygen_ratio)
|
||||
_set_needle_ratio(_battery_needle, battery_ratio)
|
||||
_set_needle_ratio(_hull_needle, hull_ratio)
|
||||
_set_needle_ratio(_depth_needle, depth_ratio)
|
||||
_update_indicator_bank(alert_level)
|
||||
_set_needle_ratio("Needle_Oxygen", oxygen_ratio)
|
||||
_set_needle_ratio("Needle_Battery", battery_ratio)
|
||||
_set_needle_ratio("Needle_Hull", hull_ratio)
|
||||
_set_needle_ratio("Needle_Depth", depth_ratio)
|
||||
|
||||
|
||||
func gauge_count() -> int:
|
||||
return _needles.size()
|
||||
|
||||
|
||||
func pilot_light_count() -> int:
|
||||
return _pilot_light_materials.size()
|
||||
|
||||
|
||||
func lit_pilot_light_count() -> int:
|
||||
var count := 0
|
||||
for material: BaseMaterial3D in _pilot_light_materials:
|
||||
if material.emission_energy_multiplier > 0.0:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func needle_degrees(needle_name: String) -> float:
|
||||
var needle: Node3D = _needles.get(needle_name)
|
||||
return 0.0 if needle == null else needle.rotation_degrees.z
|
||||
|
||||
|
||||
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
|
||||
return needle_degrees("Needle_Battery")
|
||||
|
||||
|
||||
func _build_instrument_cluster() -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"InstrumentClusterHousing",
|
||||
self,
|
||||
Vector3(-0.04, 0.105, 0.095),
|
||||
Vector3(1.42, 0.31, 0.105),
|
||||
_materials.overhead_steel
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"InstrumentClusterFoot",
|
||||
self,
|
||||
Vector3(-0.04, -0.025, 0.075),
|
||||
Vector3(1.48, 0.040, 0.14),
|
||||
_materials.dark_steel
|
||||
)
|
||||
|
||||
func _build_indicator_bank() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "IndicatorBank"
|
||||
add_child(root)
|
||||
var colors: Array[Color] = [
|
||||
Color(0.08, 0.72, 0.38),
|
||||
Color(1.0, 0.46, 0.035),
|
||||
Color(0.92, 0.035, 0.018),
|
||||
]
|
||||
var captions: Array[String] = ["SYS", "ATTN", "ALRM"]
|
||||
for indicator_index in colors.size():
|
||||
var y := 0.165 - indicator_index * 0.050
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"%sBezel" % captions[indicator_index],
|
||||
root,
|
||||
Vector3(0.68, y, 0.156),
|
||||
0.016,
|
||||
0.012,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var material := _indicator_material(colors[indicator_index])
|
||||
_indicator_materials.append(material)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"%sLens" % captions[indicator_index],
|
||||
root,
|
||||
Vector3(0.68, y, 0.165),
|
||||
0.009,
|
||||
0.008,
|
||||
material,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var label := Label3D.new()
|
||||
label.name = "%sLabel" % captions[indicator_index]
|
||||
label.text = captions[indicator_index]
|
||||
label.font_size = 8
|
||||
label.pixel_size = 0.00042
|
||||
label.modulate = Color(0.70, 0.67, 0.56)
|
||||
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
|
||||
material.metallic = 0.05
|
||||
material.roughness = 0.16
|
||||
material.emission_enabled = true
|
||||
material.emission = color
|
||||
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:
|
||||
active_index = 2
|
||||
elif alert_level >= DiveAlertLevel.Value.ATTENTION:
|
||||
active_index = 1
|
||||
for indicator_index in _indicator_materials.size():
|
||||
_indicator_materials[indicator_index].emission_energy_multiplier = (
|
||||
2.8 if indicator_index == active_index else 0.04
|
||||
)
|
||||
_active_indicator_count = 1
|
||||
func dial_texture_is_loaded() -> bool:
|
||||
if _dials == null or _dials.mesh == null or _dials.mesh.get_surface_count() == 0:
|
||||
return false
|
||||
var material := _dials.mesh.surface_get_material(0) as BaseMaterial3D
|
||||
return material != null and material.albedo_texture != null
|
||||
|
||||
|
||||
func _add_gauge(
|
||||
gauge_name: String,
|
||||
position: Vector3,
|
||||
gauge_scale: float,
|
||||
face_material: Material
|
||||
) -> Node3D:
|
||||
var gauge := Node3D.new()
|
||||
gauge.name = gauge_name
|
||||
gauge.position = position
|
||||
gauge.scale = Vector3(gauge_scale, gauge_scale, gauge_scale)
|
||||
add_child(gauge)
|
||||
CockpitMeshFactory.add_box(
|
||||
"MountingPlate",
|
||||
gauge,
|
||||
Vector3(0.0, 0.0, -0.025),
|
||||
Vector3(0.225, 0.225, 0.055),
|
||||
_materials.overhead_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)
|
||||
)
|
||||
_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 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)
|
||||
var needle_root := Node3D.new()
|
||||
needle_root.name = "Needle"
|
||||
needle_root.position.z = 0.038
|
||||
gauge.add_child(needle_root)
|
||||
CockpitMeshFactory.add_box(
|
||||
"NeedleBlade",
|
||||
needle_root,
|
||||
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,
|
||||
Vector3.ZERO,
|
||||
0.018,
|
||||
0.012,
|
||||
_materials.dark_steel,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var glass := MeshInstance3D.new()
|
||||
glass.name = "GaugeGlass"
|
||||
var glass_mesh := CylinderMesh.new()
|
||||
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.050
|
||||
glass.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
gauge.add_child(glass)
|
||||
return needle_root
|
||||
|
||||
|
||||
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:
|
||||
func _set_needle_ratio(needle_name: String, ratio: float) -> void:
|
||||
var needle: Node3D = _needles.get(needle_name)
|
||||
if needle != null:
|
||||
needle.rotation_degrees.z = lerpf(125.0, -125.0, clampf(ratio, 0.0, 1.0))
|
||||
needle.rotation_degrees.z = lerpf(
|
||||
NEEDLE_DEGREES_AT_ZERO,
|
||||
NEEDLE_DEGREES_AT_FULL,
|
||||
clampf(ratio, 0.0, 1.0)
|
||||
)
|
||||
Reference in new issue
Block a user