extends Node ## Day/night cycle. Moves the sun along its arc and drives sky colours, sun and ## moon light, ambient light and fog from the time of day. ## ## Time is a fraction of a day (0 = midnight, 0.25 = 06:00 sunrise, ## 0.5 = noon, 0.75 = 18:00 sunset). One day lasts `day_length` real seconds ## (20 min). Saved with the world (see world.gd collect_save). signal new_day(day: int) @export var day_length := 1200.0 @export var sun_tilt_deg := 35.0 ## arc leans toward +Z, so noon sun sits at 55° const DAY_BIAS := 0.2 var day := 1 var time := 8.0 / 24.0 var frozen := false ## title-screen backdrop: hold a fixed time var underground := 0.0 ## 0..1: how far into the tunnel you are (no daylight; world.gd sets it) const AERIAL := 0.55 ## the fog's sky tint with distance (scenes/default_env.tres) var sun: DirectionalLight3D var moon: DirectionalLight3D var env: Environment var sky_mat: ShaderMaterial var daylight := 1.0 ## 0 at night, 1 in full day (smooth) var sun_height := 1.0 ## sin of sun elevation (-1..1) var _sky_timer := 0.0 # Palette: [zenith, horizon, fog] const DAY_COLS := [Color(0.24, 0.44, 0.72), Color(0.72, 0.79, 0.85), Color(0.70, 0.77, 0.85)] const DUSK_COLS := [Color(0.22, 0.27, 0.48), Color(0.98, 0.62, 0.38), Color(0.82, 0.62, 0.50)] const NIGHT_COLS := [Color(0.02, 0.03, 0.07), Color(0.07, 0.09, 0.16), Color(0.07, 0.09, 0.14)] const OVERCAST_COLS := [Color(0.40, 0.44, 0.49), Color(0.58, 0.61, 0.64), Color(0.56, 0.59, 0.62)] const BASE_FOG := 0.0035 ## default_env.tres fog_density var weather: Node ## weather.gd, set by world.gd var _flash_shown := false func setup(world_sun: DirectionalLight3D, environment: Environment) -> void: sun = world_sun env = environment sky_mat = env.sky.sky_material as ShaderMaterial moon = DirectionalLight3D.new() moon.name = "Moon" moon.light_color = Color(0.55, 0.65, 1.0) moon.shadow_enabled = false moon.directional_shadow_max_distance = 120.0 moon.sky_mode = DirectionalLight3D.SKY_MODE_LIGHT_ONLY # keep the sky shader's LIGHT0 = sun sun.add_sibling(moon) apply(true) func hours() -> float: return time * 24.0 func clock_text() -> String: var h := hours() return "Day %d %02d:%02d" % [day, int(h), int(fmod(h, 1.0) * 60.0)] func is_night() -> bool: return daylight < 0.3 ## Every 7th night is a wolf moon: the moon turns red and an alpha hunts with ## the pack (wolf_pack.gd). Worked out from the day alone, so every game on a ## server agrees without being told. static func wolf_moon_day(d: int) -> bool: return d >= 7 and d % 7 == 0 ## Tonight's moon is a wolf moon (the small hours count as the night before). func wolf_moon_now() -> bool: var h := hours() return (h >= 19.5 or h < 5.5) and wolf_moon_day(day - (1 if h < 5.5 else 0)) func advance(seconds: float) -> void: time += seconds / day_length while time >= 1.0: time -= 1.0 day += 1 new_day.emit(day) apply(false) func _process(delta: float) -> void: if not frozen: advance(delta) ## Updates lights every call; the sky material (which triggers a radiance ## rebuild) only a few times a second unless `force`. func apply(force: bool) -> void: if sun == null: return var a := (time - 0.25) * TAU var t := deg_to_rad(sun_tilt_deg) # +DAY_BIAS lifts the whole arc: sunrise ~05:00, sunset ~19:00, so days # are longer than nights and dusk lingers. var to_sun := Vector3(cos(a), sin(a) * cos(t) + DAY_BIAS, sin(a) * sin(t)).normalized() sun_height = to_sun.y daylight = smoothstep(-0.2, 0.25, to_sun.y) var low := 1.0 - smoothstep(0.05, 0.45, absf(to_sun.y)) # 1 near the horizon # Weather (weather.gd) dims the sun, thickens fog and greys the sky. var w_light := 1.0 var w_fog := 1.0 var w_clouds := 0.3 var w_flash := 0.0 if weather != null: w_light = weather.light w_fog = weather.fog w_clouds = weather.clouds w_flash = weather.flash var overcast := smoothstep(0.5, 1.0, w_clouds) sun.global_basis = Basis.looking_at(-to_sun if to_sun.y > -0.2 else Vector3.DOWN, Vector3.FORWARD) sun.light_energy = 1.8 * smoothstep(-0.03, 0.15, to_sun.y) * w_light sun.light_color = Color(1.0, 0.95, 0.83).lerp(Color(1.0, 0.58, 0.32), low) sun.visible = sun.light_energy > 0.01 sun.shadow_enabled = sun.visible var moon_dir := Vector3(-to_sun.x, absf(to_sun.y) * 0.8 + 0.25, -to_sun.z * 0.5).normalized() moon.global_basis = Basis.looking_at(-moon_dir, Vector3.FORWARD) var red := wolf_moon_now() moon.light_color = Color(1.0, 0.38, 0.3) if red else Color(0.55, 0.65, 1.0) moon.light_energy = (0.5 if red else 0.38) * (1.0 - daylight) * (1.0 - 0.6 * overcast) moon.visible = moon.light_energy > 0.01 moon.shadow_enabled = moon.visible and not sun.visible env.ambient_light_energy = lerpf(0.3, 1.0, daylight) * lerpf(1.0, 0.85, overcast) + w_flash * 3.0 env.fog_light_energy = lerpf(0.25, 1.0, daylight) * lerpf(1.0, 0.7, overcast) + w_flash * 2.0 env.volumetric_fog_density = lerpf(0.0025, 0.0045, daylight) * lerpf(1.0, 2.0, overcast) if underground > 0.0: # In the tunnel (J1): no sun, moon or sky, only the lights you bring. var u := underground sun.light_energy *= 1.0 - u sun.visible = sun.light_energy > 0.01 sun.shadow_enabled = sun.visible moon.light_energy *= 1.0 - u moon.visible = moon.light_energy > 0.01 env.ambient_light_energy = lerpf(env.ambient_light_energy, 0.015, u) env.ambient_light_sky_contribution = 1.0 - u env.fog_light_energy = lerpf(env.fog_light_energy, 0.0, u) env.volumetric_fog_density *= 1.0 - u # (Sky reflections and the fog's sky-coloured distance would light the # rock blue-grey all the way down: off.) env.fog_aerial_perspective = lerpf(AERIAL, 0.0, u) env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED if u > 0.5 else Environment.REFLECTION_SOURCE_SKY else: env.ambient_light_sky_contribution = 1.0 env.fog_aerial_perspective = AERIAL env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY env.fog_density = BASE_FOG * w_fog _sky_timer -= get_process_delta_time() if not force and _sky_timer > 0.0 and w_flash < 0.05 and not _flash_shown: return _flash_shown = w_flash >= 0.05 _sky_timer = 0.25 var cols: Array = [] for i in range(3): var dayc: Color = DAY_COLS[i].lerp(DUSK_COLS[i], low * daylight * (1.0 - overcast * 0.7)) dayc = dayc.lerp(OVERCAST_COLS[i], overcast * 0.85) var night: Color = NIGHT_COLS[i] if red: night = night.lerp(Color(0.2, 0.04, 0.04), 0.5) # a blood-red night sky cols.append(night.lerp(dayc, daylight)) env.fog_light_color = cols[2] if sky_mat != null: sky_mat.set_shader_parameter("zenith_col", cols[0]) sky_mat.set_shader_parameter("horizon_col", cols[1]) sky_mat.set_shader_parameter("energy", lerpf(0.35, 1.0, daylight) * lerpf(1.0, 0.72, overcast) + w_flash * 2.5) sky_mat.set_shader_parameter("star_strength", (1.0 - smoothstep(0.0, 0.35, daylight)) * (1.0 - overcast)) sky_mat.set_shader_parameter("cloud_cover", lerpf(0.35, 1.05, w_clouds)) var lit := Color(0.16, 0.18, 0.24).lerp(Color(1, 1, 1), daylight) sky_mat.set_shader_parameter("cloud_lit", lit.lerp(lit * Color(0.55, 0.57, 0.6), overcast)) sky_mat.set_shader_parameter("sun_tint", Color(1.0, 0.86, 0.62).lerp(Color(1.0, 0.55, 0.3), low)) ## 0..100 "how warm the air is" before fires, shelter and clothes. func air_warmth() -> float: return lerpf(22.0, 68.0, daylight)