Модуль «start-fixes» — фиксы старта с земли

Ветка feature/start-fixes (от main @ 113878d), копия ~/deltaplan-start. Журнал — docs/archive/plan/start-fixes-progress.md, контракты — docs/contracts/start-fixes.md (+ tests/game/test_start_fixes_contracts.gd). Источник — TODO.md, строки 22–28 (отзыв пользователя 30.09.2026).

Цель

Старт с земли в любой точке карты должен быть возможен и выглядеть правдой: вокруг старта нет леса и кустов; под деревьями растёт трава; пилот с крылом на земле стоит ровно по горизонту, крен держит «рука пилота» с физически ограниченной силой, переход в полёт без рывка; крыло не проходит сквозь землю на ровном старте.

Решения пользователя (не обсуждаются)

  1. Пустырь: вокруг точки старта убирать деревья и кусты; радиус пустыря — не меньше 2× длины разбега (иначе сразу за разбегом стоят деревья и набрать высоту до них невозможно). Тест проверяет, что в радиусе ≥ 2× длины разбега нет деревьев и кустов.
  2. Трава на подложке «лес»: рисовать траву и там (деревья не должны стоять на голой плоской текстуре).
  3. Пилот на земле до взлёта:
    • стоит — всегда вертикален, только поворот вокруг вертикальной оси; бежит — A/D заворачивают его по дуге;
    • переход в полёт без рывка: удержание крена ослабевает по мере того, как крыло забирает вес с ног (стоит — полностью, крыло несёт половину веса — наполовину, оторвался — крен свободен);
    • не жёсткая блокировка, а сильная «рука пилота» (момент с физическим смыслом, ограниченный силой пилота): слабый ветер перебарывает, сильный боковой порыв может поднять консоль и опрокинуть — опасность учит последствием, без подсказок и сообщений;
    • «вертикален» — относительно горизонта, не склона: на косом склоне верхняя консоль близко к земле — правда жизни;
    • если крыло цепляет землю на ровном старте — это отдельная ошибка столкновения/постановки пилота на рельеф: найти причину с доказательством (числа/кадры), а не прятать удержанием.
  4. Пустырь = только убрать деревья (30.09, уточнение решения 1): траву и всё остальное (кусты, камни, подложку) не убирать — пустырь на старте с травой. Новая поляна: только FOREST → GRASS (деревья, импостеры, маска леса уходят); SHRUB и кусты не трогаются. Приёмка SF-1: в радиусе ≥ 2× разбега нет деревьев, трава на месте; координатор проверяет по скриншоту старта (в т.ч. старт в лесу).
  5. Коридора разбега для старта с карты нет (30.09): кусты не убирать — при боковом ветре игрок может развернуться и бежать вдоль склона, направление разбега заранее неизвестно. К1 не меняется.
  6. Отдельной задачи по test_takeoff::test_headwind_launch (atlas, nose_high) нет (30.09): не заводить; падение — известное, было на базе до модуля.
  7. SF-4 передаётся исполнителю SF-3 (30.09): связанные задачи ведёт один исполнитель. Исполнитель SF-4 коммитит наработки и находки в start/sf4 и останавливается; исполнитель SF-3 вливает start/sf4 в start/sf3 и доводит обе задачи вместе — приёмка по условиям SF-3 + SF-4; копия ~/deltaplan-start-sf4 удаляется после слияния.

Разведка (что есть в коде, 30.09)

  • Поляны у встроенных стартов уже есть: Terrain.set_surfaces → SurfaceLayer.replace_in_circle(FOREST|SHRUB → GRASS) радиусом locations/<id>.json → start_clearing_radius_m (70 м) вокруг _sites; маска леса 10 м обнуляется там же. Деревья (TreePlacer/TerrainTreeModels), импостеры, кусты (ShrubScatter), forest_at (столкновение с кронами, CollisionCheck) и трава — все читают эту карту поверхности. Старт с карты (game.gd::_choose_start, StartPlacement.find_launch) выбирается после загрузки рельефа — поляны там нет; в лесу CollisionCheck ещё и засчитывает удар о кроны.
  • Трава (grass.gdshader): класс поверхности по карте — луг/пашня/кустарник (shrub_density); на классе FOREST (1) пучков нет.
  • Крен на земле (GroundRun._ground_bank): rate = 10°/с/(м/с)·боковой_ветер + 30°/с·input.roll − bank/0,6 с — A/D на земле одновременно поворачивают курс (_turn) и кренят крыло (установившийся крен 18° при полном A/D) — это «неваляшка»; боковой ветер 3 м/с даёт 18°. Нет ограничения силы пилота и нет зависимости от разгрузки ног; A/D на разбеге поворачивают курс с постоянной угловой скоростью, а не по дуге.
  • Касание крылом: в режиме GROUND столкновения крыла с землёй нет — это визуальное проникновение. Кандидаты (проверить числами): крен от A/D/ветра (половина размаха ~5 м на высоте ~1,5–1,8 м → касание при ~19°); тангаж theta = gamma + нос при стоянии (v < min_v → gamma = 0, нос вверх относительно горизонта) на склоне вниз — задняя часть киля в склон; поворот базиса вокруг ступней (Telemetry.basis в точке position = ступни); сглаживание позы в GliderVisual.

Физика и границы модели (для SF-3)

Крыло на плечах пилота вращается по крену вокруг оси киля у плеч. Моменты по крену:

  • аэродинамический от бокового ветра (поперечная устойчивость, V-образность/стреловидность: M = q·S·b·C_lβ·β, β — угол скольжения относительно набегающего потока), плюс несимметрия ветра по размаху, если есть;
  • вес крыла при крене (плечо — высота ЦТ крыла над осью вращения);
  • рука пилота: восстанавливающий момент (пилот как регулятор «пропорционально крену + демпфирование»), ограниченный M_max · (N/W), где N/W — доля веса на ногах (1 − L_верт/W, 0..1). M_max — из силы рук на стойках трапеции и опоры стоп (оценка с числами в документе, порядок сотни Н·м);
  • инерция крыла по крену и аэродемпфирование (порядок величин — из массы/размаха крыла). Не учитываем: подвижность ног, шаг в сторону, сложную позу тела — это граница модели (в docs/guide/flight.md).

Задачи

#ЗадачаТипОценкаЗависит
SF-1Пустырь вокруг точки стартаdp-engineer0,5–1 дн—
SF-2Трава на подложке «лес»dp-engineer0,5 дн—
SF-3Рука пилота: крен на земле, поворот по дуге, переход в полётdp-engineer1 дн—
SF-4Касание крылом земли: причина с доказательством + исправлениеdp-engineer0,5–1 дн— (параллельно SF-3; с учётом, что SF-3 уберёт крен от A/D)

Все четыре — одна волна, параллельно (≤ 4 исполнителей).

SF-1. Пустырь вокруг точки старта

  • Скоуп: при старте с карты (и в любом другом «произвольном» старте) — лес и кустарник в круге радиуса R вокруг точки старта → луг, как у встроенных стартов (та же replace_in_circle), с обновлением всего, что читает карту поверхности: деревья, импостеры, кусты, маска леса 10 м (forest_at, столкновения с кронами), текстура поверхности у рельефа/травы. R — не меньше 2× длины разбега (решение пользователя); длина разбега L_run — горизонтальная дистанция от старта с места до отрыва в штиль на склоне start_search.min_slope_deg (самый длинный из пригодных) для самого «длинного» крыла линейки при эталонной массе; если в штиль на этом склоне взлёт не удаётся — дистанция за takeoff.max_run_time_s. R — в конфиге с обоснованием в _doc (числа); встроенные старты — max(start_clearing_radius_m, R).
  • Ключевые файлы: scripts/terrain/terrain.gd, scripts/game/game.gd, scripts/game/start_placement.gd, configs/game.json (start_search), при необходимости tree_placer.gd/terrain_tree_models.gd/forest_impostors.gd/shrub_scatter.gd (только перестроение/обновление), docs/guide/terrain.md/docs/guide/game.md.
  • Не трогать: grass.gdshader, grass_field.gd (кроме вызова уже существующего обновления текстуры поверхности, если нужно — через координатора), configs/vegetation.json (SF-2); scripts/flight/* (SF-3/SF-4).
  • Условия приёмки:
    1. Headless-тест: L_run измеряется в тесте живым прогоном GroundRun (синтетический склон min_slope_deg, штиль, все крылья линейки) и проверяется R ≥ 2·L_run.
    2. Headless-тест: после старта в точке, где лес/кустарник, в круге R нет деревьев (экземпляры TerrainTreeModels/TreePlacer), кустов (ShrubScatter), forest_at = 0 и surface_at ∉ {FOREST, SHRUB} (сетка проверки ≤ 5 м); за пределами круга лес остаётся.
    3. Разбег с этой точки не кончается crash_trees (CollisionCheck по пути разбега и первых ~100 м полёта над поляной).
    4. Скриншот: старт с карты в лесу — поляна вокруг, вид с земли и сверху (build/screenshots/sf1_pustyr/).
    5. Старые тесты рельефа/деревьев/кустов/травы — зелёные.

SF-2. Трава на подложке «лес»

  • Скоуп: травинки растут и на классе FOREST — плотность и высота из конфига (configs/vegetation.json → grass, новые ключи с _doc: доля пучков под пологом, высота — трава под лесом ниже и реже луга; обоснование — тень полога), цвет — как у луга в этой точке или темнее (палитра рельефа); у ствола не обязательно выкашивать. Дальняя трава/подложка (far_layer, если он рисует луг) — то же правило. Визуальные мелочи — не больше 2 попыток.
  • Ключевые файлы: scripts/terrain/grass.gdshader, scripts/terrain/grass_field.gd, configs/vegetation.json, docs/guide/vegetation.md, тест tests/vegetation/test_grass_field.gd.
  • Не трогать: terrain.gd, game.gd, деревья/кусты (SF-1), scripts/flight/*.
  • Условия приёмки:
    1. Тест: параметр плотности на классе FOREST доходит до материала травы (grass_field → uniform), по умолчанию > 0; при 0 — как раньше.
    2. Скриншоты с земли: лес на Аскарово (или другой встроенной локации с лесом у старта) — до/после, трава между стволами (build/screenshots/sf2_trava_les/).
    3. Замер кадра на стенде/в игре: время кадра до/после при виде на лес с земли (число; рост — в пределах нескольких %; если больше — сказать).
    4. Старые тесты травы — зелёные.

SF-3. Рука пилота: крен на земле, поворот по дуге, переход в полёт

  • Скоуп: заменить GroundRun._ground_bank на динамику крена крыла на плечах (см. «Физика»): моменты ветра, веса крыла, рука пилота с пределом M_max·(N/W); A/D на земле не кренят крыло: стоя/шагом — поворот на месте вокруг вертикали, на разбеге — по дуге (угловая скорость = скорость / радиус, радиус ограничен снизу физически: боковая сила ног/сцепление, число с обоснованием). Крен — относительно горизонта (не склона). Опрокидывание (крен ≥ fail_bank_deg дольше fail_time_s) — срыв «crosswind», как сейчас (итог есть в интерфейсе; новых подсказок/сообщений нет). Переход в полёт: крен и скорость крена непрерывны на отрыве; в воздухе — прежняя модель крена. Параметры — configs/flight.json → ground_bank (новые ключи с _doc и единицами, старые crosswind_roll_dps_per_ms/pilot_roll_rate_dps/level_time_s — убрать); описание, законы и границы — docs/guide/flight.md.
  • Ключевые файлы: scripts/flight/ground_run.gd, scripts/flight/flight_model.gd (только стык перехода GROUND→AIR, roll_rate на отрыве), configs/flight.json, configs/pilot.json (если поворот/сила пилота там), docs/guide/flight.md, tests/flight/test_takeoff.gd + новый tests/flight/test_ground_bank.gd.
  • Не трогать: glider_visual.gd _ground_pose, flight_telemetry.gd (SF-4), рельеф/растительность.
  • Условия приёмки (headless):
    1. Штиль, стоит 10 с с зажатой A (и D): |крен| < 0,5°, курс поворачивается (> 30°).
    2. Боковой ветер слабый (2,5 м/с, как в старом test_crosswind_corrected, без ввода крена): установившийся |крен| < 5°, взлёт удаётся.
    3. Боковой порыв выше порога (порог M_aero = M_max — посчитан и назван в документе): крыло опрокидывается → FAILED «crosswind» за разумное время; порыв на 20 % ниже порога — держит.
    4. Разбег с зажатой A: траектория — дуга, радиус ≥ R_min (число), крен на бегу < 3° при штиле.
    5. Переход: на отрыве |Δкрен| за шаг и скачок скорости крена < порога (например < 1°/с) при штиле и при слабом боковом ветре; удержание пропорционально доле веса на ногах (тест: при искусственной разгрузке 50 % предельный момент — вдвое меньше).
    6. Косой склон (поперечный уклон 15°): крен ≈ 0 относительно горизонта (|крен| < 0,5°).
    7. Все тесты flight/ и game/ зелёные (test_crosswind_* переписаны под новую модель).

SF-4. Касание крылом земли: причина и исправление

  • Скоуп: найти, почему крыло проходит в землю на старте (сначала — на ровном; затем — на встроенных стартах локаций): замер нижней точки крыла (концы консолей, задний и передний конец киля, трапеция) над рельефом по кадрам стоя/шагом/на разбеге — отдельно вклад крена, тангажа, точки поворота базиса (ступни), высоты подвески/модели, сглаживания позы, выборки высоты рельефа. Доказательство — таблица чисел и кадры. Исправить, если причина локальна (постановка на рельеф, тангаж стоя, ось поворота, поза визуала). Если причина требует менять модель столкновений шире задачи — шлюз: остановиться и вернуть варианты координатору.
  • Ключевые файлы: scripts/flight/glider_visual.gd (_ground_pose, поза крыла на земле), scripts/flight/flight_telemetry.gd (basis на земле), scripts/flight/ground_run.gd (_aero_force — только тангаж стоя; крен — SF-3), scripts/flight/glider.gd, инструмент замера (в tools/ или тест), docs/guide/flight.md (раздел о позе на земле — согласовать слияние с SF-3 через координатора).
  • Не трогать: _ground_bank, _turn в ground_run.gd (SF-3); рельеф/растительность.
  • Условия приёмки:
    1. Отчёт о причине: таблица (старт, фаза, крен, тангаж, уклон, нижняя точка крыла над землёй, м) до и после, 2–4 кадра до/после (build/screenshots/sf4_kasanie/).
    2. Headless-тест: на ровном старте при штиле (стоит, шагом, разбег до отрыва, крен = 0) нижняя точка крыла ≥ зазора (число с обоснованием, например ≥ 0,3 м) над рельефом; на склоне 20° по курсу вниз — то же.
    3. На косом склоне 15° тест только фиксирует число (верхняя консоль близко к склону — правда жизни, не исправляется).
    4. Тесты flight/ и game/ зелёные.

Риски

  • SF-3 и SF-4 правят ground_run.gd в разных функциях — слияние через координатора (сначала принимается первая готовая, вторая — git merge feature/start-fixes перед отчётом).
  • SF-1: перестройка деревьев/кустов после выбора старта увеличит время загрузки при старте с карты — замерить (число в отчёте).
  • Если причина касания крылом — геометрия модели крыла/высота подвески, затрагивающая полёт (камеры, столкновения) — шлюз.