Контракты модуля «start-fixes»
Внутренний документ. План — docs/archive/plan/start-fixes.md, журнал — docs/archive/plan/start-fixes-progress.md. Контрактный тест — tests/game/test_start_fixes_contracts.gd (форма стыков; ломается при смене формата без правки контракта). Менять интерфейс — только через координатора: версия +1, что изменилось, уведомить потребителей.
К1. Карта поверхности и поляны у стартов — v2
Владелец: SF-1 (рельеф, scripts/terrain/terrain.gd, surface_layer.gd). Потребители: деревья (TreePlacer, TerrainTreeModels, ForestImpostors), кусты (ShrubScatter), камни (RockScatter), трава (GrassField, grass.gdshader — SF-2), столкновения (CollisionCheck через Terrain.forest_at), термики.
Что есть (фиксируем):
- Классы
SurfaceLayer:NONE=0, FOREST=1, GRASS=2, CROP=3, SHRUB=4, BARE=5, WATER=6, BUILT=7, SNOW=8,CLASS_COUNT=9. Шейдеры читают класс ближайшего узла карты (какTerrain.surface_at). SurfaceLayer.replace_in_circle(cx: float, cz: float, r_in: float, from_class: int, to_class: int)— мир (x — восток, z — юг), м; дляfrom_class == FORESTтакже обнуляет маску леса 10 м в круге.Terrain.surface_at(x, z) -> int,Terrain.forest_at(x, z) -> float(0..1, лес — от 0,5,CollisionCheck.FOREST_MIN).- Встроенные старты:
Terrain.set_surfacesвычищает FOREST и SHRUB → GRASS в кругеlocations/<id>.json → start_clearing_radius_mвокруг каждогоget_start_sites()[i].position. Новое (SF-1, v2 — 30.09): Terrain.add_start_clearing(x: float, z: float, radius_m: float) -> void— синхронный, вызывается после загрузки (Game._choose_startдля старта с карты). Только лес:FOREST → GRASSво всех картах поверхности, маска леса 10 м — ноль (доradius + 1,42·шаг маски);SHRUB, кусты, камни, трава не трогаются (решение пользователя 30.09: пустырь — с травой, без деревьев). Сразу, в том же вызове:surface_at/forest_at, текстуры классов и маски у рельефа (ImageTexture.updateна месте — их же читают трава,far_layer, импостеры, процедурные кроны), маска уTerrainTreeModels/ForestImpostors. Деревья и кусты перестраиваются со следующего кадра (TerrainTreeModels.invalidate(), кешShrubScatterсбрасывается поsurface_revision— кусты на бывшем лесе растут как на лугу);rebuild_now/build_tileвидят новое сразу.Terrain.surface_revision: int— растёт при каждой правке карты после сборки.Terrain.get_start_clearings() -> Array[Vector3]— все пустыриVector3(x, z, r): встроенные старты и добавленные.ShrubScatterне ставит внутри одиночные деревья (кусты — ставит);RockScatterне меняется.TerrainRenderer.refresh_surface(li: int, surface: SurfaceLayer)— обновить текстуры слоя на месте.- Радиус:
configs/game.json → start_search.clearing_radius_m(160 м; обоснование —_doc),Terrain.start_clearing_radius_m()(static) читает его. Встроенные старты: как раньше,FOREST|SHRUB → GRASSвlocations/<id>.json → start_clearing_radius_m(70 м), и дальше доmax(start_clearing_radius_m, clearing_radius_m)— толькоFOREST → GRASS. - Инварианты после вызова: для всех точек круга
surface_at != FOREST,forest_at == 0;SHRUB— где был; ни одного дерева (модели, импостеры, одиночные деревьяShrubScatter) в круге; за кругом карта не меняется. Класс, в который переходит лес, —GRASS(трава SF-2 растёт там как на лугу). - R
≥ 2·L_run— проверяетtests/game/test_start_clearing.gdживым прогономGroundRunпо всем крыльям.
К2. Трава по классам поверхности — v2
Владелец: SF-2 (grass.gdshader, grass_field.gd, configs/vegetation.json → grass). Потребители: нет (визуал).
v1: GrassField.setup(...) передаёт в материал shrub_density (доля пучков на классе SHRUB); пучки на FOREST, BARE, WATER, BUILT, SNOW — не рисуются; высота: луг blade_height_m, нива crop_height_m.
v2 (SF-2): трава и на FOREST. Ключи configs/vegetation.json → grass (каждый с _doc) → uniform grass.gdshader с теми же именами, ближний и дальний (far) слой:
forest_density(float, 0,4) — доля пучков на FOREST; 0 — травы в лесу нет (как v1);forest_height_m([мин, макс], м; 0,12–0,35) — высота травы под пологом;forest_shade(float, 0,8) — множитель цвета (× цвет луга в этой точке). Формула доли по классу в GDScript —GrassField.class_share(c, shrub_density, forest_density). Проверка —test_k2_grass_config,test_forest_grass_reaches_material. SF-1 от этого не зависит (поляна — класс GRASS).
К3. Крен на земле и переход в полёт — v2 (заменён К3 v3 — docs/contracts/control-fix.md, С3)
Владелец: SF-3 (scripts/flight/ground_run.gd: _ground_bank, _turn; стык GROUND→AIR в flight_model.gd). Потребители: FlightTelemetry (bank_deg, basis), GliderVisual, камеры, боты (bot_pilot.gd — управляют через ControlInput), сеть (net_flight.gd передаёт bank), SF-4.
Не меняется (v1):
FlightModel.bank: float— рад, + вправо, относительно горизонта (не склона);FlightModel.roll_rate: float— рад/с;FlightModel.heading— рад, 0 — север, по часовой;FlightModel.positionна земле — точка ступней на рельефе.GroundRun.step(m: FlightModel, dt: float, input: ControlInput, air_fn: Callable, ground_fn: Callable) -> Result(NONE, TOOK_OFF, FAILED);GroundRun.phase ∈ {"standing","walking","running"};GroundRun.failure— причиныnose_high, nose_low, tailwind, crosswind, weak_run.ControlInput.roll ∈ [−1, 1]— на земле A/D: только курс, крыло не кренит (стоя/шагом — поворот на месте, на бегу — по дуге);input.run,input.walk,input.pitch.- Опрокидывание —
failure == "crosswind"(порогflight.json → ground_bank.fail_bank_deg,takeoff.fail_time_s). Изменилось в v2 (SF-3): - Крен на земле — динамика крыла на плечах (
docs/guide/flight.md→ «Крен на земле»): на землеGroundRunпишет иbank, иroll_rate; на отрыве оба непрерывны, дальше — модель крена в воздухе. - Срыв
crosswindпроверяется и стоя/шагом (раньше — только после начала разбега); остальные причины — как раньше, после разбега. configs/flight.json → ground_bank:pilot_moment_max_nm(Н·м),pilot_response_s(с),wing_cg_above_axis_m(м),span_mass_fraction(доля),slip_roll_per_cl(|C_lβ|/C_L, 1/рад),fail_bank_deg(°). Убраныcrosswind_roll_dps_per_ms,pilot_roll_rate_dps,level_time_s.configs/pilot.json → run.turn_accel_max_ms2(м/с², предельное боковое ускорение на дуге) вместоrun.ground_turn_rate_dps.- Публичные поля
GroundRun(читать, не писать):feet_load: float— доля веса на ногах N/W ∈ [0, 1] (1 — стоит, 0 — отрыв);wind_moment_nm: float— кренящий момент ветра, Н·м, + вправо;hold_limit_nm: float— предел руки пилота сейчас,pilot_moment_max_nm · feet_load, Н·м. - Статические
GroundRun.roll_inertia(m: FlightModel) -> float(кг·м²) иGroundRun.pilot_moment(m, bank, rate, load_frac, inertia) -> float(Н·м) — для тестов.
К4. Поза крыла на земле — v1
Владелец: SF-4 (glider_visual.gd _ground_pose, flight_telemetry.gd basis на земле, тангаж стоя в ground_run.gd::_aero_force). Потребители: камеры, CollisionCheck (точки крыла над ступнями: BODY_POINTS_M, концы консолей по span_m, hang_m), сеть, SF-3 (крен).
Что есть: Telemetry.basis = Basis.from_euler(Vector3(theta, −heading, −bank)), начало координат планера — ступни (FlightModel.position); карабин/килевая труба — на hang_height_m над ступнями; GliderVisual сглаживает позу пилота (input_smoothing_s).
Новое (SF-4): если меняется точка поворота базиса на земле или смысл theta стоя — вписать сюда (версия 2) и сообщить координатору до коммита: это стык с CollisionCheck и камерами.
Уточнение (координатор, 30.09, без смены версии): после SF-4 (3707069) и SF-3 (5f8990e) стоя заданный угол атаки отсчитывается от направления бега вдоль склона по курсу (до min_airspeed_ms — плавное смешивание с набегающим потоком), reset_on_ground ставит установившийся тангаж стоя. Смысл theta (тангаж киля относительно горизонта), Telemetry.basis и точка поворота (ступни) не изменились — потребители (CollisionCheck, камеры, сеть) не затронуты, поэтому К4 остаётся v1. Вертикальное тело пилота стоя — только в GliderVisual._ground_pose (узел пилота), крыло не двигается.