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