320 lines
26 KiB
Markdown
320 lines
26 KiB
Markdown
# Базовая версия — чеклист реализации
|
||
|
||
Практический план: что писать, в каком порядке и когда считать готовым.
|
||
Дизайн-решения и обоснования — в `DEVELOPMENT_CHECKLIST.md`. Данные карт — в `content/cards-mvp.md`.
|
||
|
||
---
|
||
|
||
## 0. Границы базовой версии
|
||
|
||
**Входит:**
|
||
- Платформа: Windows. Unity + WPF-лаунчер.
|
||
- Формат: `Classic`, один сет — BT01 + TD01 + TD02 (94 карты).
|
||
- Аккаунт, коллекция, полный декбилдер, магазин бустеров и триал-дек.
|
||
- 2 очереди: Classic Ranked и Classic Casual. Матч 1×1, BO1.
|
||
- Полный игровой цикл: матч → результат → Gold/XP → бустер → карта в коллекцию.
|
||
- Базовый туториал + выдача триал-деки и 5 токенов.
|
||
- Локализация RU + EN.
|
||
- Лаунчер с проверкой версии и докачкой.
|
||
|
||
**Не входит:** турниры, команды, территории, чат, друзья, баттлпасс, квесты, Draft, косметика, мобилка, реплеи в клиенте (только серверная запись).
|
||
|
||
### Стек (зафиксирован)
|
||
|
||
| Слой | Технология |
|
||
|---|---|
|
||
| Сервер | **.NET 10 LTS**, ASP.NET Core, Orleans, EF Core, PostgreSQL 16, Redis 7 |
|
||
| Протокол матча | TCP + MessagePack, кадры с префиксом длины |
|
||
| Мета-API | HTTP/JSON, JWT |
|
||
| Клиент | Unity 6 LTS, URP, Addressables, Input System, Unity Localization, DOTween |
|
||
| Лаунчер | WPF, **.NET 10 LTS**, CommunityToolkit.Mvvm |
|
||
| Контейнеры | Docker Compose, nginx |
|
||
|
||
### Оценка (один разработчик, полный день)
|
||
|
||
| Этап | Дни |
|
||
|---|---|
|
||
| 1. Репозиторий и каркас | 2–3 |
|
||
| 2. Контент-пайплайн | 4–6 |
|
||
| 3. Игровое ядро | 20–30 |
|
||
| 4. Скрипты 94 карт | 15–25 |
|
||
| 5. Сеть и матч | 8–12 |
|
||
| 6. Мета-API | 10–14 |
|
||
| 7. Клиент: каркас и меню | 15–20 |
|
||
| 8. Клиент: боевая сцена | 20–30 |
|
||
| 9. Туториал | 5–7 |
|
||
| 10. Лаунчер | 5–7 |
|
||
| 11. QA и сборка | 7–10 |
|
||
| **Итого** | **~110–165** |
|
||
|
||
---
|
||
|
||
## 1. Репозиторий и каркас `[2–3 дня]`
|
||
|
||
- [x] `git init`, ветка `master`.
|
||
- [x] `.gitignore`: Unity (`Library/`, `Temp/`, `Logs/`, `UserSettings/`, `Builds/`), .NET (`bin/`, `obj/`), WPF (`publish/`), секреты (`.env`, `appsettings.Production.json`).
|
||
- [x] Отдельно игнорировать только Unity-генерируемые `*.csproj`/`*.sln` в `/client`, чтобы не убить `server/*.sln`.
|
||
- [x] `.gitattributes`: `* text=auto eol=lf`, `*.unity`, `*.prefab`, `*.asset` → `merge=unityyamlmerge`, `*.png`/`*.wav` → `binary`.
|
||
- [x] Структура (на 20.10.2026 — `server`, `content`, `tools`; `client` и `launcher` появятся со своими блоками):
|
||
```
|
||
/server
|
||
Vanguard.slnx
|
||
/src/Vanguard.Rules чистое ядро правил, без Orleans и БД
|
||
/src/Vanguard.Content загрузчик и валидатор контента
|
||
/src/Vanguard.Shared контракты, DTO, MessagePack
|
||
/src/Vanguard.Cli headless-раннер матчей
|
||
/tests/Vanguard.Rules.Tests
|
||
/content/sets/BT01.yaml
|
||
/content/sets/TD01.yaml
|
||
/content/sets/TD02.yaml
|
||
/tools/import_cards.py
|
||
```
|
||
- [x] `Directory.Build.props`: `net10.0`, `Nullable=enable`, `LangVersion=latest`.
|
||
- [x] `docker-compose.yml`: postgres, redis (AOF), game-server с контентом.
|
||
- [x] `docker compose up` поднимает БД, Redis и API; `/health` отвечает.
|
||
- [ ] CI: сборка solution + `dotnet test` на push.
|
||
|
||
**Готово, когда:** `docker compose up` + `dotnet run` дают живой `/health`.
|
||
|
||
---
|
||
|
||
## 2. Контент-пайплайн `[4–6 дней]`
|
||
|
||
- [x] Схема YAML: `printingId`, `nameId`, `cardType`, `grade`, `power`, `shield`, `critical`, `triggerType`, `clan`, `rarity`, `isSentinel`, `isRestraint`, `name_en/ru`, `text_en/ru`, `imageKey`.
|
||
- [x] Импорт с официального сайта → три YAML-файла (BT01, TD01, TD02). 94 карты, 124 принта.
|
||
- [x] Валидатор:
|
||
- [x] `sum(deck) = 50`, `≤4` по `nameId`, `≤4` Sentinel, `≤4` Heal.
|
||
- [x] Инвариант щита: grade 0 → 5000 **только** у Draw, иначе 10000.
|
||
- [x] Уникальность `printingId`, наличие `nameId`.
|
||
- [x] Ссылка SP-принта на существующий базовый номер.
|
||
- [x] Загрузчик: YAML → in-memory каталог, используется и сервером, и тестами.
|
||
- [x] Ключевые слова и шаблоны: `content/keywords.yaml`, `content/translations/ru.yaml`, команды `translations build|check`.
|
||
- [ ] Сид в PostgreSQL: `cards`, `printings`, `sets`, `card_legality` (пустая).
|
||
- [ ] Экспорт каталога для клиента (JSON + список Addressables-ключей).
|
||
|
||
**Готово:** `dotnet run --project server/src/Vanguard.Cli -- validate` → `94 карты, 124 принта, 0 ошибок`.
|
||
|
||
---
|
||
|
||
## 3. Игровое ядро `[20–30 дней]` — критический путь
|
||
|
||
Пишется **как чистая библиотека**: без Orleans, без БД, без сети. Вход — состояние + интент, выход — новое состояние + события.
|
||
|
||
- [x] `GameState`: игроки, зоны, фазы, счётчики, `rngState`.
|
||
- [x] Зоны: Vanguard, Rear-Guard (3 колонки × 2 ряда), Guardian, Soul, Damage, Drop, Trigger, Deck, Hand.
|
||
- [x] Юнит: `instanceId`, `printingId`, состояние Stand/Rest.
|
||
- [x] Damage: список карт с `faceUp`, скрыт от обоих игроков.
|
||
- [x] Фазы: Mulligan → Stand → Draw → Ride → Main → Battle → End.
|
||
- [x] Старт: выбор grade 0 → выставление → маллиган.
|
||
- [x] Действия: Ride, Call, Attack, Guard, Drive, Damage, Stand/Rest, Retire, Draw.
|
||
- [x] Поддержка (Boost) как отдельная способность: по умолчанию у 0–1 ранга, передаёт силу карте впереди в той же колонне; `BoostOverride` позволяет выдать или снять её эффектом.
|
||
- [x] Идеальная защита (Sentinel): щит 0; оплата сбросом карты того же клана полностью отменяет попадание; без оплаты не даёт ничего.
|
||
- [x] Перехват: карта 2 ранга из переднего ряда выходит в круг защиты, сила атаки сравнивается с ней; `InterceptOverride` и `InterceptFromBackRow` позволяют выдать способность эффектом.
|
||
- [x] Перемещение Арьергарда в пределах колонки; если круг занят — карты меняются местами; у центрального заднего ряда пары нет.
|
||
- [ ] Discard, Shuffle как отдельные интенты.
|
||
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost, OnBoosted, OnDriveCheck, OnLeaveGuardian, OnPhaseStart, OnAttackDeclared, OnAttackHit), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
||
- [ ] Событие OnDamageTaken и триггеры на получение урона.
|
||
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
|
||
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
||
- [x] Стоимости: CB, SB, сброс карты, поворот себя, уничтожение себя; «карты из поля в душу» и «карта из руки в колоду» — частными хелперами.
|
||
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» — `AskUnitMultiChoice`.
|
||
- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
|
||
- [x] Модификаторы силы/крита с источником и сроком жизни (`StatModifier`); выдача и снятие способностей эффектами (`GrantedAbility`).
|
||
- [x] Выбор от `min` до `max` целей на поле (`AskUnitMultiChoice`) для «до N» и «ровно N».
|
||
- [x] Запреты с источником и сроком жизни (`CardRestriction`): не вставать, не атаковать, не поддерживать, не перехватывать, не быть выбранным; снятие Сдерживания эффектом.
|
||
- [x] `OnAttackHit` срабатывает и при попадании по Арьергарду.
|
||
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
||
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API готов; Llew и Aleph ждут стоимость «карты из поля в душу»).
|
||
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
||
- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет.
|
||
- [x] Валидация интентов: `Rejected(reason)` без изменения состояния.
|
||
- [x] Список легальных интентов на каждый момент (для UI и античита).
|
||
— `GameEngine.LegalCommands()` покрывает фазы, выборы, триггеры и активируемые способности.
|
||
- [~] Детерминированный RNG: один поток на партию, состояние в `GameState`.
|
||
- [x] Журнал событий + seed; сериализуемый реплей.
|
||
— `MatchRecord`, `MatchRecorder`, `CommandCodec`, `ReplayRunner`, `EventHasher`; запись переживает JSON и воспроизводится бит-в-бит по хэшу событий.
|
||
- [~] Защита от бесконечных циклов и дедлоков.
|
||
— 50 партий случайного бота в тестах и 1000 через `stress`; лимит 5000 шагов на партию в стресс-тесте. Глобального лимита шагов внутри движка нет.
|
||
- [x] Инварианты партии: 50 карт в зонах каждого игрока, уникальные `instanceId`, урон ≤ 6.
|
||
- [x] Карты «в пути» между зонами (`GameState.InTransit`) — например, найденная карта ждёт выбора круга.
|
||
|
||
**Сделано:** каркас движка (`Engine/`), все фазы, бой, способности, 66/66 карт, `LegalCommands`, случайный бот (1000 партий через `stress`), реплей с хэшем событий. Всего 117 тестов.
|
||
|
||
**Готово, когда:** два бота случайными легальными интентами доигрывают 1000 партий без падений, инварианты (сумма карт, счётчики ≥ 0) держатся, реплей воспроизводится бит-в-бит.
|
||
— выполнено: `dotnet run --project server/src/Vanguard.Cli -- stress 1000` → 1000/1000 без ошибок; реплей проверяется тестом.
|
||
|
||
---
|
||
|
||
## 4. Карты `[15–25 дней]`
|
||
|
||
- [ ] Хелперы: `Draw`, `Retire`, `SearchCall`, `SearchHand`, `SearchRide`, `Stand`, `SoulCharge`, `SoulBlast`, `CounterBlast`, `FlipDamage`, `ToSoul`, `ToDeck`, `Discard`, `LookTop`, `ModifyPower`, `ModifyCrit`, `GainKeyword`.
|
||
— большая часть уже есть в `GameEngine`; не хватает общих «карты из поля в душу» и «из руки в колоду» с выбором любых карт.
|
||
- [~] Шаблон карты: скрипт = класс `CardScript`, текст карты хранится в YAML (отдельного комментария с текстом нет).
|
||
- [x] Порядок: сначала простые триггеры, затем стоимости и активируемые способности.
|
||
- [x] По каждой карте: скрипт + тест «дано → сыграл → ожидаю» (для ключевых механик).
|
||
- [x] RU/EN тексты для всех 94.
|
||
- [x] Сила/щит для 14 карт, которых нет в BT01.
|
||
- [ ] Дашборд покрытия: `scripted / tested / total`.
|
||
— **66 из 66** карт с текстом закрыты: 59 скриптов + Tejas (флаг атаки заднего ряда) + 6 карт Sentinel/Heal, которые обрабатывает движок. Оставшихся карт нет.
|
||
- [x] Триггеры `OnDriveCheck` (Asura Kaiser, Brigitte, Tristan, Goku), `OnBoostedHit` (Dreadmaster, Aermo), `OnGuard` (Hagakure), `OnRetireFromRC` (Blightops), `OnOpponentRetire` (Joka, Rakshasa, Yaksha).
|
||
- [x] Способности из руки (Gancelot, Yaksha) и из сброса (Monster Frank).
|
||
- [x] Возврат карты с поля в колоду: после боя (Juggernaut, Brakki) и в конце хода (Lozenge, Battleraizer).
|
||
- [x] Триггер `OnLeaveGuardian` (Rock the Wall, Redshoe Milly).
|
||
- [x] Wyvern Strike, Tejas: атака заднего ряда только из своей колонки; обычные карты атакуют только передний ряд.
|
||
|
||
**Готово, когда:** 94/94 заскриптованы, 94/94 покрыты тестом, приёмка пройдена по чеклисту карты.
|
||
|
||
---
|
||
|
||
## 5. Сеть и матч `[8–12 дней]`
|
||
|
||
- [x] Спецификация протокола в `docs/protocol.md`: кадры, handshake, версия, ping/pong, reconnect.
|
||
- [x] `Vanguard.Shared`: DTO интентов и событий, MessagePack-резолвер.
|
||
- [x] `IGameTicketService`: одноразовый тикет на подключение (TTL 60 с).
|
||
- [x] Транспорт матча: WebSocket (ASP.NET Core); резерв — TCP с префиксом длины. Accept, handshake, auth по тикету, привязка к grain.
|
||
- [x] `IMatchGrain`: 1 матч = 1 grain, последовательная обработка.
|
||
— реализовано in-process; Orleans-хостинг — следующий шаг, контракт отделён.
|
||
- [x] Проекция состояния под клиента: своя рука, свой trigger, открытые зоны; **ничего** о колоде, руке противника и закрытом уроне.
|
||
- [x] `eventSeq` + `resync` по запросу.
|
||
- [x] Реконнект: 0 / 2 / 10 / 30 с, затем экран «не удалось».
|
||
- [x] Политика отсутствия: 5 минут → авто-поражение.
|
||
- [x] Таймер хода: 90 с + банк 5 мин.
|
||
- [x] Снапшот матча в PostgreSQL + восстановление grain после рестарта.
|
||
— заголовок + журнал интентов, реплей команд при старте.
|
||
- [x] Redis: одноразовые тикеты и очередь.
|
||
- [x] Idempotency по `intentId`.
|
||
- [x] HTTP-очередь и тикеты: `docs/server-api.md`.
|
||
- [x] E2E: два клиента играют полный матч по WebSocket; обрыв и возврат проверены.
|
||
|
||
**Готово, когда:** два клиента играют полный матч по сети; kill процесса клиента на любом шаге восстанавливается.
|
||
— выполнено в `Vanguard.Server.Tests`; рестарт сервера восстанавливает матчи из PostgreSQL (проверено на `ssh home`).
|
||
|
||
---
|
||
|
||
## 6. Мета-API `[10–14 дней]`
|
||
|
||
- [x] Модель `User`, BCrypt, JWT (access 15 мин + refresh 30 дней, ротация).
|
||
- [x] Эндпоинты: `register`, `login`, `refresh`, `logout`, `me`.
|
||
- [x] Коллекция: `GET /collection` (сгруппировано по принтам), счётчики без верхней границы.
|
||
- [x] Колоды: CRUD, `validate`, лимит 30 на формат, шаринг по коду.
|
||
- [x] Магазин: `GET /store`, `POST /buy/booster`, `POST /buy/trialdeck`, `POST /redeem/token`.
|
||
- [x] Генератор бустера: слот `4 × C` + `1 × R+` (R 70 / RR 22 / RRR 7 / SP 1).
|
||
- [x] Duplicate protection по `printingId`, замена внутри редкости, переброс редкости при её насыщении.
|
||
- [x] Экономика: журнал транзакций, `idempotencyKey`, баланс вычисляемый.
|
||
- [x] Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт ×4, SP не крафтится.
|
||
- [x] Онбординг: выбор триал-деки + 5 токенов, флаг «выбор сделан».
|
||
- [x] Матчмейкинг: 2 очереди, скрытый MMR, расширяющееся окно; с JWT по `deckId`.
|
||
- [x] Результат матча: начисление Gold 30/15/20 и XP 150/75/100, запись в историю.
|
||
- [x] История матчей и реплеи: `GET /matches`, `GET /replays/{id}`, проекция под игрока.
|
||
- [x] Rate limiting на все мутирующие эндпоинты.
|
||
|
||
**Готово, когда:** новый игрок через API проходит путь регистрация → триал-дека → токены → бустеры → колода → очередь.
|
||
— выполнено: `MetaGameE2ETests` проходит регистрацию, онбординг, бустер, колоду,
|
||
очередь, полный матч, историю и реплей; проверено и на реальном PostgreSQL.
|
||
|
||
После MVP: косметика, квесты, баттлпасс, Draft, друзья/чат.
|
||
|
||
---
|
||
|
||
## 7. Клиент: каркас и меню `[15–20 дней]`
|
||
|
||
- [ ] Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
|
||
- [ ] Сцены: `Boot`, `Meta`, `Battle`.
|
||
- [ ] Composition root + DI (VContainer), без синглтон-свалок.
|
||
- [ ] `ApiClient` (HTTP/JSON), `MetaState`, обработка офлайна и таймаутов.
|
||
- [ ] `GameConnection` (TCP + MessagePack), очередь событий, `ClientGameState`.
|
||
- [ ] Экраны: логин/регистрация, главное меню, профиль (минимальный), магазин, коллекция, декбилдер, очередь, настройки.
|
||
- [ ] Декбилдер: фильтры (клан, грейд, редкость, тип, арт), счётчики, валидация в реальном времени.
|
||
- [ ] Коллекция: арты раздельно, «сколько в колодах», разбор в пыль с подтверждением.
|
||
- [ ] Магазин: бустер (анимация открытия), триал-деки, обмен токенов.
|
||
- [ ] Локализация RU/EN, переключение на лету.
|
||
- [ ] Настройки: язык, звук, FPS, мьют.
|
||
|
||
**Готово, когда:** все мета-экраны работают против реального сервера, язык переключается без перезапуска.
|
||
|
||
---
|
||
|
||
## 8. Клиент: боевая сцена `[20–30 дней]`
|
||
|
||
- [ ] Поле: Vanguard, 2 колонки × 2 ряда, Guardian, рука, Damage, Soul, Drop, Trigger.
|
||
- [ ] Старт: выбор grade 0, маллиган.
|
||
- [ ] Ввод: клик/тап → контекстное меню → подсветка валидных целей.
|
||
- [ ] Отображение силы/крита с модификаторами и подсветкой баффов.
|
||
- [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard.
|
||
- [ ] Скип/ускорение анимаций.
|
||
- [ ] Экран противника: таймлайн и текстовый лог боя.
|
||
- [ ] Таймер хода, индикатор реконнекта, кнопка сдачи.
|
||
- [ ] Экран результата: победа/поражение, Gold/XP, кнопка «ещё матч».
|
||
- [ ] Догон состояния после реконнекта (resync + переигрывание очереди).
|
||
|
||
**Готово, когда:** полный матч играется мышью без консоли и без рассинхрона с сервером.
|
||
|
||
---
|
||
|
||
## 9. Туториал `[5–7 дней]`
|
||
|
||
- [ ] `TutorialStep` как ScriptableObject: текст, подсветка, ожидаемое действие.
|
||
- [ ] Сценарий: Ride → Call → Boost → Attack → Drive Check → Damage Check → победа.
|
||
- [ ] Работает поверх реального движка (локальный матч), не фейковая сцена.
|
||
- [ ] Кнопка «Пропустить».
|
||
- [ ] По завершении: выбор триал-деки + 5 токенов + флаг онбординга.
|
||
|
||
**Готово, когда:** новый аккаунт с нуля доходит до первой своей колоды без подсказок извне.
|
||
|
||
---
|
||
|
||
## 10. Лаунчер `[5–7 дней]`
|
||
|
||
- [ ] WPF, MVVM, тёмная тема, окно 1000×640.
|
||
- [ ] Логин через тот же API, сохранение refresh-токена в DPAPI.
|
||
- [ ] `version.json`: `clientVersion`, `minVersion`, список файлов `{path, hash, size, url}`.
|
||
- [ ] Проверка хешей локальных файлов, докачка только изменённого.
|
||
- [ ] Прогресс, скорость, пауза, возобновление.
|
||
- [ ] Кнопка «Играть» запускает клиент с game-тикетом.
|
||
- [ ] Обработка «сервер недоступен», «нет места», «файл повреждён → перекачать».
|
||
- [ ] Самообновление лаунчера.
|
||
- [ ] Установщик (Inno Setup), установка без прав администратора.
|
||
|
||
**Готово, когда:** чистая машина → установка → логин → докачка → запуск игры.
|
||
|
||
---
|
||
|
||
## 11. Сборка и QA `[7–10 дней]`
|
||
|
||
- [ ] Скрипт сборки: сервер (docker), клиент (Unity batchmode), лаунчер (dotnet publish), манифест, загрузка на CDN.
|
||
- [ ] Тестовый стенд: БД с бесконечной валютой, аккаунты QA.
|
||
- [ ] Прогон «золотых матчей»: 50 реплеев воспроизводятся после каждого изменения ядра.
|
||
- [ ] E2E: регистрация → туториал → бустеры → колода → матч → награда.
|
||
- [ ] Проверка скрытой информации: дамп трафика не содержит руки и колоды противника, закрытого урона и seed.
|
||
- [ ] Проверка реконнекта на 20 разных шагах.
|
||
- [ ] Профилирование клиента: 60 FPS на средней машине.
|
||
- [ ] Инструкция запуска для нового разработчика (README).
|
||
|
||
---
|
||
|
||
## 12. Финальный коммит
|
||
|
||
- [ ] `.gitignore` актуален, в индексе нет `Library/`, `bin/`, `obj/`, `.env`, билдов.
|
||
- [ ] Нет секретов и ключей в истории (`git log -p | Select-String "password|secret|key"`).
|
||
- [ ] `git add -A` и один коммит:
|
||
```
|
||
git commit -m "Base version: server, client, launcher"
|
||
```
|
||
- [ ] Тег: `git tag v0.1.0-base`.
|
||
- [ ] Дальше — обычная история, по одному смыслу на коммит.
|
||
|
||
---
|
||
|
||
## Гейты (не двигаться дальше, пока не выполнено)
|
||
|
||
| Гейт | Критерий |
|
||
|---|---|
|
||
| **G1. Ядро** | 1000 случайных партий без падений, реплей бит-в-бит |
|
||
| **G2. Контент** | 94/94 карт заскриптованы и протестированы |
|
||
| **G3. Сеть** | Полный матч по сети, реконнект на любом шаге |
|
||
| **G4. Мета** | Регистрация → колода → ранкед матч → награда |
|
||
| **G5. Клиент** | Полный цикл мышью, без консоли |
|
||
| **G6. Доставка** | Чистая машина → установка → игра |
|