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

Состояние на 04.10.2026: сетевая игра в игре с 0.8.0 и развивается (решение пользователя 04.10: план активный); открытые задачи — TODO.md T-37 (NET-60), T-38 (NET-61), T-39 (NET-20, NET-21).

Статус (на выпуск 0.8.0): выполнено, влито в main (08c614f, выпуск 0.8.0, 29.09.2026): M0–M5 (NET-00…NET-53) и сервер на Go с Docker-развёртыванием. Не сделано: NET-60 (tools/net/e2e.sh, 30-минутный прогон) и NET-61 (docs/net.md, проверка через интернет с пилотами); нагрузочный прогон NET-20 и systemd-юнит NET-21 — не найдены. Исходный план (28.09.2026): Документ — для планирования агентов: модули, в каждом — задачи со скоупом, ключевыми файлами и условиями приёмки. Код детально не анализировали: пути к файлам ориентировочные, первая задача каждого модуля — уточнить их.

Цель

Летать вместе с живыми пилотами в одном небе. Пользователей ~10 (пилоты, друзья) → никакой авторизации, аккаунтов, лобби, масштабирования. Один сервер на Go на VPS пользователя; Telegram-бота и прочих сервисов нет.

Как это выглядит для игрока

Основной случай — друзья полетали вместе. Код зоны пилоты передают друг другу сами (голосом, в мессенджере) — игра этим не занимается.

  1. «Сетевая игра» в главном меню → экран: адрес сервера (IP:порт, запоминается), своё имя пилота (из настроек).
  2. «Создать»: выбирается только место; крыло, масса, время, погода — из «Полёт…». Сервер выдаёт 4-значный код (например, 4721) — его диктуют/пересылают друзьям.
  3. «Присоединиться»: ввод кода. Мир (место, дата, время, погода, сид) берётся у зоны. Ведущий ещё на земле → встаёшь в очередь на старт; уже летит → автоматически «догнать» (появляешься в воздухе рядом с ним).
  4. Ведущий — первый подключившийся; вышел → ведущий следующий по порядку подключения. Незаметно для игроков.
  5. «Догнать» в любой момент: клавиша = → поверх полёта быстрое полупрозрачное меню со списком пилотов (кроме себя) → стрелками выбрать, Enter — крыло само летит к нему (обычно ~10 с: плавный разгон, быстро в середине, плавное торможение) и отдаёт управление рядом с целью.
  6. Боты. Число ботов берётся из настроек создателя зоны (ведущего на момент создания) и в сети такое же, как в одиночной игре. Боты всегда стоят в очереди после живых пилотов. При смене ведущего число ботов не пересчитывается.
  7. После полёта (посадка, авария, сорванный старт) — обычное окно итога со статистикой, в нём главная кнопка «Продолжить рядом» (в фокусе: Enter — и буксир везёт к ближайшему другу в воздухе; друзей в воздухе несколько — открывается меню «Догнать» со списком) и «На старт» (в конец текущей очереди; пустая — сразу первым). В воздухе никого — «Продолжить рядом» не показывается. Уход в конец очереди после сорванного старта — намеренно: лёгкое наказание за неудачный старт и живая очередь, как на настоящем старте. Не «чинить»; менять, только если пожалуются сами игроки. Обходных путей (пропуск очереди, «вне очереди») не делаем: если очередь мешает, игроки создают зону без ботов (0 в настройках) и взлетают друг за другом.
  8. Конец игры. Зона закрывается, когда из неё вышли все живые пилоты (боты не держат зону); код освобождается сразу.

Главный случай: одна комната (добавлено 28.09.2026)

Скорее всего пилоты сидят в одной комнате (локальная сеть) и общаются голосом. Поэтому:

  • Сервер внутри игры. «Создать» без адреса сервера запускает встроенный сервер на этом компьютере (тот же контракт, что у Go-сервера); остальные подключаются к нему. Go-сервер на VPS — для игры через интернет.
  • Поиск в локальной сети. Компьютер с зоной раз в секунду объявляет её в сети (UDP broadcast); у остальных на экране «Сетевая игра» — список «Рядом: зона 4721 — Коля», Enter — войти. Поле адреса остаётся для VPS.
  • Ограничение: со встроенным сервером выход создателя закрывает зону (смены ведущего нет) — для одной комнаты это нормально.

Архитектура

  • Мир не передаётся. Термики, облака и ветер детерминированы по (место, дата, время, погода, сид, время зоны) — каждый клиент считает атмосферу сам. По сети — только параметры зоны, часы зоны, очередь на старт и состояния пилотов (~100 байт × 10 Гц × ≤10 пилотов).
  • Сервер на Go (папка server/ в этой репе): ретранслятор. Выдаёт и проверяет коды зон, ведёт порядок подключения (из него — ведущий), пересылает сообщения внутри зоны. Логики полёта не знает. Без авторизации.
  • Код зоны — 4 цифры, выдаёт и валидирует только сервер (уникален среди активных зон, освобождается, когда зона пуста). Для клиента код — просто строка.
  • Контракт сообщений — машиночитаемый. server/proto/deltaplan/v1/net.proto (Protocol Buffers) — единственный источник правды: все сообщения, поля, комментарии (смысл, единицы, частота, кто шлёт). По нему люди и AI понимают протокол; из него генерируется Go-код.
    • Почему не gRPC. gRPC (HTTP/2) в Godot нет — ни в движке, ни живого аддона; GDExtension с C++ grpc — тяжёлая сборка под каждую платформу. Двунаправленный поток даёт WebSocket.
    • Транспорт: один WebSocket на клиента (ws://IP:порт/v1/ws). В Godot — встроенный WebSocketPeer, без аддонов. Каждый кадр — Envelope с oneof.
    • Кодировка на проводе: proto3 JSON (Go — protojson, Godot — встроенный JSON): читается глазами при отладке, контракт тот же .proto. Бинарный protobuf (godobuf — генератор GDScript из .proto) — позже при необходимости, без смены контракта.
  • Протокол не зависит от транспорта: позже ретранслятор можно заменить на WebRTC-mesh (VPS — сигналинг/TURN) без изменений игровой логики.
  • Ведущий держит часы зоны и очередь на старт, раз в секунду рассылает ZoneState и считает ботов (их состояния рассылает так же, как состояния пилотов, с пометкой «бот»). Состояние зоны и последние состояния ботов есть у всех, поэтому смена ведущего бесшовна: новый ведущий продолжает ботов с их последних состояний. Кто ведущий — решает сервер (самый ранний по порядку подключения).

Ветка и работа агентов

  • Вся работа — в ветке feature/multiplayer, в отдельном git worktree (например, ~/deltaplan-mp), чтобы не мешать работе в main. Слияние в main — после приёмки пользователем.
  • Один агент — одна задача (NET-xx); у модуля может быть ведущий агент, который раздаёт задачи.
  • Коммиты — только свои файлы (git commit -- <пути>), сообщения на русском. Godot — всегда с временным XDG_DATA_HOME.
  • Периодически подтягивать main в ветку (git merge main); конфликты решает агент модуля.
  • Видимые изменения — со скриншотами (ru/en для интерфейса).

Агенты: координаторы и исполнители

Правило: один исполнитель — одна задача NET-xx (узкий контекст); координатор держит план своего модуля, раздаёт задачи, принимает результат (тесты, скриншоты), сливает main в ветку и решает конфликты. Модели: Opus по умолчанию, Sonnet — простые задачи (помечены), повтор неудачной задачи — Fable с чёткой спецификацией и приёмкой.

КоординаторМодулиИсполнители (задачи)Число
К0. Главный — ветка и worktree, порядок волн, приёмка у пользователя, слияние в mainM6NET-60, NET-612
К1. Сеть — контракт, сервер, клиентM1, M2, M3NET-10; NET-20; NET-21 (Sonnet); NET-22; NET-23; NET-30; NET-31; NET-328
К2. Игра в сети — мир, режим, чужие пилоты, «догнать», очередь, ботыM0, M4NET-00; NET-40; NET-41; NET-42 ×2 (меню + буксир); NET-43; NET-447
К3. ИнтерфейсM5NET-50; NET-51 (Sonnet); NET-52 (Sonnet); NET-534
Итого4 координатора21 исполнитель

Волны (одновременно — не больше ~5 исполнителей):

  1. NET-00, NET-10.
  2. NET-20, NET-21, NET-30, NET-50 (на заглушках), NET-51.
  3. NET-31, NET-32, NET-41 (на поддельных состояниях), NET-52.
  4. NET-40 → затем NET-42 (меню и буксир — параллельно) и NET-43 (правят game.gd — по очереди, через К2).
  5. NET-44.
  6. NET-60 → NET-61.

Порядок и зависимости

M0 Детерминизм ─┐
M1 Контракт ────┼─► M2 Сервер Go ──┐
                └─► M3 Клиент сети ┼─► M4 Игра в сети ─► M6 Проверка и выпуск
                    M5 Интерфейс ──┘

M0 и M1 — первыми, параллельно. M2 и M3 — параллельно после M1. M5 можно начинать после M1 (экраны на заглушках). M4 — после M3. M6 — в конце.


M0. Детерминизм мира

NET-00. Одинаковый мир у всех клиентов (1–2 дня)

  • Скоуп: доказать, что два запуска с одинаковыми (место, дата, время, погода, сид) дают одинаковые термики, облака и ветер; что атмосферу можно начать с произвольного времени зоны t и получить то же состояние, что при прогоне от 0; что это верно на Linux и Windows. Если нет — найти источники расхождений и исправить (или ограничить).
  • Ключевые файлы: scripts/atmosphere/atmosphere.gd, thermal_field.gd, wind_model.gd, cloud_physics.gd; scripts/game/game.gd (старт атмосферы, sim_time_s); scripts/world/sun_clock.gd; новый tests/atmosphere/test_determinism.gd.
  • Приёмка: тест сравнивает «отпечаток» (термики: позиция, сила, фаза; облака; ветер в 20 точках) в моменты 0 / 600 / 3600 с для двух независимых запусков и для «прыжка» сразу в t — совпадение до 1e-3; тот же отпечаток в Windows-сборке (Wine или вручную) — в отчёте; тест зелёный в tools/check.sh.

M1. Контракт сообщений

NET-10. net.proto — машиночитаемый контракт (1 день)

  • Скоуп: все сообщения с комментариями. Envelope{oneof}. Клиент → сервер: Hello{game_version, name}, CreateZone{zone}, JoinZone{code}, LeaveZone, Ping. Сервер → клиент: Welcome{your_id}, ZoneCreated{code}, ZoneJoined{zone, peers, leader_id}, PeerJoined, PeerLeft, LeaderChanged, Error{code, text}, Pong{server_time}. Пересылаемые внутри зоны: PilotState{pilot_id, is_bot, name, t, pos, rot, vel, phase, wing, colors} (10 Гц; ботов шлёт ведущий), ZoneState{clock, queue} (только ведущий, 1 Гц; очередь — живые пилоты, затем боты). Zone{location_id, pick_lat, pick_lon, month, day, start_hour, forecast, seed, bots_count}. Коды ошибок: ZONE_NOT_FOUND, VERSION_MISMATCH, ZONE_FULL, BAD_MESSAGE. Пакет deltaplan.v1; правила эволюции — только добавлять поля.
  • Ключевые файлы: server/proto/deltaplan/v1/net.proto, server/buf.yaml (или скрипт protoc), docs/guide/net-protocol.md (человеческое описание, пример proto3 JSON каждого сообщения, диаграммы последовательностей).
  • Приёмка: buf lint (или protoc) без ошибок; Go-код генерируется одной командой; в docs/guide/net-protocol.md — пример JSON каждого сообщения и 4 сценария: создать, войти, ведущий вышел, догнать; пользователь согласовал контракт.

M2. Сервер на Go (server/)

NET-20. Сервер и зоны (2 дня)

  • Скоуп: Go-модуль; WebSocket /v1/ws; Hello с проверкой версии игры; создание зоны → уникальный 4-значный код (случайный, без повторов среди активных); вход по коду; порядок подключения; ведущий = самый ранний; выход/обрыв → PeerLeft и при необходимости LeaderChanged; зона удаляется сразу, как из неё вышел последний живой пилот (код освобождается); пересылка PilotState/ZoneState остальным в зоне; не больше 16 в зоне; Ping/Pong с временем сервера. Без авторизации, всё в памяти.
  • Ключевые файлы: server/go.mod, server/cmd/deltaplan-server/main.go, server/internal/zone/, server/internal/ws/, сгенерированный код из M1.
  • Приёмка: go test ./... покрывает выдачу уникальных кодов, неверный код → ZONE_NOT_FOUND, неверную версию → VERSION_MISMATCH, смену ведущего при уходе первого, удаление зоны при уходе последнего пилота, пересылку только внутри своей зоны; go vet и staticcheck чисто; нагрузка 3 зоны × 10 клиентов × 10 Гц 10 мин — память стабильна.

NET-21. Развёртывание на VPS (0,5–1 день)

  • Скоуп: статический бинарник (make), адрес и порт — флаги/переменные окружения, логи в stdout (journald), systemd-юнит, /healthz (200) и /v1/status (JSON: зоны, участники — для отладки), инструкция.
  • Ключевые файлы: server/Makefile, server/deploy/deltaplan-server.service, server/README.md.
  • Приёмка: по README сервер поднимается на чистом VPS за ≤ 10 мин; переживает перезагрузку VPS; curl /healthz = 200.

NET-22. Встроенный сервер в игре (1,5 дня)

  • Скоуп: сервер на GDScript внутри игры по тому же контракту (net.proto, proto3 JSON поверх WebSocket, тот же путь /v1/ws и порт по умолчанию): TCPServer + WebSocketPeer (серверная сторона), коды зон, порядок подключения, ведущий, пересылка, Ping/Pong. «Создать» без адреса сервера → запуск встроенного сервера и подключение к нему же (127.0.0.1); остальные подключаются по локальному адресу. Выход создателя → сервер останавливается, зона закрывается (остальным — disconnected).
  • Ключевые файлы: scripts/net/local_server.gd (новый), scripts/net/net_zone.gd, tests/net/test_local_server.gd.
  • Приёмка: те же интеграционные тесты клиента (NET-30/31/32), что гоняются против Go-сервера, проходят и против встроенного (параметризовать тесты сервером); 3 клиента на одной машине через встроенный сервер — создание, вход, пересылка состояний.

NET-23. Поиск зон в локальной сети (0,5 дня)

  • Скоуп: пока идёт зона на встроенном сервере — объявление раз в секунду UDP broadcast на фиксированный порт: LanAnnounce{code, host_name, address, port, game_version, pilots_count} (добавить в net.proto, контракт только расширяется); у клиентов — слушатель и список зон рядом (пропадает через 3 с без объявлений); зоны другой версии — помечены.
  • Ключевые файлы: server/proto/deltaplan/v1/net.proto, docs/guide/net-protocol.md, scripts/net/lan_discovery.gd (новый), tests/net/test_lan_discovery.gd.
  • Приёмка: два процесса на одной машине: второй видит зону первого за ≤ 2 с, после остановки — пропадает за ≤ 4 с; сообщение описано в контракте с примером.

M3. Клиент сети в игре (scripts/net/)

NET-30. Соединение (1 день)

  • Скоуп: автозагрузка NetClient: подключение ws://адрес/v1/ws (WebSocketPeer), Hello, кодирование/разбор сообщений по контракту (proto3 JSON), переподключение при обрыве (3 попытки, затем ошибка), Ping раз в 2 с → задержка и смещение часов сервера; сигналы connected, disconnected, error(code, text), message(type, data).
  • Ключевые файлы: scripts/net/net_client.gd, scripts/net/net_messages.gd, project.godot (автозагрузка), tests/net/test_messages.gd.
  • Приёмка: тест кодирования/разбора каждого сообщения против примеров из docs/guide/net-protocol.md; интеграционный тест с локальным Go-сервером: подключение, Hello, Ping/Pong; остановка сервера → disconnected и повторы.

NET-31. Зона и ведущий (1 день)

  • Скоуп: NetZone: создать/войти; параметры зоны ↔ FlightSettings (+ сид); список пилотов, кто ведущий; у ведущего — часы зоны и очередь, рассылка ZoneState 1 Гц; у остальных — применение; при LeaderChanged новый ведущий продолжает с последнего ZoneState без скачка часов.
  • Ключевые файлы: scripts/net/net_zone.gd, scripts/game/flight_settings.gd, tests/net/test_zone.gd.
  • Приёмка: тест: три клиента и сервер; ведущий уходит → ведущий второй, часы зоны без скачка > 0,2 с, очередь сохранилась.

NET-32. Состояния пилотов (1 день)

  • Скоуп: отправка своего состояния 10 Гц (позиция, ориентация, скорость, фаза: стоит/идёт/разбег/летит/сел; крыло; расцветка); приём; буфер интерполяции ~150 мс; экстраполяция до 1 с при потерях; «пропал» после 5 с.
  • Ключевые файлы: scripts/net/remote_pilot_state.gd, scripts/net/net_zone.gd, tests/net/test_interpolation.gd.
  • Приёмка: при 10 Гц и джиттере ±50 мс траектория гладкая (без рывков > 0,5 м между кадрами на 30 км/ч); при потере 20 % пакетов без скачков; трафик клиента ≤ 2 КБ/с.

M4. Игра в сети (scripts/game/)

NET-40. Сетевой режим полёта (1 день)

  • Скоуп: Game.start из параметров зоны; атмосфера с сидом и временем зоны (по итогам M0); время только ×1; пауза не останавливает мир (только меню); ускорение времени отключено; после посадки, аварии или сорванного старта — окно итога со статистикой и кнопками «Продолжить рядом» (в фокусе; → «догнать» NET-42) и «На старт» (в конец очереди); в воздухе никого — только «На старт».
  • Ключевые файлы: scripts/game/game.gd, scripts/game/launch_options.gd, scripts/world/sun_clock.gd, scenes/main.gd, scripts/ui/result_screen.gd (кнопки «Продолжить рядом», «На старт»).
  • Приёмка: два клиента видят одинаковые облака в одной точке неба (скриншоты рядом); пауза у одного не останавливает время у другого; одиночная игра не изменилась (прежние тесты зелёные).

NET-41. Чужие пилоты в мире (1–1,5 дня)

  • Скоуп: RemotePilots — чужие пилоты тем же видом, что боты (крыло с расцветкой, пилот, позы по фазе, имя над крылом); появление/исчезновение; вид на старте.
  • Ключевые файлы: scripts/game/remote_pilots.gd (новый), scripts/game/bot_glider.gd (вид), scripts/game/bot_pilots.gd (вывод имён).
  • Приёмка: скриншоты: чужой пилот стоит на старте, разбегается, кружит, садится; имя видно; 10 чужих пилотов — FPS падает не больше чем на 5 %.

NET-42. «Догнать» (2 дня)

  • Основные случаи (под них и проверять):
    1. «Отошёл за кофе» — друг уже в воздухе, я на земле (на старте, в очереди, на посадке) → взлететь и оказаться рядом с ним.
    2. «Поднять к другу» — я безнадёжно потерял высоту (низко над склоном, в долине, уже сел или разбился), друг высоко → поднять меня к нему; обычно несколько км и сотни–тысяча м набора.
  • Скоуп:
    • Меню. Клавиша = (переназначаемая, на геймпаде — своя кнопка) открывает поверх полёта быстрое полупрозрачное меню: список пилотов зоны кроме себя — сначала живые, потом боты; по умолчанию выделен ближайший живой пилот в воздухе, так что при двоих в зоне хватает = → Enter; в строке имя, высота, расстояние, «на старте»/«в воздухе». Полёт не останавливается, управление крылом на время меню заблокировано (крыло летит само, как при отпущенной трапеции). Стрелки вверх/вниз — выбор, Enter — догнать, Esc или повторный = — закрыть без действия. Список обновляется на лету; ушедший пилот исчезает из списка.
    • Перелёт, а не телепорт. Крыло переходит в «буксир»: кинематическое движение без физики полёта (физика на это время выключена, столкновения и сваливание не считаются). В плане — по прямой на текущую позицию цели (преследование: цель движется — курс подправляется). По высоте — огибание рельефа сверху: высота = max(высота цели, рельеф впереди по курсу + 50 м запаса), дальность просмотра и плавность — см. «Скорость — купол». Обход склонов сбоку (поиск пути) не делаем — перелёт поверх проще и надёжнее. Пилот в позе полёта, крыло с креном в поворотах, звук потока по скорости. Другие видят перелёт как обычное движение (идёт тот же PilotState).
    • Конец перелёта. В ~60 м позади-сбоку цели: скорость и курс плавно сводятся к скорости цели за 2–3 с, затем управление и физика возвращаются пилоту (крыло на триммерной скорости, без сваливания). Отмена — Esc или =: физика возвращается на месте. Цель ушла из зоны/села → перелёт останавливается и управление возвращается.
    • С земли — откуда угодно. Игрок на старте, в очереди, на посадке или после аварии, цель в воздухе → буксир поднимает крыло с места, где он стоит, по той же схеме (из очереди игрок при этом выходит; окно итога полёта закрывается). Цель на земле → вместо перелёта встать в конец очереди на старт.
    • Из окна итога — кнопка «Продолжить рядом» запускает буксир к ближайшему другу в воздухе (несколько — сначала меню со списком).
    • Высота прибытия — высота цели, даже если это +1000 м: набор идёт тем же плавным профилем, что и скорость.
    • Автоматически при входе в зону, если ведущий в воздухе: сразу перелёт от старта к нему.
    • Скорость — «купол»: смысл — в середине перелёта можно лететь очень быстро, а разгон и торможение плавные, чтобы не было дискомфорта. Комфорт задают не скорость, а плавность: разгон и торможение по 2,5 с по кривой smootherstep (в начале и в конце ускорение нулевое, без рывка), потолок v — посередине.
      • Путь такого профиля d = v·(T − t_a), поэтому v = d / (T − t_a) = d / 7,5 с при T = 10 с, t_a = 2,5 с — любое расстояние за ~10 с.
      • Потолок v_max — не «реализм», а технический предел: сколько выдерживает подгрузка мира (рельеф, лес, трава, камни) без рывков кадра и пустой земли. Подобрать тестом; начальное значение 1000 км/ч (за 10 с — ~2 км; 5 км ≈ 20 с). Выше предела время растёт: T = d / v_max + t_a.
      • d — расстояние до точки прибытия (60 м позади-сбоку цели), пересчитывается каждый кадр; v — скорость относительно цели (в конце летим ровно её скоростью); торможение начинается, когда d ≤ v·t_a/2.
      • Вертикаль: огибание рельефа сверху с просмотром вперёд на v·t_a… v·5 с по курсу и тем же плавным профилем по высоте (без резких «горок»).
      • Все числа — в конфиге (net.catch_up: t_target_s 10, ramp_s 2,5, v_max_kmh 1000 — уточнить тестом подгрузки, clearance_m 50, arrive_offset_m 60).
  • Ключевые файлы: scripts/ui/catch_up_menu.gd + scenes/ui/catch_up_menu.tscn, scripts/game/catch_up_tow.gd (новые: буксир, высота над рельефом по Terrain.height_at), scripts/game/game.gd (выключение/возврат физики полёта), scripts/game/start_placement.gd, scripts/game/input_controller.gd, configs/controls.json, locale/ui.csv.
  • Приёмка: скриншоты меню ru/en поверх полёта (из кабины и снаружи) и кадры перелёта; тесты двух основных случаев: (1) с посадочной площадки к другу на 1500 м над стартом, (2) из долины в 50 м над рельефом к другу на 2 км в стороне и на 1000 м выше — прибытие рядом, на его высоте, без касания рельефа; тест профиля: 300 м и 2 км — прибытие за 10 ± 0,5 с, дальше предела — потолок v_max; ускорение на старте и в конце разгона/торможения ≈ 0, без скачков; тест подгрузки мира на v_max: нет рывков кадра > 50 мс и «пустой» земли — по нему выбран v_max; = → Enter доводит до 30–80 м от цели, после передачи управления крыло летит без сваливания; тесты: перелёт через хребет выше цели — ни разу не ближе 30 м к рельефу; цель разворачивается — догоняем; отмена Esc — физика возвращается на месте; себя в списке нет; живые выше ботов.

NET-43. Очередь на старт (1–1,5 дня)

  • Скоуп: живая очередь как у ботов: места ожидания позади старта; сначала живые пилоты, после них боты; первый может разбегаться, у остальных разбег заблокирован; после взлёта первого следующий шагает вперёд; простой первого > 60 с → в конец; посадка/авария/сорванный старт → в конец текущей очереди (пустая — сразу первым); очередь ведёт ведущий (ZoneState.queue).
  • Ключевые файлы: scripts/game/bot_pilots.gd (места очереди), scripts/game/start_placement.gd, scripts/game/input_controller.gd, scripts/net/net_zone.gd.
  • Приёмка: 3 клиента взлетают строго по очереди, боты — после них; застрявший первый через 60 с уходит в конец; «На старт» после посадки/аварии ставит в конец; смена ведущего во время очереди её не ломает.

NET-44. Боты в сетевой зоне (1–1,5 дня)

  • Скоуп: число ботов — из настроек создателя (Zone.bots_count), в сети столько же, как в одиночной игре; ботов считает только ведущий (тот же BotPilots, «наблюдает» всех живых пилотов, а не одного игрока) и рассылает их PilotState (is_bot); у остальных боты рисуются как чужие пилоты (NET-41); при смене ведущего новый продолжает ботов с последних полученных состояний, число не пересчитывается; имена ботов — из пула языка ведущего и не меняются при смене ведущего.
  • Ключевые файлы: scripts/game/bot_pilots.gd, scripts/game/bot_agent.gd, scripts/net/net_zone.gd, scripts/game/remote_pilots.gd.
  • Приёмка: 2 клиента + 4 бота: у обоих одинаковые боты в одних местах (расхождение ≤ 5 м); ведущий выходит — боты продолжают летать без исчезновения и скачков, их по-прежнему 4.

M5. Интерфейс (scripts/ui/)

NET-50. Экран «Сетевая игра» (1 день)

  • Скоуп: кнопка «Сетевая игра» в главном меню → экран: поле «Сервер» (IP:порт или имя; запоминается в профиле), «Создать» (выбор места — как в «Полёт…»), «Присоединиться» (поле кода — строка); после создания — код крупно и «кто в зоне»; ошибки простым текстом (сервер недоступен, нет такой зоны, другая версия игры); ru/en.
  • Ключевые файлы: scripts/ui/net_screen.gd, scenes/ui/net_screen.tscn (новые), scripts/ui/start_menu.gd, scenes/main.gd, scripts/game/user_settings.gd, locale/ui.csv.
  • Приёмка: скриншоты ru/en всех состояний (ввод, подключение, код, список, каждая ошибка); тест: адрес сервера сохраняется между запусками.

NET-51. Имя пилота (0,5 дня)

  • Скоуп: поле «Имя пилота» в настройках (по умолчанию «Пилот»/“Pilot”, ≤ 20 символов), уходит в Hello, другие видят его над крылом.
  • Ключевые файлы: scripts/ui/settings_panel.gd, scripts/game/user_settings.gd, locale/ui.csv.
  • Приёмка: имя сохраняется; кириллица и латиница видны у другого клиента.

NET-52. Пауза и загрузка в сети (0,5 дня)

  • Скоуп: в паузе — код зоны, список пилотов зоны (имя, высота, ведущий) и «Выйти из зоны» («догнать» — только через меню =, NET-42); на экране загрузки — код зоны и кто в зоне.
  • Ключевые файлы: scripts/ui/pause_menu.gd, scripts/ui/loading_screen.gd, locale/ui.csv.
  • Приёмка: скриншоты ru/en; «Выйти из зоны» → главное меню, у других пилот исчезает.

NET-53. «Рядом» на экране «Сетевая игра» (0,5 дня)

  • Скоуп: на экране «Сетевая игра» — список «Рядом: зона 4721 — Коля» (из NET-23), выбор стрелками, Enter — войти; «Создать» без адреса сервера — на встроенном сервере (NET-22), с адресом — на указанном сервере; поле адреса остаётся для VPS; ru/en.
  • Ключевые файлы: scripts/ui/net_screen.gd, scenes/ui/net_screen.tscn, locale/ui.csv.
  • Приёмка: скриншоты ru/en: пустой список, 1–2 зоны рядом, зона другой версии; вход в зону из списка одним Enter.

M6. Проверка и выпуск

NET-60. Сквозной тест и прогон (1 день)

  • Скоуп: скрипт: локальный сервер + 3 клиента (headless, автопилот) и 4 бота: создать, войти, очередь, взлёт, «догнать», уход ведущего, выход всех → зона закрыта, 30 мин полёта; метрики: задержка, трафик, расхождение облаков.
  • Ключевые файлы: tools/net/e2e.sh, tests/net/.
  • Приёмка: e2e зелёный; 30 мин без ошибок и утечек; отчёт с метриками.

NET-61. Проверка через интернет и документация (1 день)

  • Скоуп: сервер на VPS; двое игроков в разных сетях (в т. ч. мобильный интернет); docs/net.md (как играть, как поднять сервер); CHANGELOG, devlog.
  • Ключевые файлы: docs/net.md, server/README.md, CHANGELOG.md, site/content/releases/<версия>/.
  • Приёмка: пользователь с пилотами пролетели вместе ≥ 20 мин; замечания записаны; feature/multiplayer слита в main.

Оценка (дни работы агента)

МодульДни
M0 Детерминизм1–2
M1 Контракт1
M2 Сервер Go + развёртывание2,5–3
Встроенный сервер и поиск в локальной сети (NET-22, NET-23, NET-53)2,5
M3 Клиент сети3
M4 Игра в сети (режим, чужие пилоты, догнать, очередь, боты)5,5–6,5
M5 Интерфейс2
M6 Проверка и выпуск2
Итого~19–22

Минимальная версия (M0–M3, NET-40/41/42, NET-50/51; старт без очереди, без ботов) — ~10 дней.

Риски

  • Детерминизм на разных ОС — M0 первым; если не сходится, решить до остального.
  • Точки с карты: разный кеш OSM → чуть разные дома/ЛЭП (на полёт почти не влияет).
  • Разные версии игры в одной зоне — сервер не пускает (VERSION_MISMATCH).
  • Один сервер: VPS недоступен → сетевой игры нет (одиночная работает).

На потом

  • WebRTC-mesh вместо ретранслятора (VPS — сигналинг/TURN).
  • Бинарный protobuf на проводе.