177 lines
7.3 KiB
GDScript
177 lines
7.3 KiB
GDScript
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)
|