@tool extends DirectionalLight3D class_name DayNightController signal time_changed(current_hours: float) @export var grid_manager_path: NodePath @export_range(1.0, 48.0, 0.5) var day_length_hours: float = 12.0 @export_range(1.0, 600.0, 1.0) var real_seconds_per_game_hour: float = 60.0 @export_range(0.0, 48.0, 0.25) var start_time_of_day_hours: float = 6.0 @export var orbit_axis: Vector3 = Vector3(1.0, 0.0, -1.0) @export_range(0.0, 90.0, 0.5) var inclination_degrees: float = 35.0 var _grid_manager: Node3D = null var _time_of_day_hours: float = 0.0 var _initial_environment_settings: Dictionary = {} var _sunlight_multiplier: float = 1.0 func _ready() -> void: set_process(true) _resolve_references() _time_of_day_hours = clamp(start_time_of_day_hours, 0.0, day_length_hours) _apply_sun_lighting() _emit_time_changed() _capture_initial_environment() _update_environment() func _process(delta: float) -> void: if Engine.is_editor_hint(): return if day_length_hours <= 0.0 or real_seconds_per_game_hour <= 0.0: return _time_of_day_hours = fposmod(_time_of_day_hours + delta / real_seconds_per_game_hour, day_length_hours) _apply_sun_lighting() _emit_time_changed() _update_environment() func _resolve_references() -> void: if grid_manager_path != NodePath(): _grid_manager = get_node_or_null(grid_manager_path) else: _grid_manager = get_parent() as Node3D func _apply_sun_lighting() -> void: if day_length_hours <= 0.0: return var cycle: float = 0.0 if day_length_hours > 0.0: cycle = fposmod(_time_of_day_hours / day_length_hours, 1.0) var sun_angle: float = cycle * TAU var axis: Vector3 = orbit_axis.normalized() if axis.length() == 0.0: axis = Vector3.RIGHT var direction: Vector3 = Vector3.UP.rotated(axis, sun_angle) direction = direction.rotated(Vector3.UP, deg_to_rad(inclination_degrees)).normalized() var target: Vector3 = _grid_manager.global_transform.origin if _grid_manager else global_transform.origin var sun_distance: float = 60.0 var sun_position: Vector3 = target - direction * sun_distance var up_vector: Vector3 = Vector3.UP if direction.is_equal_approx(up_vector) or direction.is_equal_approx(-up_vector): up_vector = Vector3.FORWARD look_at_from_position(sun_position, target, up_vector) var daylight_phase: float = sin((sun_angle - TAU * 0.25)) var daylight: float = clamp(daylight_phase * 0.5 + 0.5, 0.0, 1.0) var dawn_color: Color = Color(0.95, 0.58, 0.32) var night_color: Color = Color(0.12, 0.18, 0.35) var day_color: Color = Color(1.0, 0.96, 0.88) var warm_mix: Color = dawn_color.lerp(day_color, clamp((daylight - 0.25) / 0.5, 0.0, 1.0)) light_color = night_color.lerp(warm_mix, daylight) light_energy = lerp(0.05, 1.25, daylight) shadow_enabled = daylight > 0.1 _sunlight_multiplier = daylight func get_time_of_day_hours() -> float: return _time_of_day_hours func get_day_length_hours() -> float: return day_length_hours func _emit_time_changed() -> void: emit_signal("time_changed", _time_of_day_hours) func _capture_initial_environment() -> void: var world: World3D = get_world_3d() if not world: return var env: Environment = world.environment if env: _initial_environment_settings = { "ambient_light_color": env.ambient_light_color, "ambient_light_energy": env.ambient_light_energy, "background_color": env.background_color } func _update_environment() -> void: var world: World3D = get_world_3d() if not world: return var env: Environment = world.environment if not env: return var daylight: float = clamp(_sunlight_multiplier, 0.0, 1.0) var ambient_day: Color = Color(0.6, 0.7, 0.85) var ambient_night: Color = Color(0.02, 0.05, 0.1) var background_day: Color = Color(0.5, 0.75, 1.0) var background_night: Color = Color(0.01, 0.02, 0.08) env.ambient_light_color = ambient_night.lerp(ambient_day, daylight) env.ambient_light_energy = lerp(0.15, 1.5, daylight) env.background_color = background_night.lerp(background_day, daylight)