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" ) const CockpitPressureBulkhead = preload( "res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd" ) const CAGE_LAMP_SCENE = preload("res://game/presentation/cockpit/assets/models/cage_lamp.glb") ## Flange centre of the right-hand lamp, on the flat end cap of the pressure ## hull beside the porthole (the left lamp mirrors x). The cap is flat there, so ## the lamp is seated square to it, facing the cabin. const LAMP_MOUNT_OFFSET := Vector3(0.636, 1.106, -1.3) ## Centre of the glass globe in the lamp model: where the light is emitted. const LAMP_GLOBE_CENTRE := Vector3(0.0, 0.0, 0.064) ## Globe emission (1.5 as exported): lower, so the globe does not bloom into a blob. const LAMP_GLASS_ENERGY := 1.0 ## Tungsten-like caged lamps: the cabin's main light. const WARM_LAMP_COLOR := Color(1.0, 0.66, 0.4) const WARM_LAMP_ENERGY := 0.5 ## Same dimming ratio as before when the red emergency light takes over. const WARM_LAMP_CRITICAL_ENERGY := 0.23 ## Short range and a steep falloff: each lamp makes a pool on the wall around ## it and leaves the lower and outer hull dark. const WARM_LAMP_RANGE_M := 1.6 const WARM_LAMP_ATTENUATION := 2.0 ## Cabin parts are centimetre-scale: the default bias detaches their shadows. const WARM_LAMP_SHADOW_BIAS := 0.03 var _materials: CockpitMaterialPalette var _warm_lights: Array[OmniLight3D] = [] var _emergency_light: OmniLight3D var _porthole_fill: OmniLight3D func _init(materials: CockpitMaterialPalette) -> void: _materials = materials name = "PracticalLighting" _add_caged_lamp(-1.0) _add_caged_lamp(1.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 = ( WARM_LAMP_CRITICAL_ENERGY if alert_level >= DiveAlertLevel.Value.CRITICAL else WARM_LAMP_ENERGY ) func practical_light_count() -> int: return _warm_lights.size() func _add_caged_lamp(side: float) -> void: var root := Node3D.new() root.name = "CagedLamp" root.position = Vector3( side * LAMP_MOUNT_OFFSET.x, LAMP_MOUNT_OFFSET.y, LAMP_MOUNT_OFFSET.z ) add_child(root) var model := CAGE_LAMP_SCENE.instantiate() as Node3D model.name = "CageLampModel" root.add_child(model) # The light sits inside the cage, 3 cm from its bars: casting, the lamp's # own body would swallow it. CockpitMeshFactory.disable_shadow_casting(model) var glass := CockpitMeshFactory.own_surface_material( model.find_child("CageLamp_Glass", true, false) as MeshInstance3D ) if glass != null: glass.emission_energy_multiplier = LAMP_GLASS_ENERGY var light := OmniLight3D.new() light.name = "WarmPracticalLight" light.position = LAMP_GLOBE_CENTRE light.light_color = WARM_LAMP_COLOR light.light_energy = WARM_LAMP_ENERGY light.omni_range = WARM_LAMP_RANGE_M light.omni_attenuation = WARM_LAMP_ATTENUATION light.shadow_enabled = true light.shadow_bias = WARM_LAMP_SHADOW_BIAS root.add_child(light) _warm_lights.append(light) func _add_fill_light() -> void: # The only cool light in the cabin: sea light entering through the porthole, # placed just inside the glass so it rims the ring, its bolts and the first # band of wall, and reaches the dashboard only from behind. var porthole_fill := OmniLight3D.new() porthole_fill.name = "PortholeFill" porthole_fill.position = Vector3(0.0, 0.47, -1.2) porthole_fill.light_color = Color(0.30, 0.56, 0.78) porthole_fill.light_energy = 1.0 porthole_fill.omni_range = 1.1 porthole_fill.shadow_enabled = false # 30 cm behind the glass, it would mirror itself as a hard blue point. porthole_fill.light_cull_mask &= ~CockpitPressureBulkhead.PORTHOLE_GLASS_RENDER_LAYER_BIT add_child(porthole_fill) _porthole_fill = porthole_fill func fill_light_ignores_porthole_glass() -> bool: return ( _porthole_fill != null and ( _porthole_fill.light_cull_mask & CockpitPressureBulkhead.PORTHOLE_GLASS_RENDER_LAYER_BIT ) == 0 ) func _add_emergency_light() -> void: _emergency_light = OmniLight3D.new() _emergency_light.name = "EmergencyLight" # High and central, so no single dial sits right under it and washes out. _emergency_light.position = Vector3(0.0, 1.1, -0.95) _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)