Files
g-world/scripts/game/weather_system.gd
T

222 lines
8.7 KiB
GDScript

class_name WeatherSystem
extends Node
## Погода: тип, температура, ветер и плавные переходы между состояниями.
##
## Данные и правила, без отрисовки. Вид (surface_view.gd, sky_view.gd) читает
## отсюда `clouds`, `precipitation`, `fog` и `wind`, HUD — `temperature`
## и `wind_speed`. Норму по сезону даёт climate.gd.
##
## Такт берётся у GameClock, поэтому погода стоит вместе с паузой и ускоряется
## вместе со скоростью игры — своего таймера здесь нет намеренно.
enum Kind { CLEAR, CLOUDY, OVERCAST, RAIN, STORM, FOG, SNOW }
## Целевые параметры картинки для каждого типа. Температура и ветер сюда не
## входят: они зависят ещё и от сезона, их считает Climate.
##
## `key` — латинское имя для консоли: набирать русские названия с латинской
## раскладки неудобно, а переключаться посреди отладки — тем более.
const KINDS := {
Kind.CLEAR: {
"label": "Ясно", "key": "clear",
"clouds": 0.12, "precipitation": 0.0, "fog": 0.0,
},
Kind.CLOUDY: {
"label": "Облачно", "key": "cloudy",
"clouds": 0.45, "precipitation": 0.0, "fog": 0.0,
},
Kind.OVERCAST: {
"label": "Пасмурно", "key": "overcast",
"clouds": 0.85, "precipitation": 0.0, "fog": 0.06,
},
Kind.RAIN: {
"label": "Дождь", "key": "rain",
"clouds": 0.95, "precipitation": 0.55, "fog": 0.14,
},
Kind.STORM: {
"label": "Ливень", "key": "storm",
"clouds": 1.0, "precipitation": 1.0, "fog": 0.22,
},
Kind.FOG: {
"label": "Туман", "key": "fog",
"clouds": 0.3, "precipitation": 0.0, "fog": 0.62,
},
Kind.SNOW: {
"label": "Снег", "key": "snow",
"clouds": 0.9, "precipitation": 0.5, "fog": 0.24,
},
}
## Куда погода может перейти и с каким относительным весом. Матрица, а не
## общий мешок: так дождь приходит через пасмурно, а не из ясного неба.
const TRANSITIONS := {
Kind.CLEAR: {Kind.CLEAR: 4, Kind.CLOUDY: 5, Kind.FOG: 1},
Kind.CLOUDY: {Kind.CLEAR: 4, Kind.CLOUDY: 2, Kind.OVERCAST: 4, Kind.FOG: 1},
Kind.OVERCAST: {Kind.CLOUDY: 4, Kind.OVERCAST: 2, Kind.RAIN: 4, Kind.SNOW: 1},
Kind.RAIN: {Kind.RAIN: 2, Kind.STORM: 2, Kind.OVERCAST: 5},
Kind.STORM: {Kind.RAIN: 5, Kind.OVERCAST: 2},
Kind.FOG: {Kind.FOG: 2, Kind.CLEAR: 4, Kind.CLOUDY: 3},
Kind.SNOW: {Kind.SNOW: 3, Kind.OVERCAST: 4, Kind.CLOUDY: 2},
}
const MIN_DURATION := 120
const MAX_DURATION := 420
## За сколько игровых минут параметры доходят до целевых.
const BLEND_MINUTES := 45.0
## Насколько воздушная масса может отклонить температуру от нормы, °C.
const DRIFT_RANGE := 3.0
## Сменился тип погоды — для HUD и звука.
signal changed(kind: int)
var kind := Kind.CLEAR
var clouds := 0.12
var precipitation := 0.0
var fog := 0.0
## Ветер 0..1 для картинки; в метрах в секунду — wind_speed.
var wind := 0.15
var wind_speed := 2.5
var temperature := 8.0
var _minutes_left := MIN_DURATION
## Отклонение текущей воздушной массы от климатической нормы, °C.
var _temperature_drift := 0.0
var _rng := RandomNumberGenerator.new()
var _clock: GameClock
func _ready() -> void:
_rng.randomize()
_apply_target(1.0)
## Сцена связывает погоду с часами: без этого она стоит на месте.
func attach_clock(clock: GameClock) -> void:
_clock = clock
_clock.minute_passed.connect(_on_minute_passed)
# Прыжок во времени — не то же, что ход времени: догонять новый сезон
# плавно после переноса на полгода бессмысленно, берём норму сразу.
_clock.jumped.connect(_on_clock_jumped)
_apply_climate(1.0)
func label() -> String:
return String(KINDS[kind]["label"])
## Идёт ли снег, а не дождь — виду нужно рисовать разное.
func is_snowing() -> bool:
return kind == Kind.SNOW
# --- Ручное управление (консоль разработчика) -------------------------------
## Поставить конкретную погоду немедленно, минуя матрицу переходов.
func force_kind(value: int, hold_minutes := MAX_DURATION) -> void:
kind = clampi(value, 0, KINDS.size() - 1)
_minutes_left = hold_minutes
changed.emit(kind)
## Сдвинуть воздушную массу так, чтобы сейчас было примерно `celsius`.
## Значение держится до следующей смены погоды, а не жёстко фиксируется.
func nudge_temperature(celsius: float) -> void:
_temperature_drift += celsius - temperature
temperature = celsius
func set_wind_speed(speed: float) -> void:
wind_speed = maxf(speed, 0.0)
wind = clampf(wind_speed / Climate.WIND_REFERENCE, 0.0, 1.0)
# --- Сохранение -------------------------------------------------------------
func save_data() -> Dictionary:
# Облачность, осадки и дымку не пишем: это промежуточные значения перехода,
# и в слоте они противоречили бы названию погоды — туман с ливневыми
# осадками. При загрузке они берутся из типа напрямую.
return {
"kind": int(kind),
"minutes_left": _minutes_left,
"wind_speed": wind_speed,
"temperature": temperature,
"drift": _temperature_drift,
}
func load_data(data: Dictionary) -> void:
kind = clampi(int(data.get("kind", Kind.CLEAR)), 0, KINDS.size() - 1)
_minutes_left = int(data.get("minutes_left", MIN_DURATION))
temperature = float(data.get("temperature", 8.0))
_temperature_drift = float(data.get("drift", 0.0))
set_wind_speed(float(data.get("wind_speed", 2.5)))
# Картинка сразу соответствует названию погоды, без доблёкивания.
_apply_target(1.0)
changed.emit(kind)
# --- Внутреннее -------------------------------------------------------------
func _on_clock_jumped() -> void:
_apply_climate(1.0)
func _on_minute_passed(_total_minutes: int) -> void:
_apply_target(1.0 / BLEND_MINUTES)
_apply_climate(1.0 / BLEND_MINUTES)
_minutes_left -= 1
if _minutes_left > 0:
return
kind = _roll_next()
_minutes_left = _rng.randi_range(MIN_DURATION, MAX_DURATION)
# Новая погода — новая воздушная масса.
_temperature_drift = _rng.randf_range(-DRIFT_RANGE, DRIFT_RANGE)
changed.emit(kind)
## Параметры картинки не прыгают вместе с типом: они доходят до цели плавно.
func _apply_target(weight: float) -> void:
var target: Dictionary = KINDS[kind]
clouds = lerpf(clouds, float(target["clouds"]), weight)
precipitation = lerpf(precipitation, float(target["precipitation"]), weight)
fog = lerpf(fog, float(target["fog"]), weight)
## Температура и ветер идут к сезонной норме с поправкой на тип погоды.
func _apply_climate(weight: float) -> void:
if _clock == null:
return
var moment := _clock.now()
var target := Climate.base_temperature(int(moment["month"]), SkyCycle.hour_of(moment))
target += Climate.weather_offset(kind) + _temperature_drift
temperature = lerpf(temperature, target, weight)
set_wind_speed(lerpf(wind_speed, Climate.base_wind(kind), weight))
func _roll_next() -> int:
var options: Dictionary = TRANSITIONS[kind]
var total := 0
for weight: int in options.values():
total += weight
var pick := _rng.randi_range(1, total)
for candidate: int in options:
pick -= int(options[candidate])
if pick <= 0:
return _by_temperature(candidate)
return kind
## Форму осадков решает градусник, а не календарь: у нуля дождь становится
## снегом и наоборот. Так это можно проверить, подкрутив температуру.
func _by_temperature(candidate: int) -> int:
if candidate == Kind.SNOW and temperature > Climate.SNOW_THRESHOLD + 1.5:
return Kind.RAIN
if candidate == Kind.RAIN and temperature < Climate.SNOW_THRESHOLD:
return Kind.SNOW
return candidate