class_name WeatherSystem extends Node ## Погода: текущий тип, плавные переходы и смена по игровому времени. ## ## Данные и правила, без отрисовки. Вид (surface_view.gd, sky_view.gd) читает ## отсюда `clouds`, `precipitation`, `fog` и `wind` и рисует по ним. ## ## Такт берётся у GameClock, поэтому погода стоит вместе с паузой и ускоряется ## вместе со скоростью игры — своего таймера здесь нет намеренно. enum Kind { CLEAR, CLOUDY, OVERCAST, RAIN, STORM, FOG, SNOW } ## Целевые параметры каждого типа. `clouds` — плотность облаков, `precipitation` ## — сила осадков, `fog` — дымка у горизонта, `wind` — скорость сноса. const KINDS := { Kind.CLEAR: {"label": "Ясно", "clouds": 0.12, "precipitation": 0.0, "fog": 0.0, "wind": 0.3}, Kind.CLOUDY: {"label": "Облачно", "clouds": 0.45, "precipitation": 0.0, "fog": 0.0, "wind": 0.5}, Kind.OVERCAST: {"label": "Пасмурно", "clouds": 0.85, "precipitation": 0.0, "fog": 0.06, "wind": 0.4}, Kind.RAIN: {"label": "Дождь", "clouds": 0.95, "precipitation": 0.55, "fog": 0.14, "wind": 0.7}, Kind.STORM: {"label": "Ливень", "clouds": 1.0, "precipitation": 1.0, "fog": 0.22, "wind": 1.0}, Kind.FOG: {"label": "Туман", "clouds": 0.3, "precipitation": 0.0, "fog": 0.62, "wind": 0.12}, Kind.SNOW: {"label": "Снег", "clouds": 0.9, "precipitation": 0.5, "fog": 0.24, "wind": 0.35}, } ## Куда погода может перейти и с каким относительным весом. Матрица, а не ## общий мешок: так дождь приходит через пасмурно, а не из ясного неба. 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: 3, Kind.SNOW: 2}, 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 WINTER_MONTHS: Array[int] = [11, 12, 1, 2] const MIN_DURATION := 120 const MAX_DURATION := 420 ## За сколько игровых минут параметры доходят до целевых. const BLEND_MINUTES := 45.0 ## Сменился тип погоды — для HUD и звука. signal changed(kind: int) var kind := Kind.CLEAR var clouds := 0.12 var precipitation := 0.0 var fog := 0.0 var wind := 0.3 var _minutes_left := MIN_DURATION 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) func label() -> String: return String(KINDS[kind]["label"]) ## Идёт ли снег, а не дождь — виду нужно рисовать разное. func is_snowing() -> bool: return kind == Kind.SNOW func save_data() -> Dictionary: return { "kind": int(kind), "minutes_left": _minutes_left, "clouds": clouds, "precipitation": precipitation, "fog": fog, "wind": wind, } 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)) clouds = float(data.get("clouds", 0.12)) precipitation = float(data.get("precipitation", 0.0)) fog = float(data.get("fog", 0.0)) wind = float(data.get("wind", 0.3)) changed.emit(kind) # --- Внутреннее ------------------------------------------------------------- func _on_minute_passed(_total_minutes: int) -> void: _blend_toward_target() _minutes_left -= 1 if _minutes_left > 0: return kind = _roll_next() _minutes_left = _rng.randi_range(MIN_DURATION, MAX_DURATION) changed.emit(kind) ## Параметры не прыгают вместе с типом: они доходят до цели за BLEND_MINUTES. func _blend_toward_target() -> void: _apply_target(1.0 / BLEND_MINUTES) 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) wind = lerpf(wind, float(target["wind"]), 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 _seasonal(candidate) return kind ## Снег уместен только зимой; в остальное время он превращается в дождь. func _seasonal(candidate: int) -> int: if candidate != Kind.SNOW: return candidate if _clock == null: return Kind.RAIN var month := int(_clock.now()["month"]) return Kind.SNOW if WINTER_MONTHS.has(month) else Kind.RAIN