Контракты модуля «ui-controls»
Внутренний документ. План — docs/archive/plan/ui-controls.md. Контрактный тест — tests/game/test_ui_controls_contracts.gd. Менять — только через координатора модуля: версия +1, что изменилось, потребители правятся в том же шаге.
Опора: ControlInput.pitch (control-fix С1) — без изменений
ControlInput.pitch ∈ [−1, 1]: +1 — трапеция от себя (нос вверх, торможение, угол атаки больше), −1 — на себя (нос вниз, разгон). Этот модуль смысл поля не меняет — меняет только, какие устройства ввода дают какой знак.
У1. Раскладка тангажа и крена — v3 (этап 3, 02.10)
Владелец: этот модуль (configs/controls.json → keys, invert_pitch; знак стика в InputController._apply_gamepad). Потребители: InputController (полёт и земля), экран «Управление» (configs/ui.json → controls_help), подсказки/локали, документация, автопилот и тесты (жмут действия по имени).
Действия (имена — смысл, не клавиша):
pitch_push_out= [Up] — трапеция от себя →pitch+;pitch_pull_in= [Down] — на себя →pitch−.roll_left= [Left],roll_right= [Right] — крен (roll−/+).- W/S/A/D не назначены ни на одно действие трапеции (
pitch_*,roll_*). - Мышь — всегда крыло (трапеция); настройки режима мыши нет (
controls.json → mouse.modeнет). Мышь захвачена (InputController.mouse_captured, M;mouse.capture_on_start): от себя (курсор вверх,relative.y < 0) →pitch+, вбок →roll; зажата правая кнопка — мышь крутит голову в кабине, трапеция держит положение. Не захвачена — мышь трапецию не двигает, трапеция держит последнее положение мыши. Свободная камера (hands_off) — мышь крутит камеру, смещение трапеции не копится (UC-5). - Стик: вперёд (ось
gamepad.pitch_axis< 0) →pitch+. invert_pitch(по умолчаниюfalse):trueменяет знак тангажа для стрелок, мыши и стика разом («как в самолёте»).- Земля (С2 v2 control-fix): трапеция (нос и заданный крен крыла) — те же действия
pitch_*/roll_*(стрелки), мышь, стик — стоя, шагом и на разбеге. - Экран «Управление»: строки «идти/шаг назад/поворот» привязаны к
walk_*/turn_*, строки трапеции — кpitch_*/roll_*; поляmouse_modeу строк нет. - Инвариант (тест): в полёте по умолчанию ↑, мышь от себя, стик вперёд дают
pitch> 0 и рост угла атаки; ↓, мышь на себя —pitch< 0; инверсия — наоборот; W/S/A/D —pitch=roll= 0.
История: v1 (v1.0.0) — W/↑ на себя, «как в самолёте»; v2 (01.10) — W/↑ от себя, мышь bar/look; v3 (02.10, решение пользователя «крыло через WASD не нужно, настройку убрать») — W/S/A/D сняты с трапеции, режим мыши убран (мышь всегда крыло).
У2. W/S/A/D — пилот на земле, голова в полёте — v2 (этап 3, 02.10)
Владелец: этот модуль. Потребители: InputController, CameraRig, Game (связка), экран «Управление» и тексты, автопилот (не затронут — жмёт действия по имени).
Действия (configs/controls.json → keys): на земле walk_forward = [W], walk_back = [S], turn_left = [A], turn_right = [D] (С2 v2); в полёте look_up = [W], look_down = [S], look_left = [A], look_right = [D].
Условие — InputController.keys_look() -> bool:
true ⇔ enabled и не hands_off и не on_ground. От мыши (захват) не зависит.
- Земля (стоя, шагом, разбег): W/S — шаг (
ControlInput.walk), A/D — поворот на месте (turn); на разбеге (Shift, CF-3)walk=turn= 0 — бег вперёд Shift, курс — от крена крыла. W/S/A/D в трапецию не идут; голову не крутят (keys_look()= false). - Полёт: W/S/A/D в трапецию не идут (не назначены, У1 v3); при
keys_look()в камере кабины (CameraRig.mode == "cockpit") удержаниеlook_left/look_rightменяет рыскание головы (+ влево),look_up/look_down— тангаж (+ вверх) со скоростьюcamera.json → cockpit.head.key_rate_deg_s(°/с, > 0), в пределахcockpit.head(yaw_limit_deg, pitch_up_limit_deg, pitch_down_limit_deg); отпустил — голова остаётся; V/средняя — плавно вперёд.set_look(скриншоты) — клавиши голову не двигают. - Камера
chase— клавиши обзора ничего не делают;free— без изменений (WASD двигают её, крыло без рук,hands_off). - Отрыв/касание: вклад W/S/A/D в трапецию 0 и до, и после — скачка нет; зажатая клавиша меняет роль сразу (защёлки нет).
Связка: CameraRig.keys_look_fn: Callable () -> bool; не задана — false. Game ставит camera.keys_look_fn = input_controller.keys_look.
Наблюдаемое для тестов: CameraRig.head_look_deg() -> Vector2 — поворот головы (x — рыскание + влево, y — тангаж + вверх), °, без взгляда на прибор и без look_down_deg.
Инварианты (тесты test_u2_*, test_gameplay.gd::test_ground_bar_keys): полёт (с захватом мыши и без): W/S/A/D 0,5 с → pitch = roll = 0 (|·| < 1e-6), keys_look() = true; ↑ → pitch > 0, → → roll > 0; action_press("pitch_push_out") → pitch > 0. Земля стоя: keys_look() = false; W → walk > 0, pitch = 0; A → turn < 0, roll = 0; разбег (Shift): W/A → pitch = roll = 0, walk = turn = 0, ↑ → pitch > 0. Камера кабины с keys_look_fn = true: A 0,5 с → рыскание ≈ +45° (при 90°/с), W 0,5 с → тангаж > 0; с false — 0.
История: v1 (этап 2) — обзор с клавиш только при мыши-трапеции с захватом, в полёте и на разбеге; v2 (02.10) — обзор с клавиш в полёте всегда, на земле никогда; режима мыши нет.