План: погода из прогноза — температура и ветер вместо пресетов

Запрос пилота (пилот-консультант, опытный дельтапланерист): выбор «Слабый / Средний / Сильный день / Гроза / Волна» для пилота странный. Пилот знает из прогноза температуру воздуха, направление и силу ветра — это и задаём. Всё остальное (сила и частота термиков, кромка, облачность, сухие термики, пылевые вихри, грозы, болтанка…) выводится из этих трёх чисел плюс уже известных даты, места и рельефа — правдоподобно физически.

Дополнение пилота: слово «волна» ей незнакомо, в её практике его не используют. Поэтому «волны» нигде в интерфейсе нет (ни пресета, ни подписи, ни текста на планшете). Физика подветренной волны остаётся только скрытым эффектом (сильный ветер поперёк хребта + устойчивый воздух) и по умолчанию выключена конфигом (см. §2.8, вопрос 2). Роторы и «прижим» за хребтом (подветренная зона) остаются — их знает каждый пилот.

Статус: 28.09.2026 пилот ответила на вопросы §6 — план поправлен (облачность, ветер на старте и с высотой, опасный ротор, 0…+40 °C). Ход реализации — коммиты «WeatherModel…», «Настройки полёта…», «Погода по солнцу…». Было: план, код не менялся. Параллельно другой агент переводит интерфейс на английский (scripts/ui/*, locale/ui.csv, конфиги) — карточки 4–5 начинать после его слияния.


0. Что есть сейчас (коротко)

  • configs/weather/{weak,medium,strong,storm,wave}.json — пресеты: ~25 полей (wind_speed_kmh, wind_from_deg, cloudbase_agl_m, thermal_strength_ms, thermal_radius_m, thermal_spacing_m, thermal_duty, background_sink_ms, convective_turbulence_ms, cloud_min_strength_ms, cloud_depth_m, street_strength, overdevelopment_chance, cloud_size_factor, cirrus_cover, cb_chance, cb_top_above_base_m, wave_strength, stability_n_per_s, lens_level_above_crest_m, dry_thermal_fraction, thermal_extreme_chance, thermal_extreme_ms, dust_devil_chance, thermal_mode, static_thermals, name).
  • Atmosphere.set_weather(имя | словарь) / configure() — всё дальше читает только этот словарь (thermal_field, wave_field, dust_model, cirrus_layer, cloud_layer, calm_air). Значит, новый вывод погоды может просто собирать такой же словарь — физику атмосферы не трогаем.
  • Ветер задан на 10 м (atmosphere.json → wind.reference_height_m), выше — степенной профиль до ×1,8.
  • Кромка: cloudbase_agl_m над средней высотой земли в радиусе 10 км (ground.reference_*).
  • FlightSettings.weather (“weather/medium”) и wind_mode (“into_site” | “preset”) хранятся в user://last_flight.json; UserSettings.load_last_flight сбрасывает неизвестный пресет.
  • Game.start: рельеф грузится до air.set_weather(...); при into_site после выбора старта set_wind(сила пресета, курс старта).
  • Термики не зависят от времени суток (решение пользователя, docs/guide/game.md → «Время суток»); солнце для источников — статичное. Это сохраняем: модель описывает «день в разгар».
  • Бот-пилоты (bot_pilot.gd, bots.json) и задания/тренировки (configs/tasks/*) погоду не читают. Планшет (страница ветра) оценивает ветер по GPS — от погоды не зависит.
  • Пресеты по имени используют тесты и инструменты: test_atmosphere, test_calibration, test_pilot_words, test_hazards, test_wave, test_clouds_and_generation, test_birds, test_cloud_*, test_ridge_starts, test_strong_wind_launch, test_input_launch, test_matrix, test_game_logic, test_menu_setup, xc/xc_run.gd, tools/atmosphere/xc_matrix.sh, tools/flight/roll_sway.gd, tools/shots/cloud_cut_shot.gd, tools/soak.sh, atmosphere_preview, world_objects_preview, launch_options --weather=.

1. Интерфейс «Полёт…»

Строка «Погода» (выпадающий список пресетов) и «Ветер: встречный / из пресета» удаляются. Вместо них три строки (после «Масса пилота», до «Время старта»):

СтрокаЭлементДиапазон, шагПодпись значения
Температура днёмHSlider (UiKit.slider_row)0…+40 °C, шаг 1 (пилот, 28.09)«+26 °C»
(подсказка)HintLabel под ползунком—«Обычно днём в этом месяце: +25 °C» — обновляется при смене месяца
ВетерHSlider0…12 м/с, шаг 1«3 м/с (11 км/ч)»
Откуда ветерOptionButton«Встречный», С, СВ, В, ЮВ, Ю, ЮЗ, З, СЗпри выбранной площадке рядом подсказка «встречный для этого старта — З»
ОблачностьOptionButtonЯсно / Переменная / Облачно (пилот, 28.09)—

Почему так:

  • Температура — дневной максимум по прогнозу у земли в долине (на посадке) («днём до +26») — то, что пилот видит в прогнозе. Подпись сразу «Температура днём», чтобы в фазе 2 (ход дня, §7) смысл не менялся: утром модель сама возьмёт прохладнее, чем максимум. Ползунок, а не поле ввода: мышью/стрелками, как масса и время. Смена месяца не двигает ползунок (выбор пилота важнее), только подсказку «обычно…» (таблица typical_max_c по месяцам). По умолчанию (первый запуск, нет сохранённого) — typical_max_c для даты по умолчанию (15 июля → +26).
  • Ветер в м/с: в русских прогнозах и у наших пилотов ветер в м/с; км/ч — в скобках (в игре везде км/ч). Хранится wind_speed_kmh = м/с × 3,6. 0…12 м/с ≈ 0…43 км/ч — выше дельтапланеристы не летают. Шаг 1 м/с (точнее прогноз всё равно не даёт). Вопрос 1 — в каких единицах привычнее.
  • Направление — 8 румбов + «Встречный», а не ползунок в градусах: прогноз даёт румб («ЮЗ 4 м/с»), 16 румбов избыточны (на игру влияет только угол к склону, а «в лоб» покрывает главный случай). «Встречный» — по умолчанию и первым пунктом (как сейчас). При ветре 0 список неактивен. Внутри: wind_into_launch: bool + wind_from_deg: float (0, 45, … 315).
  • Облачность (пилот, 28.09): «Ясно / Переменная / Облачно», по умолчанию «Ясно». Влажность пилот сочла ненужной — не добавляем. Ожидаемую кромку/силу термиков в меню не показываем — пилот читает день по небу и вариометру (правило «без подсказок», FR-22).

Сводка в главном меню (start_menu.gd, сейчас «Средний день · 13:00») → «+26 °C · ветер 3 м/с, встречный · 13:00» (или «… ветер 3 м/с, ЮЗ · 13:00»; при 0 — «штиль»).

Сохранение и перенос старых настроек

29.09.2026: перенос убран — пользователей нет: legacy_presets, WeatherModel.legacy_forecast, FlightSettings.set_legacy_weather, чтение weather/wind_mode из last_flight.json, флаги --weather=<id> и --wind=into_site|preset удалены; тесты и инструменты задают прогноз числами (--temp/--wind/--from). Эталоны configs/weather/* остались (калибровка, превью, xc_run).

FlightSettings: поля weather, wind_mode → temperature_c: float, wind_speed_kmh: float, wind_into_launch: bool, wind_from_deg: float. from_dict: если в словаре нет temperature_c, но есть weather — взять прогноз из таблицы legacy_presets конфига (§3): weak → +20 °C / 2 м/с, medium → +26 / 3, strong → +31 / 5, storm → +34 / 4, wave → +18 / 10 (волна больше не выбирается); wind_mode == "preset" → wind_into_launch = false, wind_from_deg = 270 (из старого пресета), иначе true. Проверка в UserSettings.load_last_flight: температура/ветер — зажать в диапазон (вместо «неизвестный пресет → по умолчанию»). Значения по умолчанию — configs/game.json: default_temperature_c (или null → typical_max_c по дате), default_wind_speed_kmh (11), default_wind_into_launch (true), default_wind_from_deg (270); default_weather/default_wind_mode удалить.


2. Модель: из прогноза — в погодный словарь

Новый класс WeatherModel (scripts/atmosphere/weather_model.gd, RefCounted, только статические чистые функции, без узлов):

# forecast: {temperature_c, wind_speed_kmh, wind_from_deg}
# ctx: {month, day, lat, valley_msl_m, mean_msl_m}   (из рельефа; для тестов — reference_context из конфига)
static func derive(forecast: Dictionary, ctx: Dictionary, cfg: Dictionary, hour: float = NAN) -> Dictionary
# hour = NAN — «разгар дня» (фаза 1); фаза 2 передаёт час и получает день в этот момент (§7)
static func ground_context(height_fn: Callable, radius_m, samples, low_percentile) -> Dictionary
static func legacy_forecast(preset_id: String, cfg) -> Dictionary     # "medium" → {26, 11, 270}

derive возвращает словарь с теми же ключами, что пресет (+ служебный _derived с промежуточными величинами для тестов и tools/weather/derive_table.gd). Детерминированно: одни входы → один день (сид атмосферы и так постоянный, atmosphere.json → seed).

2.1 Скрытые допущения (всё в configs/weather_model.json)

  1. Температура T — дневной максимум у земли в долине. Высота долины h_v — нижние 10 % высот рельефа в радиусе 10 км (ground_context, тот же радиус и сетка, что для отсчёта кромки); средняя высота h_m — как сейчас. В перемешанном слое воздух сухоадиабатический, поэтому верх термиков и кромка над уровнем моря не зависят от того, на какой высоте «измерена» температура — достаточно одной точки.
  2. Воздух наверху — климатический для месяца: температура T_u(месяц) на z_u = 3000 м н. у. м. (≈ 700 гПа), таблица по месяцам для 50–56° с. ш. (Алтай и Башкирия одной таблицей). Выше перемешанного слоя градиент Γ_env = 4 К/км (умеренно устойчивый воздух хорошей погоды). То есть жарче у земли при том же воздухе наверху = неустойчивее = выше и сильнее термики. Весной воздух наверху холодный — и +15 °C в апреле даёт хороший термический день (как в жизни: «весенние термики злые»).
  3. Влажность — обычная для месяца: точка росы Td(месяц) постоянна в течение дня (таблица по месяцам). Кромка ≈ 122 м × (T − Td) — правило, которое пилоты знают.
  4. Перегрев пузыря над окружающим воздухом у земли δ = 1 К.
  5. Солнце: только сезон — высота солнца в полдень выбранной даты на широте места; от часа старта термики не зависят (прежнее решение пользователя). Облачность/пелену не выбираем.
  6. Ветер — на старте (10 м над ним; пилот, 28.09), одинаковое направление на всех высотах; профиль над рельефом — существующий, плюс рост с высотой над стартом (wind.altitude_gain_per_km, §6 п. 2).
  7. Перистая пелена — небольшая постоянная, растёт при грозах (остатки наковален).
  8. Волна — по умолчанию выключена (wave.enabled: false), см. §2.8.

2.2 Высота термиков и кромка

Все высоты ниже — в км; Δ = z − h_v (над долиной).

Пузырь (сухая адиабата):     T_p(z) = T + δ − 9,8·(z − h_v)
Окружающий воздух:           T_e(z) = T_u + Γ_env·(z_u − z)
Верх сухих термиков:         z_dry = h_v + (T + δ − T_u − Γ_env·(z_u − h_v)) / (9,8 − Γ_env)
Уровень конденсации (кромка): z_lcl = h_v + 0,122·(T − Td)
Запас на облако:             m = z_dry − z_lcl            (м)
Верх подъёма:                z_top = min(z_dry, z_lcl)
cloudbase_agl_m = clamp(z_top − h_m, 300, 3800)           (над средней землёй, как сейчас)
top_agl = cloudbase_agl_m                                  (ключ для таблиц силы, §2.3)
  • m < −100 м — «голубой» день: пузыри не доходят до конденсации, облаков нет (cloud_min_strength_ms = 99), термики упираются в z_dry (сухие термики, искать по вариометру).
  • m ≥ 0 — кучевые, кромка на z_lcl, облака тем мощнее, чем больше m (§2.4).
  • Устойчивость для волны: N = √(g/T_K · (9,8 − Γ_env)/1000) ≈ 0,0145 1/с (пресет «волна» был 0,012).

Числа для опорного места (Алтай, июль: h_v ≈ 0,4 км, h_m ≈ 0,9 км — уточнить в карточке 1 инструментом derive_table), T_u(июль) = +2 °C, Td(июль) = +8 °C, Γ_env = 4, δ = 1:

T у землиz_dry, кмz_lcl, кмm, мкромка над средней землёйдень
+120,50,9−400300 (минимум)почти мёртвый, голубой
+161,21,4−200~300–500слабые сухие термики
+201,871,86≈ 0~960≈ слабый (редкие облачка)
+262,92,6300~1700≈ средний
+313,763,21550~2300≈ сильный, переразвитие
+344,33,57700~2650грозовой: Cb заметно часто

Рост кромки ≈ 120–140 м на градус — как у пилотов («+1 °C разницы с точкой росы — +120 м»). Башкирия (h_v ≈ 0,35, h_m ≈ 0,55) при той же температуре — кромка над землёй на ~300 м выше (ниже рельеф) — правдоподобно.

2.3 Сила, размер и частота термиков — таблица опор по top_agl

Физический смысл — масштаб Дирдорфа w* = (g/T · H0 · zi)^(1/3): выше слой и сильнее прогрев → сильнее и шире термики, реже источники (Allen: шаг ∝ zi). Но куб. корень слишком плоский против наших откалиброванных пресетов, поэтому берём кусочно-линейную таблицу опор anchors_by_top_agl, строки которой — ровно значения старых пресетов (калибровка карточки 03 сохраняется):

top_agl, мstrength, м/сradius, мspacing, мdutyфон, м/сболтанка σcloud_sizeextreme
300[0,4; 1,0][40; 80]22000,5−0,30,251,00
1000 (weak)[1,2; 2,5][60; 110]15000,7−0,40,41,00
1700 (medium)[2,4; 4,4][90; 170]8000,85−0,50,80,80
2300 (strong)[2,5; 5,0][110; 210]7000,9−0,71,30,90,04
3200[3,0; 5,5][120; 230]7000,9−0,81,51,00,06

Поправки к таблице:

  • Солнце (сезон): sun_k = (sin el_полдень / sin el_ref)^0,5, el_ref = 59,5° (15 июля, 52° с. ш.), зажать [0,5; 1,05]. Сила × sun_k, duty × sun_k^0,5. Апрель ≈ 0,93, январь ≈ 0,58.
  • Ветер рвёт термики (wind_thermal_factor по U10, км/ч): 0–15 → 1,0; 25 → 0,85; 35 → 0,65; 45 → 0,5 — на силу; duty × (1; 1; 0,9; 0,75; 0,6). Механическая болтанка, наклон и снос термиков, склон, подветренные роторы уже считаются в атмосфере от ветра — их не дублируем.
  • Облачные улицы: street_strength = 0,7 при кучевых, 0,4 в голубой день (улицы сухих термиков); включаются ли — уже решает код по ветру (thermal.street_min/full_wind_ms: при 3 м/с у земли на половине кромки ≈ 5 м/с — частичные улицы, при 5+ м/с — полные).
  • Пылевые вихри: dust_devil_chance по top_agl: 1000 → 0; 1700 → 0,15; 2300 → 0,3 × ветровой множитель (0–15 км/ч → 1; 25 → 0,3; 30+ → 0 — ветер их сдувает).

2.4 Облака — по запасу m

m, мdry_thermal_fractioncloud_depth_moverdevelopmentcb_chancecb_thermal_chance
< −1001,0 (облаков нет, cloud_min_strength_ms = 99)—000
00,55300000
1500,35500000
3000,25700000
4500,210000,1500
5500,1513000,20,050
600(линейно 550→750)0,1250
700(линейно 550→750)0,2750,0001
7500,116000,450,350,0004
800 / 900 / 11000,116000,450,350,001 / 0,0025 / 0,005
  • Грозы редкие (решение пилота, 29.09.2026; было: cb_chance 0,2 уже при +26 — десятки Cb в час вокруг старта). cb_chance теперь — «грозовость дня» (клеточный день, наковальни в пелене, см. ниже), а доля сильных термиков, которые правда становятся Cb, — cb_thermal_chance (× sky.cb_k). Вокруг старта (15 км) за час рождаются ~200–450 сильных термиков, так что 0,0001 ≈ одна гроза на 20–50 полётов. Онгудай, 100 сидов (tools/weather/storm_rarity.tscn): +26 — Cb ближе 15 км за первый час в 0–1 % полётов, буря у старта в начале — 0 %; +30 — Cb в 20–40 %, у старта 0–4 %; +34 — Cb в 50–70 % (~1 в час), у старта в начале 3–6 %. На низких локациях (Аскарово, Аушкуль, «Алтай») запас меньше — грозы там начинаются лишь к +34…+38.

  • cloud_min_strength_ms — 1,0 (как medium) при m ≥ −100.

  • cb_top_above_base_m = tropopause_msl_m (11 000) − z_lcl.

  • Гроза делает день «клеточным»: thermal_spacing_m × (1 + 1,3·cb_chance), cloud_size_factor + cb_chance, background_sink_ms − 0,3·cb_chance, cirrus_cover = 0,1 + 0,5·cb_chance, thermal_extreme_chance

    • 0,05·cb_chance.
  • Гроза детерминирована: какие именно термики станут Cb — как сейчас, по сиду клетки (cb_thermal_chance — доля). В июле в Онгудае Cb начинаются примерно с +28…+30 °C, при +34 — ≈ 0,5 % сильных термиков (гроза где-то в округе — в большинстве полётов, над стартом — редко). Весной тот же запас получается при меньшей температуре (холодный воздух наверху) — весенние ливни из Cb.

2.5 Опорные прогнозы ≈ старые пресеты (опорное место, июль)

Прогноз≈ старый пресетЧто отличается
+20 °C, 2 м/сweakкромка ~960 (было 1100)
+26 °C, 3 м/сmediumкромка ~1700 (было 1800)
+31 °C, 5 м/сstrongкромка ~2300 (было 2600)
+34 °C, 4 м/сstormкромка ~2650 (было 1700): постоянная точка росы даёт «сухие» высокие грозы; если пилот скажет, что грозы с низкой кромкой — поднять Td при жаре (td_follow_heat, по умолчанию 0)
+18 °C, 10 м/с (+ wave.enabled)waveтолько для тестов волны

Расхождения кромки ±15 % — «достаточно хорошо»; T_u, Td, Γ_env подгоняются в калибровке карточки 1, если пилот скажет, что опоры не те (вопрос 3).

2.6 Холод и зима

Ниже m < −100 и top_agl 300 — «мёртвый» день: слабые пузыри у склонов, полёт — спуск или склон при ветре. Это честно (ноябрьский полёт так и выглядит). Облака не рисуются — небо чистое.

2.7 Что остаётся постоянным

thermal_mode = “dynamic”, static_thermals = [] (статичные ставит локация, как сейчас), thermal_extreme_ms = [7; 9], lens_level_above_crest_m = 2200. Поле name больше не нужно (сводка меню строится из прогноза).

2.8 Волна (скрыто, по умолчанию выкл.)

wave.enabled = false → wave_strength = 0 всегда. Если включить (после ответа пилота, вопрос 2): wave_strength = max_strength (0,5) × clamp((U10 − 24)/16, 0, 1) × clamp((1700 − top_agl)/700, 0, 1) — только сильный ветер (≥ 7 м/с у земли, ≥ 12 м/с на 1,5 км) и слабая конвекция (устойчивый воздух); сильные термики ломают волновую картину. stability_n_per_s = N из §2.2. Подветренная зона (прижим, роторная болтанка, ослабленный ветер за хребтом — atmosphere.json → lee) работает всегда, от ветра.


3. Конфиги

Новый configs/weather_model.json (все числа с _doc, по-русски):

{
  "_doc": "Погода из прогноза (FR-16): температура + ветер → словарь погоды (как бывшие пресеты). scripts/atmosphere/weather_model.gd, docs/guide/atmosphere.md → «Погода из прогноза».",
  "ui": { "temperature_c": [-10, 40, 1], "wind_ms": [0, 12, 1], "compass_points": 8 },
  "typical_max_c":  [-12,-10,-2,9,18,23,26,23,16,7,-3,-9],   // подсказка и значение по умолчанию, по месяцам
  "upper_air": { "z_msl_m": 3000, "temp_c": [-17,-16,-12,-7,-2,1,2,1,-3,-8,-13,-16], "lapse_k_per_km": 4.0 },
  "dew_point_c": [-20,-19,-12,-4,1,6,8,7,2,-3,-10,-17],
  "td_follow_heat": 0.0,
  "parcel_excess_k": 1.0,
  "lcl_m_per_k": 122,
  "valley_percentile": 0.1,
  "cloudbase_agl_clamp_m": [300, 3800],
  "tropopause_msl_m": 11000,
  "sun": { "ref_elevation_deg": 59.5, "exponent": 0.5, "clamp": [0.5, 1.05] },
  "anchors_by_top_agl": [ {"top_agl_m": 300, ...}, {"top_agl_m": 1000, ...}, ... ],   // §2.3
  "clouds_by_margin": [ {"margin_m": -100, ...}, ... ],                               // §2.4
  "blue_margin_m": -100,
  "wind_factors": { "wind_kmh": [0,15,25,35,45], "strength": [1,1,0.85,0.65,0.5], "duty": [1,1,0.9,0.75,0.6], "dust": [1,1,0.3,0,0] },
  "street_strength": { "cumulus": 0.7, "blue": 0.4 },
  "dust_by_top_agl": { "top_agl_m": [1000,1700,2300], "chance": [0,0.15,0.3] },
  "storm": { "spacing_k": 1.3, "cloud_size_k": 1.0, "sink_k": -0.3, "cirrus_k": 0.5, "extreme_k": 0.05 },
  "cirrus_base": 0.1,
  "wave": { "enabled": false, "max_strength": 0.5, "min_wind_kmh": 24, "full_wind_kmh": 40, "max_top_agl_m": 1700, "fade_top_m": 700 },
  "constants": { "thermal_extreme_ms": [7,9], "cloud_min_strength_ms": 1.0, "lens_level_above_crest_m": 2200 },
  "reference_context": { "month": 7, "day": 15, "lat": 51.9, "valley_msl_m": 400, "mean_msl_m": 900 },
  "legacy_presets": {
    "weak":   {"temperature_c": 20, "wind_speed_kmh": 7,  "wind_from_deg": 270},
    "medium": {"temperature_c": 26, "wind_speed_kmh": 11, "wind_from_deg": 270},
    "strong": {"temperature_c": 31, "wind_speed_kmh": 18, "wind_from_deg": 270},
    "storm":  {"temperature_c": 34, "wind_speed_kmh": 14, "wind_from_deg": 270},
    "wave":   {"temperature_c": 18, "wind_speed_kmh": 36, "wind_from_deg": 270}
  }
}

(Имя weather_model.json, а не weather.json: рядом каталог configs/weather/, Config.get_config("weather") и list_configs("weather") путались бы.)

configs/weather/*.json остаются как эталоны для тестов физики (слова пилота, калибровка, гроза, волна, пыль — эти тесты проверяют атмосферу при известном словаре и не должны ломаться при подгонке вывода). В меню не показываются. В _doc каждого: «Эталон для тестов и калибровки; в игре погода выводится из прогноза (configs/weather_model.json), опорный прогноз — legacy_presets.». Поле name убрать (не нужно). atmosphere.json → default_weather = “weather/medium” остаётся (превью, тесты без рельефа). game.json: default_weather, default_wind_mode → default_temperature_c, default_wind_speed_kmh, default_wind_into_launch, default_wind_from_deg.


4. Что затронуто

ГдеЧто меняется
scripts/atmosphere/weather_model.gdновый: derive, ground_context, legacy_forecast, typical_max_c(month)
scripts/atmosphere/atmosphere.gdбез изменений логики; set_weather принимает словарь (уже умеет). Опционально set_weather("forecast:…") не нужно
scripts/game/game.gdпосле загрузки рельефа: ctx = WeatherModel.ground_context(terrain.height_at, …) + дата/широта → air.set_weather(WeatherModel.derive(settings.forecast(), ctx, cfg)); ветер: into_launch → set_wind(settings.wind_speed_kmh, _start_heading), иначе направление из настроек (уже в словаре)
scripts/game/calm_air.gdничего (берёт словарь)
scripts/game/flight_settings.gdновые поля, forecast(), перенос из weather/wind_mode
scripts/game/user_settings.gdзажим диапазонов вместо проверки пресета
scripts/game/launch_options.gd--temp=<°C> --wind=<м/с> --from=<град|launch>; --weather=<id> оставить → legacy_forecast (скрипты, xc_matrix); старый --wind=into_site|preset → --from
scripts/ui/flight_setup_screen.gdтри строки §1, подсказки
scripts/ui/start_menu.gdсводка из прогноза
locale/ui.csvключи ниже; удалить setup_weather, setup_wind_from_preset, weather_weak/medium/strong/wave (+ «Гроза», если есть)
Бот-пилоты, задания, тренировкине читают погоду — без изменений. Тренировка «вдоль склона» требует ветра — пилот ставит сам (не подсказываем)
Планшет / страница ветраоценка по GPS — без изменений; слова «волна» там нет и не появляется
Загрузкаэтап «Погода и термики…» — как есть (вывод мгновенный, < 1 мс)
atmosphere_preview, world_objects_preview, tools/shots/*добавить --temp/--wind/--from (через derive с reference_context), --weather= оставить
Тестысм. карточку 6
ДокументыREQUIREMENTS FR-16 и VR-27, docs/guide/atmosphere.md, docs/guide/game.md, CHANGELOG.md

Ключи перевода (схема область_что, ru / en)

Ключruen
setup_temperatureТемпература днёмDaytime high
setup_temperature_value%+d °C%+d °C
setup_temperature_typicalОбычно днём в этом месяце: %+d °CTypical daytime high this month: %+d °C
setup_wind_speedВетерWind
setup_wind_speed_value%d м/с (%d км/ч)%d m/s (%d km/h)
setup_wind_calmштильcalm
setup_wind_fromОткуда ветерWind from
setup_wind_into_launch (уже есть)ВстречныйHeadwind
setup_wind_launch_facesвстречный для этого старта — %sheadwind for this launch: %s
setup_skyОблачностьCloud cover
setup_sky_clear / _partly / _overcastЯсно / Переменная / ОблачноClear / Partly cloudy / Overcast
compass_n … compass_nw (8)С, СВ, В, ЮВ, Ю, ЮЗ, З, СЗN, NE, E, SE, S, SW, W, NW
menu_summary_forecast%+d °C · ветер %s%+d °C · wind %s
menu_summary_wind%d м/с, %s%d m/s, %s

(Существующий setup_wind «Ветер» можно переиспользовать вместо setup_wind_speed.)


5. Карточки

Порядок: 1 → 2 → 3 → (4, 5 после слияния английского перевода) → 6 → 7. Коммит каждой — git commit -- <свои пути>. Все запуски Godot агентами — с временным XDG_DATA_HOME (не трогать профиль пилота).

1. WeatherModel + конфиг + таблица для пилота — M, Opus

Владения: scripts/atmosphere/weather_model.gd, configs/weather_model.json, tests/atmosphere/test_weather_model.gd, tools/weather/derive_table.gd(.tscn). Шаги: формулы §2 (интерполяция таблиц — общий хелпер, как SunClock._lerp_f); ground_context по height_fn (сетка reference_samples², процентиль); derive_table печатает для 4 локаций × T {−5, 10, 15, 20, 26, 31, 34, 38} × ветер {0, 3, 5, 7, 10} м/с × месяцы {4, 7, 9}: h_v, h_m, z_dry, z_lcl, m, кромку, силу, шаг, долю сухих, Cb, пыль — и пишет ту же таблицу простым языком в tmp_weather_table.md (для пилота). Уточнить reference_context реальными h_v/h_m Алтая. Приёмка (headless-тест):

  • опорные прогнозы §2.5 в reference_context дают: thermal_strength_ms в ±20 % пресета, cloudbase_agl_m в ±20 %, dry_thermal_fraction ±0,12, background_sink_ms ±0,15; storm — cb_chance 0,2…0,45; medium/weak — cb_chance = 0, thermal_extreme_chance = 0; strong — extreme 0,02…0,06;
  • монотонность по T (шаг 1 °C, 10…34): кромка и верх силы не убывают;
  • по ветру (0…45 км/ч): сила не растёт, dust_devil_chance = 0 при ≥ 30 км/ч;
  • голубой день: T = +16, июль → cloud_min_strength_ms ≥ 50; T = +12 → кромка = 300;
  • апрель +15 °C сильнее июля +15 °C (кромка выше);
  • wave_strength = 0 при любом входе, пока wave.enabled = false; при true и 10 м/с, +18 °C — > 0;
  • детерминизм: два вызова — равные словари; все ключи пресета присутствуют и числовые;
  • время derive < 1 мс, ground_context < 20 мс.

2. Настройки, перенос, командная строка — S, Sonnet

Владения: flight_settings.gd, user_settings.gd, launch_options.gd, configs/game.json (ключи погоды), tests/game/test_game_logic.gd (часть про настройки). Приёмка: старый last_flight.json {weather: "weather/strong", wind_mode: "preset"} → +31 °C, 18 км/ч, wind_into_launch = false, 270°; {weather: "weather/wave"} → +18 °C, 36 км/ч; неизвестный пресет → значения по умолчанию; мусорные числа зажаты в диапазон; to_dict/from_dict — туда-обратно без потерь; --temp=31 --wind=5 --from=launch и --weather=medium работают.

3. Подключение в игре — S, Sonnet

Владения: game.gd (участок start), calm_air.gd при необходимости, atmosphere.md раздел «Погода из прогноза». Приёмка: tests/game — полёт с +26/3 м/с на altai даёт в air.weather кромку в ±10 % от derive с ctx этого рельефа; «в лоб» — wind.from_deg = курс старта; румб — заданный; точка с карты работает (ctx от загруженного вокруг точки рельефа).

4. Экран «Полёт…» и сводка меню — S, Sonnet (после слияния перевода)

Владения: flight_setup_screen.gd, start_menu.gd, locale/ui.csv (ключи §4), tests/ui/test_menu_setup.gd. Приёмка: нет строк «Погода» и «Направление из пресета»; нигде нет слова «волна/wave» (grep -ri "волн\|wave" locale scripts/ui — пусто); смена месяца меняет подсказку, но не ползунок; ветер 0 → список направлений неактивен; «Готово» → FlightSettings с выбранными числами; подписи на ru и en (переключение языка) влезают в панель без обрезки; скриншот экрана (ru и en) — в отчёт.

5. Тесты и инструменты на прогноз — M, Sonnet

Владения: tests/game/test_input_launch.gd, tests/stability/test_matrix.gd, tests/flight/test_strong_wind_launch.gd, tests/atmosphere/xc/xc_run.gd, tools/atmosphere/xc_matrix.sh, tools/flight/roll_sway.gd, tools/shots/*, tools/soak.sh, atmosphere_preview.gd, world_objects_preview.gd. Шаги: матрицы старта/стабильности — по прогнозам {+20/2, +26/3, +31/5 м/с} вместо трёх пресетов (плюс один прогон +34/4 и +15/8 на стабильность); превью и кадры — --temp/--wind/--from; --weather= оставить. Физические тесты (test_pilot_words, test_calibration, test_hazards, test_wave, облака, птицы, склон) не трогать — они на эталонах configs/weather/*. Приёмка: весь набор godot --headless --path . res://tests/run_tests.tscn зелёный; xc_matrix.sh с XC_WEATHERS="medium strong" работает как раньше.

6. Кадры в реальном рендере — S, Sonnet

Инструмент tools/shots/forecast_shot.gd (по образцу itch_shot): Алтай, Синюха-запад, 13:00, 15 июля, вид со старта на долину и вид сверху на 1 км выше старта (облака и тени). Матрица: T {15, 25, 32} × ветер {5, 20, 35 км/ч ≈ 1, 6, 10 м/с} = 9 кадров + 2 весенних (апрель +15, +25). Приёмка (глазами + лог _derived рядом с кадром): +15 — голубое небо или редкие клочья; +25 — кучёвка 2–4 окт.; +32 — крупные кучевые, переразвитие, при слабом ветре пыльные вихри; 35 км/ч — улицы вытянуты по ветру, облака рваные, меньше; ни одного лентикуляра (волна выкл.). Принятые кадры — в build/screenshots с русскими нумерованными именами («01_плюс15_ветер1.png» …).

7. Документы и вопросы пилоту — S, Sonnet

REQUIREMENTS.md: FR-16 → «Погода задаётся как в прогнозе: температура воздуха, сила и направление ветра (или встречный на старте); сила и частота термиков, кромка, облачность, сухие термики, пылевые вихри и грозы выводятся из них, даты и места». VR-27 → «(скрытый эффект сильного ветра поперёк хребтов, по умолчанию выключен)». docs/guide/atmosphere.md (раздел «Погода из прогноза» с формулами и таблицами §2, строки 12 и 356 про пресеты), docs/guide/game.md (экран «Полёт…»), CHANGELOG.md, вопросы §6 — в questions.md простым языком + таблица из карточки 1 (что получается при каких числах).


6. Вопросы пилоту — ответы (28.09.2026)

Ответы пилота (через пилота-консультанта) и что из них сделано:

  1. Ветер — в м/с, и это ветер на старте (не на 10 м в долине). → Ползунок в м/с (км/ч в скобках). Профиль ветра опирается на старт: Game передаёт высоту старта Atmosphere.set_wind(скорость, курс, ref_msl); на старте (10 м над землёй) — ровно ветер прогноза.

  2. Ветер растёт с высотой: в горах Алтая на ~1 км выше старта ветер до 15 м/с — обычное дело, «никуда не улетишь». → atmosphere.json → wind.altitude_gain_per_km (0,6), altitude_min/max_factor (0,5 / 2,0): множитель 1 + 0,6 × (высота над стартом, км) поверх профиля над рельефом (×1,8). 5 м/с на старте → ~14 м/с на 1 км выше; в долине ниже старта ветер слабее.

  3. Волну пилот не знает — остаётся выключенной (wave.enabled: false), в интерфейсе её нет. За хребтом (подветренная сторона) — смертельно опасно: ротор бьёт сверху сразу, туда нельзя. → Подветренное опускание и роторная болтанка усилены (растут с ветром поперёк хребта, при слабом ветре мягкие), ротор у подветренного склона с обратным потоком — карточка «ротор», без подсказок.

  4. Кромка в грозовой день зависит от температуры и высоты инверсии. → Кромка Cb выводится так же, как у кучевых: уровень конденсации по разнице T − Td, ограниченный верхом перемешанного слоя (инверсией): min(z_dry, z_lcl); утром — ещё и приземной инверсией (§7).

  5. Нужна облачность: «ясно / переменная / облачно» (en: clear / partly cloudy / overcast). Облачно — прогрев на порядок слабее и мягче, мало слабых термиков; переменная — между. Влажность не нужна. → Четвёртая строка экрана «Облачность» (FlightSettings.sky, weather_model.json → sky): пелена (cirrus_cover) ослабляет прогрев в атмосфере, плюс мягкость, меньше рождений, без гроз под облачностью.

  6. Температура 0…+40 °C (снег лежащий не рисуем — ниже нуля не нужно). +15 °C весной — хорошие термики (проверено тестом: апрель +15 на Алтае — верх ~1,8 км, 2–4 м/с). Зимой термики бывают — от прогретых солнцем поверхностей (лес, край белого и зелёного), но без снега на рельефе это только заметка к фазе 2 (§7.3 п. 10).

  7. Температура в меню — дневной максимум по прогнозу (пилот подтвердила 28.09.2026). Модель сама считает температуру на каждый час по суточному ходу (фаза 2, §7.2: Parton–Logan от максимума, утренняя инверсия) — WeatherModel.diurnal_state, weather_model.json → diurnal.

Открытым остаётся только общее «похоже ли на правду» по опорам (июль: +20 — слабо, +26 — хороший день, +31 — сильно, с +30 грозы) — проверит в игре; таблица — tools/weather/derive_table → tmp_weather_table.md.


7. Фаза 2 — «атмосфера следует за солнцем» (после фазы 1, её не блокирует)

TODO.md, этап 6 → «Время суток»: прогрев и остывание с инерцией, сила и высота термиков по времени дня, вечерняя дымка; слова пилота: утром — мягкие термики; днём — жёсткие, всё острее; вечером — тёплый, очень хороший мягкий воздух, хорошая динамика у склона, термиков мало. Облачно — прогрев на порядок слабее и мягче. Зимой при ясном небе — термики от леса против снежного поля; утром и вечером — только ветер.

7.1 Что уже есть (проверено по коду)

  • Источники термиков уже следуют за солнцем. Game.start подключает sky.clock.sun_changed → terrain.set_sun; Terrain.thermal_source_strength_at = class_strength[класс] · exposure_gain · (n·sun)^power · (1 + edge) с живым _sun_dir, GroundField.sun() не кеширует. Отсечка thermal.sun_min = 0,35 уже даёт «утром/вечером — только склоны к солнцу»: ровный луг при солнце ниже ~20° не даёт термиков. (docs/guide/game.md → «Время суток» тут устарел: пишет, что источники берут статичное солнце.)
  • Перистая пелена ослабляет прогрев (insolation), тени зрелых облаков гасят новые термики.
  • Нет: инерции прогрева (поверхность откликается на солнце мгновенно, одинаково для камня и поля); хода по времени самой силы, частоты, «жёсткости» термиков, кромки и верха (весь день — «разгар»); тени облаков для воздуха берут статичное солнце (air.set_sun_direction один раз из world.json → sun, а ThermalField.sun_dir — из clouds.sun_*); вечерней дымки; сезонного снега; «облачного» дня.

7.2 Что значит температура пилота

Дневной максимум по прогнозу («днём до +26»), как в фазе 1 — не температура на час старта. Почему: в прогнозе крупно пишут максимум; пилот ставит одно число и не пересчитывает его при старте в 9 утра; при ускорении времени (×10, ×60) смысл не «плывёт». Температура в момент t — из суточного хода (Parton–Logan):

T(t) = T_max − A·(1 − f(t)),   A = diurnal_range_k[месяц] (июль ≈ 11 К, апрель ≈ 12, январь ≈ 6)
f — синус от восхода (0) до t_peak = солнечный полдень + 2,5 ч (1), после — спад к закату до f ≈ 0,65

7.3 Модель

  1. День в момент t: WeatherModel.derive(forecast, ctx, cfg, hour) берёт T(t) → верх сухих термиков и кромка (§2.2) растут с утра («кромка поднимается весь день»), облака появляются, когда z_dry дорос до уровня конденсации (утро голубое, кучёвка с ~11–12, Cb — во второй половине дня), вечером тают.
  2. Поток тепла от земли с инерцией (новый SurfaceHeating, 9 классов): для каждого класса dS_c/dt = (a_c · I(t) − S_c)/τ_c, I(t) = max(0, sin h_солнца) · insolation. τ_c: поле/луг 0,5 ч, скалы/осыпи 1,5 ч, лес 1 ч (и a_c ниже — испарение), застройка 2 ч, вода 10 ч (≈ нет хода), снег — a_c ≈ 0,1 (альбедо). Экспозиция склона берётся по солнцу с запаздыванием класса sun_dir(t − lag_c) (lag ≈ 0,6 τ_c): вечером камни и деревни ещё греют («вечерние термики с камней»). Стоимость — 9 скаляров на шаг часов + по вектору солнца на класс; thermal_source_strength_at — +1 умножение.
  3. Сила — от потока, высота — от температуры (Дирдорф w* ∝ (H0·zi)^(1/3)): множитель силы heat_k(t) = (H(t)/H_пик)^(1/3), где H — поток над ровным лугом из п. 2. Утро: zi мал, H растёт — слабые мягкие; 13–16 ч — пик; вечер: zi ещё высокий, H мал — редкие широкие слабые термики, воздух тёплый и ровный.
  4. «Жёсткие» и «мягкие» — один параметр sharpness(t) = H(t)/H_пик (0,5…1): при мягких радиус × 1,3, ядро × 0,8, болтанка у края (turbulence.edge_factor) × 0,6, конвективная σ ∝ w*. В полдень — как сейчас.
  5. Вечерний спокойный воздух: за 2 ч до заката механическая болтанка × 0,6 (устойчивый приземный слой), склон работает как днём → «хорошая динамика у склона».
  6. Мягкое обновление атмосферы: раз в diurnal.update_s (60 с игрового времени) Game.tick зовёт air.update_weather(derive(..., hour)); Atmosphere плавно (за ~5 мин) ведёт кромку, фон, σ, пороги облаков, а ThermalField рождает новые термики с новыми параметрами — без пересоздания поля (шаг сетки thermal_spacing_m фиксирован на весь день — по пику, иначе термики «прыгают»). Живые термики доживают со старыми числами — переход сам собой плавный.
  7. Тени облаков для воздуха — по часам: sky.clock.sun_changed.connect(air.set_sun_direction) (+ ThermalField.sun_dir оттуда же).
  8. Вечерняя дымка: плотность дымки растёт после пика и у низкого солнца, верх дымки — верх перемешанного слоя в момент t (sky.set_inversion_height_msl на каждом обновлении).
  9. Облачный день — только если пилот попросит 4-й параметр (вопрос 5): insolation × 0,15, sharpness 0,5, без Cb. Иначе — только перистая пелена и тени своих облаков.
  10. Зима: нужен сезонный снег на открытых классах (луг/поле/кустарник → снег при устойчивом снежном покрове, месяцы из конфига локации) — карточка группы «рельеф». Тогда лес (class_strength 0,45) против снега (0,1) сам даёт «термики от леса», а низкое солнце и sun_min — «утром и вечером только ветер».

7.4 Карточки фазы 2 (оценка)

#ЧтоРазмерМодельВладения
2.1Суточный ход T(t), derive(..., hour), тесты (утро < полдень, кромка растёт до пика, облака с ~11 ч, Cb после 14 ч)SSonnetweather_model.*
2.2Мягкое обновление погоды в Atmosphere/ThermalField без пересоздания, плавная кромка, дымка за кромкой; вызов из Game.tickMOpusatmosphere.gd, thermal_field.gd, game.gd
2.3SurfaceHeating (инерция по классам) + API рельефа: запаздывающее солнце и поток по классам в thermal_source_strength_atMOpusscripts/atmosphere/surface_heating.gd, terrain.gd (стык с группой «рельеф»)
2.4«Жёсткие/мягкие» (sharpness), вечерняя спокойная болтанка; проверка словами пилота (тест: σ у края в 10 ч < 14 ч; в 19 ч термиков на 10 км² в 3+ раза меньше, чем в 14 ч; набор на склоне в 19 ч ≥ дневного)S–MOpusatmosphere.gd, weather_model.*, тесты
2.5Тени облаков по часам для воздухаXSSonnetgame.gd
2.6Вечерняя дымкаSSonnetsky_environment.gd, world.json → haze (группа «мир»)
2.7Сезонный снег + зимний тест («лес > снег, в 10 и 16 ч термиков нет»)MOpusгруппа «рельеф»
2.8Облачный день (только по ответу пилота)SSonnetweather_model.*, UI
2.9Кадры: 9, 12, 15, 18, 20 ч при +26 и +31; XC-бот по времени дня (старт в 11 ч проходит больше, чем в 17 ч)SSonnettools/shots/*, tests/atmosphere/xc/*

Итого фаза 2 ≈ L (6–8 карточек). Главный риск — 2.2 (непрерывность облаков и термиков при обновлении) и производительность 2.3 (замер thermal_source_strength_at до/после, допуск +10 %).