- Seeded weather (clear/cloudy/rain/storm, snow and blizzards on cold days and above the snowline), lightning, synthesised rain/wind/thunder sound - Rain/snow particles stopped by roofs; wet ground, settled snow and wind via global shader uniforms - Snap-together wood floor, wall, door, roof and gable with structural support, collapse and X to dismantle - Shelter detection: dry under a roof, warmer and Rested inside with a fire; rain burns an uncovered campfire faster - Bed sets the spawn point and lets you sleep through the night - Trees, boulders and ground pickups regrow; timers saved - Grass cleared under floors; shorter HUD hint - New test_weather_building suite (62 checks) and tests/run_all.sh, which also fails on script errors
192 lines
6.9 KiB
GDScript
192 lines
6.9 KiB
GDScript
extends Node
|
|
## Weather model: what the sky is doing right now, and what that means for
|
|
## warmth, wetness and light. Visuals and sound live in weather_fx.gd.
|
|
##
|
|
## Deterministic: the weather at any moment is a function of the world seed and
|
|
## the absolute world time (day + time of day), so saves don't store it and in
|
|
## M2 every client can compute the same weather as the server.
|
|
##
|
|
## A slow 1D noise "front" value picks the state: high = clear, low = rain,
|
|
## lowest = storm. About one day in four is a cold day, when rain falls as snow
|
|
## (and a storm becomes a blizzard). Above the snowline it always snows.
|
|
## Outputs ease toward the state's targets over about a minute, so fronts
|
|
## roll in rather than switching.
|
|
|
|
signal state_changed(state: String)
|
|
|
|
const STATES := ["clear", "cloudy", "rain", "storm", "snow", "blizzard"]
|
|
## Thresholds on the front value (see _state_for). Tuned with
|
|
## tests/test_weather_building.gd so that over many days it's roughly
|
|
## clear 40%, cloudy 30%, rain 20%, storm 10%.
|
|
var cut_cloudy := 0.07
|
|
var cut_rain := -0.19
|
|
var cut_storm := -0.42
|
|
|
|
## Per-state targets: clouds 0..1, precip 0..1, wind 0..1, light (sun/ambient
|
|
## multiplier), fog (density multiplier), warmth (added to air warmth).
|
|
const TARGETS := {
|
|
"clear": {"clouds": 0.30, "precip": 0.0, "wind": 0.25, "light": 1.00, "fog": 1.0, "warmth": 0.0},
|
|
"cloudy": {"clouds": 0.72, "precip": 0.0, "wind": 0.45, "light": 0.72, "fog": 1.4, "warmth": -3.0},
|
|
"rain": {"clouds": 0.92, "precip": 0.6, "wind": 0.60, "light": 0.50, "fog": 2.2, "warmth": -6.0},
|
|
"storm": {"clouds": 1.00, "precip": 1.0, "wind": 1.00, "light": 0.30, "fog": 3.0, "warmth": -12.0},
|
|
"snow": {"clouds": 0.90, "precip": 0.6, "wind": 0.35, "light": 0.68, "fog": 2.6, "warmth": -14.0},
|
|
"blizzard": {"clouds": 1.00, "precip": 1.0, "wind": 0.90, "light": 0.45, "fog": 4.5, "warmth": -24.0},
|
|
}
|
|
const COLD_DAY_WARMTH := -8.0
|
|
const BLEND_SECONDS := 50.0 ## time for outputs to cross from one state to another
|
|
const CALM_START := 600.0 ## a new world's first 10 minutes are always clear
|
|
const COLD_DAY_CHANCE := 0.25
|
|
|
|
var world_seed := 1337
|
|
var day_night: Node
|
|
var snowline := 32.0 ## above this height precipitation is snow
|
|
var frozen := false ## title-screen backdrop: hold clear weather
|
|
var override := "" ## force a state (tests, screenshots); "" = natural
|
|
|
|
var state := "clear"
|
|
var cold_day := false
|
|
# Smoothed outputs.
|
|
var clouds := 0.3
|
|
var precip := 0.0
|
|
var wind := 0.25
|
|
var light := 1.0
|
|
var fog := 1.0
|
|
var warmth := 0.0
|
|
var ground_wet := 0.0 ## 0..1 how soaked the ground looks
|
|
var snow_cover := 0.0 ## 0..1 how much snow has settled
|
|
var flash := 0.0 ## lightning brightness, decays fast
|
|
var lightning_count := 0
|
|
|
|
var _noise := FastNoiseLite.new()
|
|
var _strike_left := 8.0
|
|
var _rng := RandomNumberGenerator.new()
|
|
|
|
func setup(seed: int, dn: Node) -> void:
|
|
world_seed = seed
|
|
day_night = dn
|
|
_noise.seed = seed + 401
|
|
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
_noise.frequency = 1.0 / 900.0 # a wet spell lasts about 6 minutes
|
|
_noise.fractal_type = FastNoiseLite.FRACTAL_FBM
|
|
_noise.fractal_octaves = 2
|
|
_rng.seed = seed + 402
|
|
snap()
|
|
|
|
## Seconds since the world began (day 1, 00:00).
|
|
func world_seconds() -> float:
|
|
return (float(day_night.day - 1) + float(day_night.time)) * float(day_night.day_length)
|
|
|
|
func is_cold_day(day: int) -> bool:
|
|
if day <= 1:
|
|
return false
|
|
return float(absi(hash(Vector2i(world_seed, day))) % 10000) / 10000.0 < COLD_DAY_CHANCE
|
|
|
|
## The raw front value at an absolute time (higher = fairer).
|
|
func front_at(t: float) -> float:
|
|
var v := _noise.get_noise_1d(t)
|
|
if t < CALM_START:
|
|
v = maxf(v, 0.3)
|
|
return v
|
|
|
|
func _state_for(v: float, cold: bool) -> String:
|
|
if v > cut_cloudy:
|
|
return "clear"
|
|
if v > cut_rain:
|
|
return "cloudy"
|
|
if v > cut_storm:
|
|
return "snow" if cold else "rain"
|
|
return "blizzard" if cold else "storm"
|
|
|
|
## The state at an absolute time, ignoring smoothing and overrides.
|
|
func state_at(t: float) -> String:
|
|
var day := int(floor(t / float(day_night.day_length))) + 1
|
|
return _state_for(front_at(t), is_cold_day(day))
|
|
|
|
func force(s: String) -> void:
|
|
override = s
|
|
snap()
|
|
|
|
## Jump every output straight to the current state's targets (on load, or
|
|
## when forcing a state).
|
|
func snap() -> void:
|
|
_pick_state()
|
|
var tg: Dictionary = TARGETS[state]
|
|
clouds = tg.clouds
|
|
precip = tg.precip
|
|
wind = tg.wind
|
|
light = tg.light
|
|
fog = tg.fog
|
|
warmth = tg.warmth + (COLD_DAY_WARMTH if cold_day else 0.0)
|
|
ground_wet = 1.0 if state in ["rain", "storm"] else 0.0
|
|
snow_cover = 0.7 if state in ["snow", "blizzard"] else (0.25 if cold_day else 0.0)
|
|
|
|
func _pick_state() -> void:
|
|
if day_night == null:
|
|
return
|
|
cold_day = is_cold_day(day_night.day)
|
|
var s := override
|
|
if frozen and s.is_empty():
|
|
s = "clear"
|
|
if s.is_empty():
|
|
s = _state_for(front_at(world_seconds()), cold_day)
|
|
if s != state:
|
|
state = s
|
|
state_changed.emit(state)
|
|
|
|
func _process(delta: float) -> void:
|
|
step(delta)
|
|
|
|
func step(dt: float) -> void:
|
|
_pick_state()
|
|
var tg: Dictionary = TARGETS[state]
|
|
var k := dt / BLEND_SECONDS
|
|
clouds = move_toward(clouds, tg.clouds, k)
|
|
precip = move_toward(precip, tg.precip, k * 1.2)
|
|
wind = move_toward(wind, tg.wind, k)
|
|
light = move_toward(light, tg.light, k * 0.8)
|
|
fog = move_toward(fog, tg.fog, k * 3.0)
|
|
warmth = move_toward(warmth, tg.warmth + (COLD_DAY_WARMTH if cold_day else 0.0), k * 20.0)
|
|
# The ground soaks in a couple of minutes of rain and dries over ten.
|
|
var raining := precip > 0.2 and not snowing_globally()
|
|
ground_wet = move_toward(ground_wet, 1.0 if raining else 0.0, dt / (120.0 if raining else 600.0))
|
|
# Snow settles over a few minutes and only melts on warm days.
|
|
var snowing := precip > 0.2 and snowing_globally()
|
|
if snowing:
|
|
snow_cover = move_toward(snow_cover, 1.0, dt * precip / 180.0)
|
|
elif not cold_day:
|
|
snow_cover = move_toward(snow_cover, 0.0, dt / 480.0)
|
|
flash = maxf(flash - dt * 6.0, 0.0)
|
|
if state == "storm" and precip > 0.7:
|
|
_strike_left -= dt
|
|
if _strike_left <= 0.0:
|
|
_strike_left = _rng.randf_range(7.0, 22.0)
|
|
flash = 1.0
|
|
lightning_count += 1
|
|
|
|
func snowing_globally() -> bool:
|
|
return state in ["snow", "blizzard"]
|
|
|
|
## Whether precipitation at `pos` falls as snow (cold day or high up).
|
|
func snows_at(pos: Vector3) -> bool:
|
|
return snowing_globally() or pos.y > snowline
|
|
|
|
## 0..1 rain intensity at `pos` (0 when it's snow there).
|
|
func rain_at(pos: Vector3) -> float:
|
|
return 0.0 if snows_at(pos) else precip
|
|
|
|
## Air warmth change from the weather. Altitude snow makes the peaks colder
|
|
## still when precipitation is falling up there.
|
|
func warmth_at(pos: Vector3) -> float:
|
|
var w := warmth
|
|
if precip > 0.2 and pos.y > snowline and not snowing_globally():
|
|
w -= 10.0 * precip
|
|
return w
|
|
|
|
func label() -> String:
|
|
var names := {"clear": "Clear", "cloudy": "Cloudy", "rain": "Rain", "storm": "Storm",
|
|
"snow": "Snow", "blizzard": "Blizzard"}
|
|
var s: String = names[state]
|
|
if cold_day and state in ["clear", "cloudy"]:
|
|
s += ", cold"
|
|
return s
|