План: сетевая игра — роадмап
Состояние на 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-бота и прочих сервисов нет.
Как это выглядит для игрока
Основной случай — друзья полетали вместе. Код зоны пилоты передают друг другу сами (голосом, в мессенджере) — игра этим не занимается.
- «Сетевая игра» в главном меню → экран: адрес сервера (
IP:порт, запоминается), своё имя пилота (из настроек). - «Создать»: выбирается только место; крыло, масса, время, погода — из «Полёт…». Сервер выдаёт 4-значный код (например,
4721) — его диктуют/пересылают друзьям. - «Присоединиться»: ввод кода. Мир (место, дата, время, погода, сид) берётся у зоны. Ведущий ещё на земле → встаёшь в очередь на старт; уже летит → автоматически «догнать» (появляешься в воздухе рядом с ним).
- Ведущий — первый подключившийся; вышел → ведущий следующий по порядку подключения. Незаметно для игроков.
- «Догнать» в любой момент: клавиша
=→ поверх полёта быстрое полупрозрачное меню со списком пилотов (кроме себя) → стрелками выбрать,Enter— крыло само летит к нему (обычно ~10 с: плавный разгон, быстро в середине, плавное торможение) и отдаёт управление рядом с целью. - Боты. Число ботов берётся из настроек создателя зоны (ведущего на момент создания) и в сети такое же, как в одиночной игре. Боты всегда стоят в очереди после живых пилотов. При смене ведущего число ботов не пересчитывается.
- После полёта (посадка, авария, сорванный старт) — обычное окно итога со статистикой, в нём главная кнопка «Продолжить рядом» (в фокусе:
Enter— и буксир везёт к ближайшему другу в воздухе; друзей в воздухе несколько — открывается меню «Догнать» со списком) и «На старт» (в конец текущей очереди; пустая — сразу первым). В воздухе никого — «Продолжить рядом» не показывается. Уход в конец очереди после сорванного старта — намеренно: лёгкое наказание за неудачный старт и живая очередь, как на настоящем старте. Не «чинить»; менять, только если пожалуются сами игроки. Обходных путей (пропуск очереди, «вне очереди») не делаем: если очередь мешает, игроки создают зону без ботов (0 в настройках) и взлетают друг за другом. - Конец игры. Зона закрывается, когда из неё вышли все живые пилоты (боты не держат зону); код освобождается сразу.
Главный случай: одна комната (добавлено 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, порядок волн, приёмка у пользователя, слияние в main | M6 | NET-60, NET-61 | 2 |
| К1. Сеть — контракт, сервер, клиент | M1, M2, M3 | NET-10; NET-20; NET-21 (Sonnet); NET-22; NET-23; NET-30; NET-31; NET-32 | 8 |
| К2. Игра в сети — мир, режим, чужие пилоты, «догнать», очередь, боты | M0, M4 | NET-00; NET-40; NET-41; NET-42 ×2 (меню + буксир); NET-43; NET-44 | 7 |
| К3. Интерфейс | M5 | NET-50; NET-51 (Sonnet); NET-52 (Sonnet); NET-53 | 4 |
| Итого | 4 координатора | 21 исполнитель |
Волны (одновременно — не больше ~5 исполнителей):
- NET-00, NET-10.
- NET-20, NET-21, NET-30, NET-50 (на заглушках), NET-51.
- NET-31, NET-32, NET-41 (на поддельных состояниях), NET-52.
- NET-40 → затем NET-42 (меню и буксир — параллельно) и NET-43 (правят
game.gd— по очереди, через К2). - NET-44.
- 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(+ сид); список пилотов, кто ведущий; у ведущего — часы зоны и очередь, рассылкаZoneState1 Гц; у остальных — применение; при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 дня)
- Основные случаи (под них и проверять):
- «Отошёл за кофе» — друг уже в воздухе, я на земле (на старте, в очереди, на посадке) → взлететь и оказаться рядом с ним.
- «Поднять к другу» — я безнадёжно потерял высоту (низко над склоном, в долине, уже сел или разбился), друг высоко → поднять меня к нему; обычно несколько км и сотни–тысяча м набора.
- Скоуп:
- Меню. Клавиша
=(переназначаемая, на геймпаде — своя кнопка) открывает поверх полёта быстрое полупрозрачное меню: список пилотов зоны кроме себя — сначала живые, потом боты; по умолчанию выделен ближайший живой пилот в воздухе, так что при двоих в зоне хватает=→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_s10,ramp_s2,5,v_max_kmh1000 — уточнить тестом подгрузки,clearance_m50,arrive_offset_m60).
- Путь такого профиля
- Меню. Клавиша
- Ключевые файлы:
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 на проводе.