149 lines
5.4 KiB
GDScript
149 lines
5.4 KiB
GDScript
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
|