1094 lines
84 KiB
Markdown
1094 lines
84 KiB
Markdown
# Передача сессии
|
||
|
||
Состояние проекта на **23 октября 2026**. Документ нужен, чтобы новая сессия
|
||
продолжила работу без повторного разбора истории. Краткая оперативная
|
||
сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю.
|
||
|
||
---
|
||
|
||
## 2026-10-23 — защита и оплата способностей
|
||
|
||
- Панель «Фазы» переименована в «Действия»; защита перенесена туда.
|
||
- Защита выбирается кликом/драгом по картам в руке и перехватчикам на поле.
|
||
В панели одна кнопка: «Не защищаться» без выбора, «Защищаться» с выбранными
|
||
картами. Щит выбранных карт сразу входит в `POWER vs SHIELD`; после
|
||
подтверждения карты отправляются в Guardian, шаг защиты завершается.
|
||
- Дубли карт в Guardian (`player.Guardian` + `Battle.GuardCards`) устранены
|
||
дедупликацией по `InstanceId`.
|
||
- Оплата активируемых способностей: сброс выбирается в руке, Counter Blast —
|
||
кликом по урону. Доступные и выбранные карты цены подсвечиваются; оплата
|
||
подтверждается кнопкой «Подтвердить оплату» в панели «Действия».
|
||
Урон больше не подсвечивается вне режима оплаты. Правый клик отменяет выбор.
|
||
- Призыв в занятый RC заменяет юнита: прежний уходит в Дроп; правило
|
||
распространено на призыв из руки, души и колоды. Сервер задеплоен.
|
||
- Клик по карте в руке открывает меню действий с валидными пунктами
|
||
(Призвать / Сменить / Активировать способность / Подробнее); способность
|
||
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
||
убраны из панелей, смена/призыв — драгом.
|
||
- Защита из руки ограничена рангом авангарда.
|
||
- Draw-триггер после выбора цели силы сразу тянет карту; Heal после цели
|
||
силы просит карту только в своей зоне урона и только при выполнении условий.
|
||
- Tejas может атаковать задний ряд не только с RC, но и с VC (центр).
|
||
- Перемещение RC с единственным доступным кругом выполняется без выбора цели.
|
||
- Проведён аудит всех 66 карт с текстом: `docs/ability-implementation.md`.
|
||
Исправлены Conroe AUTO, зоны у Maiden of Libra/Starlight/Hungry/Clay/
|
||
Joka/Rakshasa, запрет поддержки Alfred.
|
||
- Тесты: сервер **149/149** правил + **45/45** серверных, клиент
|
||
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
||
|
||
---
|
||
|
||
## 1. Проект
|
||
|
||
Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**.
|
||
MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты, 124 принта.
|
||
|
||
Стек:
|
||
|
||
- сервер — **.NET 10**, **Orleans** (опционально), PostgreSQL, Redis;
|
||
- клиент — **Unity 6000.6.6f1**, URP, 3D-сцены (каркас готов);
|
||
- лаунчер — **WPF** (ещё не начат);
|
||
- контент — YAML + C#-скрипты карт;
|
||
- транспорт — **WebSocket + MessagePack** (реализован в сервере и клиенте).
|
||
|
||
Репозиторий:
|
||
|
||
- ветка `master`;
|
||
- remote `ssh://git@192.168.31.158:2222/mpakobecex/VanguardReMaster.git`;
|
||
- актуальный HEAD смотреть через `git log -1`;
|
||
- рабочее дерево чистое.
|
||
|
||
Главные принципы:
|
||
|
||
- сервер авторитарный, клиент тонкий;
|
||
- скрытая информация (колода, рука противника, закрытый урон, seed) не уходит клиенту;
|
||
- `Vanguard.Rules` — чистая библиотека без сети, БД и Orleans: команда на вход, события на выход;
|
||
- каждый блок работы коммитится и пушится в `master`.
|
||
|
||
---
|
||
|
||
## 2. Текущее состояние
|
||
|
||
| Проверка | Результат |
|
||
|---|---|
|
||
| Тесты сервера | **194/194** (149 правил/контрактов + 45 сервер) |
|
||
| `validate` | 0 ошибок, 0 предупреждений |
|
||
| `translations check` | 94/94 |
|
||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||
| Карты | **94/94** (66 с текстом + 28 ванильных) |
|
||
| Протокол и контракты | `docs/protocol.md`, `Vanguard.Shared` готовы |
|
||
| Сервер | сеть, матчмейкинг, PostgreSQL + Redis, Orleans, Docker |
|
||
| Мета-API | JWT, коллекция, колоды, магазин, экономика, история, реплеи |
|
||
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/DeckBuilder/Battle, логин/коллекция/колоды/очередь/бой, 124 арта, RU/EN, анимации карт |
|
||
| Лаунчер | не начат |
|
||
|
||
Блок «Игровое ядро» из `IMPLEMENTATION_CHECKLIST.md` закрыт по его критериям:
|
||
боты доигрывают 1000 партий, инварианты держатся, реплей воспроизводится
|
||
бит-в-бит по хэшу событий.
|
||
|
||
---
|
||
|
||
## 3. Как проверить
|
||
|
||
`dotnet` установлен в `%LOCALAPPDATA%\dotnet`, поэтому в PowerShell:
|
||
|
||
```powershell
|
||
$env:PATH = "$env:LOCALAPPDATA\dotnet;$env:PATH"
|
||
|
||
dotnet test server\Vanguard.slnx
|
||
dotnet run --project server\src\Vanguard.Cli -- validate
|
||
dotnet run --project server\src\Vanguard.Cli -- translations check
|
||
dotnet run --project server\src\Vanguard.Cli -- stress 1000
|
||
```
|
||
|
||
Ожидаемо:
|
||
|
||
```
|
||
Пройден! : не пройдено 0, пройдено 149 (Vanguard.Rules.Tests)
|
||
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
||
OK: ошибок 0, предупреждений 0
|
||
Переведено: 94/94
|
||
Партий: 1000, завершено: 1000, ошибок: 0
|
||
```
|
||
|
||
Клиент (Unity):
|
||
|
||
```powershell
|
||
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
||
-batchmode -nographics -projectPath client `
|
||
-runTests -testPlatform EditMode -testResults client\TestResults.xml
|
||
|
||
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
||
-batchmode -nographics -projectPath client `
|
||
-executeMethod Vanguard.Editor.SceneSmoke.Run
|
||
```
|
||
|
||
Сервер локально:
|
||
|
||
```powershell
|
||
dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080
|
||
# GET http://localhost:5080/health
|
||
```
|
||
|
||
Тестовый стенд на `ssh home` (Linux, Docker):
|
||
|
||
```bash
|
||
ssh home
|
||
cd ~/VanguardReMaster
|
||
# .env: VANGUARD_PORT=18080, VANGUARD_WS_URL=ws://192.168.31.158:18080/game
|
||
docker compose up -d --build
|
||
curl http://127.0.0.1:18080/health
|
||
# остановить: docker compose down
|
||
```
|
||
|
||
`Storage:Provider=None` по умолчанию: локальный запуск без базы. Стенд
|
||
использует `Postgres`, `Redis` и восстанавливает матчи после рестарта.
|
||
|
||
Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
||
|
||
---
|
||
|
||
## 4. Что сделано
|
||
|
||
### 4.1. Контент
|
||
|
||
- 94 карты, 124 принта в `content/sets/BT01.yaml`, `TD01.yaml`, `TD02.yaml`.
|
||
- Импортёр `tools/import_cards.py`.
|
||
- Валидатор контента: щиты, Sentinel, параллельные арты, лимиты колоды.
|
||
- Токенизация текстов, `content/keywords.yaml`.
|
||
- Русские переводы `content/translations/ru.yaml` — 94/94.
|
||
- Канон перевода `docs/translation-rules.md` v0.8.
|
||
|
||
### 4.2. Игровое ядро
|
||
|
||
`server/src/Vanguard.Rules/Engine/GameEngine.cs`.
|
||
|
||
Реализовано:
|
||
|
||
- фазы `Mulligan → Stand → Draw → Ride → Main → Battle → End`;
|
||
- стартовый Авангард, маллиган, добор;
|
||
- райд, призыв, перемещение в пределах колонки (занятый круг меняется местами);
|
||
- атака по Авангарду и переднему ряду Арьергарда;
|
||
- поддержка, перехват, идеальная защита (Sentinel);
|
||
- drive check и damage check с выбором целей;
|
||
- триггеры: Critical, Stand, Draw, Heal;
|
||
- победа/поражение, детерминированный RNG;
|
||
- стоимости: Counter Blast, Soul Blast, сброс, поворот себя, уничтожение себя;
|
||
- поиск в колоде без просмотра, поиск с призывом и с райдом;
|
||
- просмотр верхней карты;
|
||
- способности: `Triggered`, `Continuous`, `Activated`;
|
||
- модификаторы силы/крита с источником и сроком жизни (`StatModifier`);
|
||
- выдача и снятие способностей (`GrantedAbility`);
|
||
- запреты (`CardRestriction`): не вставать, не атаковать, не поддерживать,
|
||
не перехватывать, не быть выбранным;
|
||
- снятие Сдерживания эффектом;
|
||
- возврат карты с поля в колоду после боя и в конце хода;
|
||
- способности из руки, сброса и круга защиты;
|
||
- `AskUnitMultiChoice` для «до N» / «ровно N»;
|
||
- `LegalCommands()` — все легальные команды текущего момента;
|
||
- `GameInvariants.Check()` — проверка 50 карт, уникальных `instanceId`, урона ≤ 6.
|
||
|
||
Триггеры (`AbilityTrigger`):
|
||
|
||
`OnRideOver`, `OnPlace`, `OnBoost`, `OnBoosted`, `OnBoostedHit`, `OnDriveCheck`,
|
||
`OnGuard`, `OnRetireFromRC`, `OnOpponentRetire`, `OnLeaveGuardian`,
|
||
`OnPhaseStart`, `OnAttackDeclared`, `OnAttackHit`, `OnDamageTaken` (объявлен,
|
||
но пока не вызывается — карт с ним нет).
|
||
|
||
### 4.3. Карты
|
||
|
||
- **59** скриптов в `CardScripts.cs`, `CardScriptsBT01.cs`, `CardScriptsTD.cs`.
|
||
- **6** карт Sentinel/Heal обрабатываются данными и движком без скрипта.
|
||
- **Tejas** реализован флагом `snipeBackRow` в YAML.
|
||
- **28** ванильных карт скрипта не требуют.
|
||
- Итого **66/66** карт с текстом закрыты.
|
||
|
||
### 4.4. Реплей
|
||
|
||
`server/src/Vanguard.Rules/Replay/`:
|
||
|
||
- `CommandCodec` — команда в стабильную строку и обратно;
|
||
- `MatchRecord` — seed, колоды, команды, хэш событий;
|
||
- `MatchRecorder` — записывает только принятые команды;
|
||
- `ReplayRunner` — воспроизводит и проверяет хэш;
|
||
- `EventHasher` — FNV-1a по тексту событий.
|
||
|
||
Запись переживает JSON и воспроизводится бит-в-бит.
|
||
|
||
### 4.5. Диагностика
|
||
|
||
- `server/src/Vanguard.Rules/Diagnostics/GameInvariants.cs`;
|
||
- CLI-команда `stress [партий]` — случайный бот, лимит 5000 шагов на партию.
|
||
|
||
### 4.6. Тесты
|
||
|
||
`server/tests/Vanguard.Rules.Tests/GameEngineTests.cs` — **117** тестов:
|
||
|
||
- механики фаз, боя, защиты, перехвата, триггеров;
|
||
- все ключевые карты и стоимости;
|
||
- 50 детерминированных партий случайного бота с инвариантами;
|
||
- JSON-реплей полной партии с проверкой хэша.
|
||
|
||
`server/tests/Vanguard.Rules.Tests/ProtocolTests.cs` — **12** тестов:
|
||
|
||
- round-trip всех кадров клиента и сервера через MessagePack, включая
|
||
вложенные union'ы `MatchView` и `EventView`;
|
||
- отказ на пустом и слишком большом кадре;
|
||
- соответствие интентов и `GameCommand` в обе стороны;
|
||
- скрытие руки противника, колоды и закрытого урона;
|
||
- отсутствие seed/RNG в DTO;
|
||
- `LegalIntents` только у решающего игрока;
|
||
- скрытие поиска в колоде от противника и сохранение глобальных номеров
|
||
событий при фильтрации.
|
||
|
||
### 4.7. Сеть: протокол и контракты
|
||
|
||
- `docs/protocol.md` — версия 1: WebSocket + MessagePack, handshake по
|
||
одноразовому тикету, `intentId` и идемпотентность, `eventSeq` и resync,
|
||
heartbeat, реконнект 0/2/10/30 с, таймер 90 с + банк 5 мин, авто-поражение
|
||
через 5 минут отсутствия, таблица видимости зон.
|
||
- `Vanguard.Shared` — мультитаргет `netstandard2.1;net10.0`:
|
||
- `netstandard2.1` — DTO и MessagePack для Unity, без ссылки на правила;
|
||
- `net10.0` — дополнительно проекция состояния поверх `Vanguard.Rules`.
|
||
- DTO: кадры клиента и сервера, 18 интентов, 30 событий, `MatchView`,
|
||
`PlayerView`, `CardView`, `ClockView`.
|
||
- `ProtocolResolver` — `StandardResolver` + интернирование строк;
|
||
`MessagePackSecurity.UntrustedData`; лимит кадра 256 КиБ.
|
||
- `MatchProjection` — отдельный `MatchView` на каждого игрока;
|
||
`EventProjection` — фильтрация скрытых событий и диапазон по `eventSeq`;
|
||
`IntentMapping` — единственное место связи DTO с командами движка.
|
||
- Проекция вызывается для наблюдателя без побочных эффектов и не трогает
|
||
`LegalCommands()` чужого игрока (иначе утекала бы рука решающего).
|
||
|
||
### 4.8. Сервер
|
||
|
||
`server/src/Vanguard.Server` — ASP.NET Core 10, один процесс.
|
||
Матчи, очередь и тикеты сохраняются в PostgreSQL/Redis, если они настроены;
|
||
по умолчанию (`Storage:Provider=None`) сервер работает без внешних сервисов.
|
||
|
||
- **HTTP**: `POST /api/game/queue`, `GET /api/game/queue/{id}`,
|
||
`DELETE /api/game/queue/{id}`, `POST /api/game/tickets`, `GET /health`.
|
||
Контракт — `docs/server-api.md`.
|
||
- **Матчмейкинг**: две очереди (Classic Ranked / Casual), скрытый MMR,
|
||
окно ±100 с расширением на 50 каждые 5 с до 2000, выбор ближайшего MMR.
|
||
Ранкед меняет MMR по Эло (K = 32), casual — нет.
|
||
- **Тикеты**: одноразовые, TTL 60 с, привязаны к `(playerId, matchId, seat)`;
|
||
при настроенном Redis изымаются через `GETDEL` (одноразовость между
|
||
инстансами), иначе — из памяти.
|
||
- **`IMatchGrain`**: 1 матч = 1 экземпляр, все входы (интенты, подключения,
|
||
тики) проходят через один семафор; внутри нет ни ASP.NET Core, ни WebSocket —
|
||
только `IMatchConnection`. Orleans-хост (`MatchActorGrain`) подключается
|
||
поверх того же контракта; соединения живут в `IMatchConnectionRegistry`,
|
||
чтобы состояние грана оставалось сериализуемым.
|
||
- **WebSocket** `/game`: handshake, MessagePack, `intentId`, idempotency,
|
||
`eventSeq`, `resync`, heartbeat (20 с простоя → ping, ещё 20 с без ответа
|
||
→ `4408`), реконнект, захват сессии (`4409`), rate limit 16 интентов/с.
|
||
- **Таймеры**: 90 с на решение + банк 300 с, отсутствие 300 с → поражение;
|
||
`ServerClock` каждые 5 с.
|
||
- **Результат**: сдача, явный выход, таймер и отсутствие завершают партию
|
||
через `GameEngine.Finish(winner)`; событие `GameFinished` уходит обоим,
|
||
результат пишется в профили.
|
||
- **Проекция**: своя рука, свой выбор, свои легальные интенты; у противника
|
||
только количества и открытые зоны. `seed` и RNG не покидают сервер.
|
||
- **PostgreSQL**: таблицы `matches` и `match_intents`. В заголовке — seed,
|
||
колоды, игроки, часы, банки, статус; в журнале — `intentId`, команда,
|
||
результат. При старте незавершённые матчи восстанавливаются повтором
|
||
принятых команд на детерминированном движке, включая ожидания выбора:
|
||
делегаты движка не сериализуются. Журнал реплея сверяется по `eventSeq`.
|
||
Банк и дедлайн пишутся каждые ~5 с; потеря при аварии — не больше тика.
|
||
Схема — EF-миграции (`Migrations/InitialCreate`), на старте `Migrate`.
|
||
- **Redis**: тикеты и записи очереди (JSON + sorted set по режимам, TTL);
|
||
очередь поднимается при старте. Ключи с префиксом `vanguard:`.
|
||
- **Конфигурация**: `Storage:Provider` = `None` / `Memory` / `Postgres`,
|
||
`Storage:ConnectionString`; `Redis:Configuration`. `Memory` — EF InMemory
|
||
для тестов, `Postgres` — постоянное хранилище.
|
||
- **Orleans**: `Orleans:Enabled` (`false` по умолчанию, `true` в compose).
|
||
Включённый режим поднимает silo с localhost-clustering в том же процессе
|
||
(`Orleans:SiloPort` / `GatewayPort`, по умолчанию 11111/30000);
|
||
`MatchActorGrain` держит доменный `MatchGrain`, `IMatchConnectionRegistry`
|
||
остаётся в процессе. Активный матч держится тикером, завершённые матчи
|
||
выгружаются после `CompletedMatchTtlSeconds`; страховочный CollectionAge
|
||
12 часов. Один silo на инстанс; для нескольких реплик нужен настоящий
|
||
кластер и внешний реестр соединений. Тест кластера — `OrleansMatchTests`.
|
||
- **Docker**: `Dockerfile` + `docker-compose.yml` поднимают postgres, redis
|
||
(AOF) и сервер; `/health` на `http://localhost:8080` (порт меняется
|
||
через `VANGUARD_PORT`, адрес WebSocket — `VANGUARD_WS_URL`).
|
||
- **CI**: `.gitea/workflows/ci.yml` — restore, тесты, `validate` и
|
||
`translations check` на push/PR. Actions включены для репозитория,
|
||
`home-runner` выполняет job (проверено: все шаги зелёные). `setup-dotnet`
|
||
указан явным URL с GitHub: в зеркале `code.forgejo.org` его нет.
|
||
- **Тесты серверного ядра** (36 из 45 в `Vanguard.Server.Tests`):
|
||
- тикеты: одноразовость и TTL;
|
||
- heartbeat: ping после 20 с простоя, закрытие через 20 с, сброс любым кадром;
|
||
- grain: attach, чужое место, duplicate intent, чужой ход, сдача,
|
||
явный выход, команды после финала, банк времени, отсутствие, старт часов,
|
||
reconnect с `eventSeq`, захват сессии;
|
||
- матчмейкинг: подбор, разные очереди, окно, плохая колода,
|
||
отсутствие стартового Авангарда, отмена, замена очереди, истечение, MMR,
|
||
перезагрузка очереди из хранилища;
|
||
- персистентность: EF-хранилище, восстановление матча повтором команд,
|
||
`MatchRecoveryService` после «рестарта»;
|
||
- внешние хранилища (`ExternalStorageTests`, запускаются при
|
||
`VANGUARD_TEST_POSTGRES` / `VANGUARD_TEST_REDIS`): реальные PostgreSQL
|
||
и Redis, изъятие тикета, очередь;
|
||
- E2E: два клиента играют полный матч по WebSocket и сходятся в победителе;
|
||
обрыв и возврат по новому тикету с догоном состояния;
|
||
- Orleans: грана инициализируется, принимает attach и интент, граница
|
||
Orleans сериализуется (тестовый `TestCluster`); полный матч двух
|
||
клиентов через HTTP/WebSocket при `Orleans:Enabled=true`.
|
||
|
||
Проверено на `ssh home`: 132 + 45 теста, включая реальные PostgreSQL и Redis;
|
||
`docker compose up` поднимает стенд, матч создаётся через HTTP-очередь,
|
||
а после `docker compose restart server` восстанавливается (`Recovery: 1 of 1`).
|
||
|
||
### 4.9. Мета-API
|
||
|
||
- **Аккаунты**: `register`, `login`, `refresh`, `logout`, `me`.
|
||
BCrypt, JWT access 15 минут, refresh 30 дней с ротацией; в БД хранится
|
||
хэш refresh-токена, старый после ротации отзывается.
|
||
- **Онбординг**: одна из двух триал-дек, 50 карт в коллекцию и 5 токенов;
|
||
повтор невозможен. Триал-деки — `content/trial-decks.yaml`.
|
||
- **Коллекция**: по `printingId`, без верхнего предела; разбор
|
||
C 5 / R 10 / RR 25 / RRR 100 пыли, крафт 20 / 40 / 100 / 400, SP нельзя.
|
||
- **Колоды**: CRUD, валидация 50 карт / ≤4 по `nameId` / Sentinel / Heal /
|
||
стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду.
|
||
- **Правило атаки**: атакуют только Вэнгард и Арьергард переднего ряда
|
||
(слоты 3/4); задний ряд (0–2) — только поддержка/boost. Задний ряд
|
||
также нельзя атаковать без `snipeBackRow`. `LegalCommands` не предлагает
|
||
атаки из заднего ряда; добавлен регрессионный тест.
|
||
- **Магазин**: бустер 100 Gold или 1 токен; вторая триал-дека 500 Gold;
|
||
состав бустера 4 C + 1 R+ (R 70 / RR 22 / RRR 7 / SP 1) из
|
||
`content/pack_odds.yaml`; duplicate protection по `printingId` с порогом 4.
|
||
- **Экономика**: журнал транзакций, баланс вычисляется, `idempotencyKey`
|
||
на всех покупках, разборе и крафте.
|
||
- **Очередь с JWT**: `{ mode, deckId }`, игрок и колода берутся из токена
|
||
и коллекции. Dev-режим `playerId + deck` остался только при
|
||
`Game:AllowDevQueue=true`.
|
||
- **Rate limiting**: 60/мин на auth, 300/мин на прочие мутации, плюс
|
||
глобальный лимит по IP.
|
||
|
||
### 4.10. История матчей и реплеи
|
||
|
||
- `MetaProfileService` после подтверждённого завершения матча пишет
|
||
историю обоим игрокам, меняет MMR в ранкеде и начисляет Gold/XP.
|
||
Победа 30 Gold / 150 XP, поражение 15 / 75; сдача, явный выход, таймер
|
||
и отсутствие дают 0, но остаются в истории.
|
||
- `GET /api/matches`, `GET /api/matches/{id}` — история игрока.
|
||
- `GET /api/replays/{id}` восстанавливает матч из seed, колод и журнала
|
||
принятых команд и отдаёт:
|
||
- `events` — события, видимые этому игроку;
|
||
- `frames` — `MatchView` до и после каждой команды.
|
||
- Реплей собирается под зрителя: рука противника и закрытый урон скрыты,
|
||
`legalIntents` пуст, часы обнулены. Смотреть может только участник.
|
||
- Проверено на `ssh home`: живой Postgres, регистрация → онбординг →
|
||
бустер за токен → колода → очередь → матч; `/api/replays/{id}` отдаёт
|
||
11 стартовых событий и кадр, рука противника пуста. После рестарта
|
||
сервера матч восстановлен (`matches: 1`).
|
||
|
||
Счётчик `Vanguard.Server.Tests` вырос до 45: добавлены auth, мета-профиль,
|
||
мета-E2E (онбординг → бустер → колода → матч → история → реплей),
|
||
проверки реплея, явного выхода и истории/реплея после выхода,
|
||
матч против бота.
|
||
|
||
---
|
||
|
||
## 5. Структура и ключевые файлы
|
||
|
||
```
|
||
/server/src/Vanguard.Rules
|
||
/Engine/GameEngine.cs основной движок (~2500 строк)
|
||
/Engine/GameState.cs состояние, зоны, InTransit
|
||
/Engine/GameModels.cs CardInstance, Step, Phase, PendingChoice
|
||
/Engine/GameCommands.cs команды
|
||
/Engine/GameEvents.cs события
|
||
/Engine/Modifiers.cs StatModifier, GrantedAbility, CardRestriction
|
||
/Engine/Abilities/Abilities.cs AbilityDefinition, ActivatedAbility, триггеры
|
||
/Engine/Abilities/CardScripts.cs реестр Default + базовые карты
|
||
/Engine/Abilities/CardScriptsBT01.cs
|
||
/Engine/Abilities/CardScriptsTD.cs
|
||
/Cards/CardDefinition.cs статы, Sentinel, Restraint, snipeBackRow
|
||
/Decks/DeckValidator.cs правила колоды
|
||
/Replay/ запись и воспроизведение
|
||
/Diagnostics/GameInvariants.cs
|
||
/server/src/Vanguard.Content загрузка YAML и валидация
|
||
/server/src/Vanguard.Cli validate, stats, translations, stress, deck
|
||
/server/src/Vanguard.Shared
|
||
/Protocol/*.cs кадры, интенты, события, view, кодек
|
||
/Projection/*.cs проекция состояния, фильтр событий, маппинг
|
||
/server/src/Vanguard.Server
|
||
/Program.cs ASP.NET Core, DI, WebSocket endpoint
|
||
/Api/ очередь, тикеты, /health
|
||
/Auth/ JWT, register/login/refresh, BCrypt
|
||
/Meta/ коллекция, колоды, магазин, экономика, история, реплеи
|
||
/Matchmaking/ очереди, MMR, Redis-хранилище очереди
|
||
/Matches/ IMatchGrain, таймеры, тикер, регистр
|
||
/Realtime/ WebSocket-обвязка, чтение кадров
|
||
/Tickets/ одноразовые тикеты: память или Redis
|
||
/Storage/ EF-хранилище, восстановление матчей
|
||
/Profiles/ интерфейс MMR
|
||
/Dockerfile
|
||
/server/tests/Vanguard.Rules.Tests 132 теста
|
||
/server/tests/Vanguard.Server.Tests 45 тестов, включая E2E и внешние хранилища
|
||
/content/sets карты
|
||
/content/trial-decks.yaml две триал-деки
|
||
/content/pack_odds.yaml веса бустера
|
||
/content/translations/ru.yaml переводы
|
||
/docs/translation-rules.md канон перевода
|
||
/docs/protocol.md протокол матча, версия 1
|
||
/docs/server-api.md HTTP-очередь, тикеты, /health
|
||
/docs/HANDOFF.md этот файл
|
||
/docker-compose.yml postgres, redis, server
|
||
/.env.example VANGUARD_PORT и VANGUARD_WS_URL
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Принятые решения
|
||
|
||
Их не нужно пересматривать без причины.
|
||
|
||
- **Общение:** ответы короткие, без повторов и длинных вступлений.
|
||
- **Сомнения:** любое сомнение/вопрос — диалог с 2–4 вариантами; выбор
|
||
делает пользователь. Не угадывать.
|
||
- Формат называется **Classic**, не Standard.
|
||
- Имена карт английские; `nameRu` зарезервировано.
|
||
- Термины: Авангард, Арьергард, ранг, смена, поддержка, проверка,
|
||
фазы Подъёма/Добора/Смены/Основная/Боевая/Завершающая, завершающий шаг,
|
||
Сдерживание, «вы», буква ё.
|
||
- Способности ищутся по `nameId`, а не по принту.
|
||
- Лимит 4 копий — по `nameId`, только правило декбилдера.
|
||
- Дубль в коллекции — по `printingId`; порог 4, замена внутри редкости.
|
||
- Бустер: 5 карт = 4 C + 1 R+; веса R 70 / RR 22 / RRR 7 / SP 1.
|
||
- Gold за матч: 30/15/20 (победа/поражение/ничья); Gold = XP / 5.
|
||
- Таймер хода 90 с + банк 5 мин.
|
||
- Транспорт матча — **WebSocket** + MessagePack; TCP с префиксом длины —
|
||
резерв для Unity, если WebSocket не подойдёт.
|
||
- Тикет на подключение — одноразовый, TTL 60 с, передаётся в `ClientHello`,
|
||
не в URL; reconnect получает новый тикет.
|
||
- `playerId` в HTTP-API — dev-заглушка до JWT. Выставлять сервер в интернет
|
||
в таком виде нельзя.
|
||
- Состояние сервера: PostgreSQL — снапшот/журнал матчей, Redis — тикеты
|
||
и очередь; без конфигурации работает in-memory. `IMatchGrain` отделён
|
||
от транспорта, Orleans подключается поверх и включается конфигом.
|
||
- Rate limit — 16 интентов в секунду на соединение.
|
||
- Скрытый MMR: старт 1000, Эло K = 32, casual рейтинг не меняет.
|
||
- Аккаунты: BCrypt, access JWT 15 минут, refresh 30 дней с ротацией;
|
||
в БД хранится только хэш refresh-токена.
|
||
- Онбординг: одна триал-дека бесплатно + 5 токенов; вторая триал-дека
|
||
за 500 Gold; бустер 100 Gold или 1 токен.
|
||
- Коллекция по `printingId` без лимита; лимит 4 по `nameId` — только декбилдер.
|
||
- Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт 20 / 40 / 100 / 400,
|
||
SP не разбирается и не крафтится.
|
||
- Матч: победа 30 Gold / 150 XP, поражение 15 / 75; Gold = XP / 5.
|
||
Сдача, явный выход, таймер и отсутствие наград не дают.
|
||
- История матча пишется обоим игрокам один раз; реплей доступен только
|
||
участникам и собирается под зрителя, без руки противника.
|
||
- Порядок триггеров выбирает игрок; опциональные способности спрашиваются.
|
||
- Непрерывные эффекты пересчитываются при каждом запросе.
|
||
- Поиск в колоде: 0 — уведомление и перемешивание, 1 — сразу, 2+ — выбор.
|
||
- Эффекты не вкладываются: цепочка строго друг за другом.
|
||
- Каждый блок — коммит и push в `master`.
|
||
|
||
---
|
||
|
||
## 7. Что осталось
|
||
|
||
### 7.1. Ближайший блок — сервер
|
||
|
||
Сделано в этом блоке:
|
||
|
||
1. **[x] `docs/protocol.md`** — версия 1:
|
||
кадры, handshake по тикету, `intent` с `intentId` и `eventSeq`,
|
||
resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение,
|
||
таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости.
|
||
2. **[x] `Vanguard.Shared`**:
|
||
DTO интентов и событий, MessagePack-резолвер, проекция состояния
|
||
на каждого игрока, фильтрация скрытых событий, маппинг интентов.
|
||
3. **[x] `Vanguard.Server`** — сетевой матч работает:
|
||
ASP.NET Core + WebSocket, тикеты, `IMatchGrain` (in-process),
|
||
матчмейкинг, MMR, идемпотентность, таймеры, реконнект, `/health`,
|
||
Dockerfile и compose. Контракт HTTP — `docs/server-api.md`.
|
||
4. **[x] Проекция состояния** подключена к грану и проверена E2E-тестом:
|
||
своя рука и свой выбор полностью, у противника только количества
|
||
и открытые зоны, seed и RNG не уходят.
|
||
5. **[x] `Docker Compose`** — postgres, redis (AOF) и сервер; `/health`,
|
||
порт через `VANGUARD_PORT`. Сервер запускается с `Orleans:Enabled=true`.
|
||
6. **[x] Персистентность**:
|
||
- PostgreSQL — заголовок матча + журнал интентов, восстановление повтором
|
||
команд после рестарта, `MatchRecoveryService`;
|
||
- Redis — одноразовые тикеты (`GETDEL`) и очередь с перезагрузкой.
|
||
|
||
Осталось в серверном направлении:
|
||
|
||
7. **Мета-API** — сделано: JWT, аккаунты, коллекция, колоды, магазин,
|
||
экономика, онбординг, матчмейкинг по `deckId`, история и реплеи.
|
||
`playerId` остался только как dev-режим при `Game:AllowDevQueue=true`.
|
||
8. **[x] Orleans** — `MatchActorGrain` + `OrleansMatchAdapter`; silo
|
||
включается `Orleans:Enabled=true`, в compose включён. `MatchGrain` остался
|
||
доменным ядром, соединения вынесены в `IMatchConnectionRegistry`,
|
||
`MatchActorGrain` восстанавливается из снапшота на активации. Покрыто
|
||
`OrleansMatchTests` (тестовый кластер) и `OrleansServerE2ETests` (полный
|
||
матч через HTTP/WebSocket при включённом silo).
|
||
|
||
Критерий готовности серверного блока выполнен: два клиента играют полный
|
||
матч по сети; kill клиента восстанавливается через новый тикет и
|
||
`resumeFromSeq`; рестарт сервера восстанавливает матчи из PostgreSQL.
|
||
CI подключён и проходит; серверный блок закрыт.
|
||
|
||
### 7.2. Мета-API
|
||
|
||
Сделано:
|
||
|
||
- `User`, BCrypt, JWT (access 15 мин + refresh 30 дней, ротация).
|
||
- `register`, `login`, `refresh`, `logout`, `me`.
|
||
- Коллекция по принтам, без верхнего предела; разбор и крафт.
|
||
- Колоды: CRUD, `validate`, лимит 30, шаринг по коду, проверка владения.
|
||
- Магазин: бустеры, триал-деки, токены; duplicate protection.
|
||
- Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт 20 / 40 / 100 / 400;
|
||
SP не разбирается и не крафтится.
|
||
- Онбординг: триал-дека + 5 токенов.
|
||
- Матчмейкинг по JWT и `deckId`; dev-режим только для разработки.
|
||
- Результат матча: Gold/XP, MMR, история, реплеи.
|
||
- Rate limiting.
|
||
|
||
Осталось после MVP: косметика, квесты, баттлпасс, Draft, друзья/чат,
|
||
история покупок в клиенте.
|
||
|
||
### 7.3. Клиент
|
||
|
||
Первый блок начат: **Unity 6000.6.6f1**, URP-проект в `client/`.
|
||
|
||
Сделано:
|
||
|
||
- проект создан из URP-шаблона Unity 6.6; пакеты: URP, Input System,
|
||
Addressables, Localization, Newtonsoft.Json, VContainer (OpenUPM);
|
||
- визуальное направление зафиксировано: **3D**, отдельные сцены-экраны
|
||
Login/Lobby/DeckBuilder/Battle (`docs/client-visual-direction.md`);
|
||
`DeckBuilder` совмещает коллекцию и колоды; UI пока code-generated gray-box;
|
||
- сцена `Boot` + runtime-инициализатор, поэтому клиент стартует и в пустой
|
||
сцене;
|
||
- composition root на VContainer: `AppConfig`, `TokenStore`, `ApiClient`,
|
||
`AuthService`, `MetaService`, `PlayerState`, `SceneFlow`;
|
||
- HTTP-клиент на `UnityWebRequest` + Newtonsoft.Json, разбор `ApiError`,
|
||
таймаут 20 с;
|
||
- login/register/refresh/me/logout, восстановление сессии по refresh,
|
||
сохранение токенов в `PlayerPrefs` (dev);
|
||
- экран входа с выбором адреса сервера (стенд по умолчанию
|
||
`http://192.168.31.158:18080`) и главный экран с балансами, MMR и
|
||
переходами в разделы;
|
||
- онбординг: список триал-дек из `/api/store`, выбор одной, 50 карт + 5
|
||
токенов, обновление профиля;
|
||
- коллекция: `/api/collection`, балансы, разбор в пыль и крафт; ПКМ по
|
||
карте открывает оверлей с текстом способностей;
|
||
- колоды: `/api/decks`, список справа, редактор; карточная сетка
|
||
коллекции с сортировкой клан→ранг→имя;
|
||
- магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт,
|
||
покупка второй триал-деки;
|
||
- история матчей и реплей: `/api/matches`, `/api/replays/{id}`, просмотр
|
||
событий и кадров состояния;
|
||
- очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`;
|
||
- `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`,
|
||
snapshot/events, интенты с ответом сервера, ping/pong, закрытие;
|
||
- серый боевой экран: доска, рука, часы, все легальные интенты кнопками,
|
||
сдача с подтверждением, результат;
|
||
- `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены
|
||
как плагины; `client/README.md` описывает запуск и ограничения;
|
||
- сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` и
|
||
`SceneFlow`; `Boot` — стартовая служебная сцена, `VanguardApp` и сервисы
|
||
живут между сценами;
|
||
- 3D-фон на каждой сцене: камера, свет, пол и тематические примитивы;
|
||
UI пока code-generated uGUI;
|
||
- EditMode smoke `Vanguard.Tests.Editor` и `Vanguard.Editor.SceneSmoke`
|
||
проверяют запуск Boot → Login и создание экранов/3D-окружения;
|
||
live-probe (не входит в набор) проверил онбординг, коллекцию,
|
||
создание/удаление колоды, а также два WebSocket-подключения, маллиган,
|
||
сдачу и `GameFinished` против стенда.
|
||
|
||
Дальше по клиенту:
|
||
|
||
1. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты.
|
||
2. Полный декбилдер: фильтры, счётчики, валидация.
|
||
3. 3D-наполнение сцен: модели/арт, prefab-экраны, камера.
|
||
4. Локализация RU/EN, настройки, DOTween.
|
||
|
||
**Зафиксированное визуальное направление** — `docs/client-visual-direction.md`:
|
||
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / DeckBuilder /
|
||
Battle; Boot и общие сервисы остаются персистентными.
|
||
Сцены и `SceneFlow` уже созданы; UI пока code-generated gray-box.
|
||
|
||
- Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
|
||
- VContainer, `ApiClient`, `GameConnection`, `SceneFlow`.
|
||
- Сцены: `Boot` (служебная), `Login`, `Lobby`, `DeckBuilder`,
|
||
`Battle`; очередь/онбординг — оверлеи Lobby.
|
||
- История матчей и экран реплея: `GET /api/matches`, `GET /api/replays/{id}`,
|
||
проигрывание `frames` + `events`.
|
||
- Локализация RU/EN на лету.
|
||
|
||
### 7.4. Лаунчер
|
||
|
||
- WPF: обновления, запуск клиента, выбор сервера.
|
||
|
||
### 7.5. Мелкие технические долги
|
||
|
||
- `OnDamageTaken` не вызывается — добавить, когда появится карта с таким триггером.
|
||
- Глобальный лимит шагов внутри движка не enforced — только в стресс-тесте.
|
||
- `GameEngine.cs` большой; при росте разнести на partial-файлы.
|
||
- `MatchRecord` сейчас JSON; для сети позже MessagePack.
|
||
- В `LegalCommands()` не входят активируемые способности со стоимостью «сброс карты» —
|
||
для UI их нужно будет перечислить с вариантами сброса.
|
||
|
||
---
|
||
|
||
## 8. Порядок действий в новой сессии
|
||
|
||
1. Прочитать `docs/SESSION-STATE.md`, `docs/HANDOFF.md`,
|
||
`IMPLEMENTATION_CHECKLIST.md`, `README.md`, `docs/game-rules.md`.
|
||
2. Прогнать проверки из раздела 3.
|
||
3. Следующий блок — клиент: магазин/бустеры в Lobby и история/реплей,
|
||
затем полный декбилдер и 3D-наполнение сцен. Сервер и мета-API готовы:
|
||
`docs/protocol.md`, `docs/server-api.md`, `Vanguard.Shared`.
|
||
4. Перед началом сверить `docs/client-visual-direction.md`: 3D-сцены,
|
||
`Boot` и общие сервисы персистентны.
|
||
5. После каждого блока: тесты, `validate`, `translations check`, коммит, push.
|
||
6. Не ломать `Vanguard.Rules`: сервер должен зависеть от него, а не наоборот.
|
||
|
||
---
|
||
|
||
## 9. Подводные камни
|
||
|
||
- **Клиент: HTTP.** В Unity по умолчанию запрещены незащищённые соединения:
|
||
для dev-стенда в `client/ProjectSettings/ProjectSettings.asset` стоит
|
||
`insecureHttpOption: 2` (Always). Для релиза — HTTPS и вернуть запрет.
|
||
- **Клиент: WebSocket.** `GameConnection` использует
|
||
`System.Net.WebSockets.ClientWebSocket`; это окно для Windows-десктопа.
|
||
Для WebGL/мобильного IL2CPP нужен отдельный транспорт/плагин.
|
||
- **Кодировка.** Не использовать PowerShell `Get-Content`/`Set-Content` для
|
||
UTF-8 файлов с русским текстом: однажды это уже испортило комментарии.
|
||
Правки — только через `edit`/`write`, либо Python с `encoding="utf-8"`.
|
||
- **PATH.** `dotnet` лежит в `%LOCALAPPDATA%\dotnet`; без добавления в PATH
|
||
команды не находятся.
|
||
- **Solution.** `server/Vanguard.slnx`, не `.sln`.
|
||
- **Тесты.** `StartedEngine()` принудительно ставит Conroe стартовым Авангардом,
|
||
чтобы Barcgal не срабатывал.
|
||
- **Shared.** Мультитаргет: файлы `Projection/` компилируются только для
|
||
`net10.0`; в `netstandard2.1` (Unity) ссылки на `Vanguard.Rules` нет.
|
||
- **Видимость.** `LegalCommands()` описывает решающего игрока. Вызывать его
|
||
для наблюдателя нельзя — в списке есть printingId из чужой руки.
|
||
- **Сервер: rate limit.** 16 интентов в секунду на соединение. Быстрый бот
|
||
в E2E специально притормаживается; живой UI в лимит не упирается.
|
||
- **Сервер: хранилище.** По умолчанию `Storage:Provider=None`: матчи живут
|
||
в памяти (`NullMatchStore`), мета-API — в EF InMemory. Для рестарта нужен
|
||
`Postgres` + `Storage:ConnectionString`; `Memory` — EF InMemory для всего.
|
||
Схема — EF-миграции (`Migrations/InitialCreate`), на старте `MigrateAsync`;
|
||
для InMemory — `EnsureCreated`. Миграции применяются автоматически.
|
||
- **Сервер: снапшот матча.** Сериализуется не состояние движка, а seed, колоды,
|
||
игроки, часы и журнал интентов. При восстановлении команды проигрываются
|
||
заново; `EventSeq` обязан совпасть, иначе матч пропускается с ошибкой.
|
||
Делегаты `PendingChoice`/`Continuation` восстанавливаются самим реплеем.
|
||
- **Сервер: восстановление.** `MatchRecoveryService` поднимает только
|
||
незавершённые матчи за последние 7 дней. Завершённые остаются историей
|
||
в PostgreSQL и не восстанавливаются в память. Банк/дедлайн пишутся
|
||
раз в ~5 с, поэтому авария теряет не больше тика.
|
||
- **Сервер: Redis.** Тикеты изымаются через `GETDEL` (Redis 6.2+; в compose 7).
|
||
Очередь — JSON + sorted set по режимам, TTL в два раза больше очереди.
|
||
Префикс ключей — `Redis:Prefix` (`vanguard:`).
|
||
- **Сервер: Docker.** На домашнем сервере порт 8080 занят, поэтому стенд
|
||
поднимается с `VANGUARD_PORT=18080` и `VANGUARD_WS_URL` в `.env`.
|
||
`docker compose restart server` проверяет восстановление матчей.
|
||
- **Сервер: внешние тесты.** `VANGUARD_TEST_POSTGRES` и `VANGUARD_TEST_REDIS`
|
||
включают `ExternalStorageTests`; без них тесты проходят как no-op.
|
||
Postgres-тест делает `EnsureDeleted`, поэтому БД должна быть тестовой.
|
||
- **Сервер: тесты.** E2E поднимает `WebApplicationFactory<Program>`,
|
||
играет полный матч по WebSocket и проверяет, что победитель совпал.
|
||
Случайные партии завершаются за ~350 действий; таймаут теста — 120 с.
|
||
- **Мета: JWT.** `Jwt:SigningKey` — не короче 32 символов; дефолтный ключ
|
||
только для разработки. Access 15 минут, refresh 30 дней с ротацией;
|
||
в БД лежит SHA-256 от refresh, старый токен после ротации недействителен.
|
||
- **Мета: dev-очередь.** `Game:AllowDevQueue=true` разрешает очередь по
|
||
`playerId` без JWT. В compose выключено; не включать в production.
|
||
- **Мета: колода.** Сохранение проверяет не только правила Classic, но и
|
||
владение каждой картой. Триал-дека даёт карты в коллекцию, а не готовую
|
||
колоду — её собирают декбилдером.
|
||
- **Мета: бустер.** 4 C + 1 R+, веса в `content/pack_odds.yaml`.
|
||
Duplicate protection считает `printingId` и порог 4; при насыщении
|
||
редкости выбирается другая, иначе допускается дубль. SP не крафтится
|
||
и не разбирается.
|
||
- **Мета: награды.** Gold/XP только за `normal`; сдача, явный выход, таймер
|
||
и отсутствие дают 0. История пишется один раз на `(userId, matchId)`,
|
||
повтор результата не дублирует валюту.
|
||
- **Реплей.** Доступен только участнику; собирается из seed, колод и журнала
|
||
принятых команд. `frames` — состояние после каждой команды, `events` —
|
||
только видимые зрителю события; рука противника и закрытый урон скрыты.
|
||
Сырые колоды и seed наружу не отдаются.
|
||
- **Сервер: таймеры.** Тик раз в секунду. После 90 с банк списывается
|
||
по фактическому превышению; `TurnDeadlineMs` в этот момент сдвигается,
|
||
поэтому UI должен показывать ещё и `BankMs`.
|
||
- **Сервер: часы.** Отсчёт стартует только когда подключились оба места.
|
||
До этого `ClockView.IsRunning = false`, но таймер отсутствия уже идёт.
|
||
- **Сервер: проекция.** `ServerSnapshot` и `ServerEvents` собираются
|
||
отдельно на каждое место; не переиспользовать один `MatchView` на двоих.
|
||
- **Порядок фаз.** Вход в Основную — `EnterMainPhase()` с `OnPhaseStart`.
|
||
- **Отложенные продолжения.** `State.Continuation` — что выполнить после паузы;
|
||
`State.PendingQueue` + `_resolvingDepth` запрещают вложенность эффектов.
|
||
- **`OfferAbility`.** Возвращает `Skipped / Paused / Resolved`; вызывающий код
|
||
продолжает очередь только при `Skipped`. Ошибка здесь уже приводила к потере
|
||
шага защиты.
|
||
- **`InTransit`.** Найденная карта, ждущая выбора круга, лежит в
|
||
`State.InTransit`, иначе инвариант «50 карт» падает.
|
||
- **Задний ряд.** Обычные карты атакуют только передний ряд; Tejas — задний
|
||
в своей колонке через `snipeBackRow`.
|
||
- **Реестр карт.** Новая карта без записи в `CardScripts.Default` молча
|
||
останется без способностей; проверять покрытие скриптом.
|
||
- **Тестовые колоды.** `BuildDeck()` в тестах не содержит все карты; для
|
||
конкретной карты использовать `MakeCard()` и класть вручную.
|
||
|
||
## 2026-10-21 — декбилдер и каталог карт
|
||
|
||
- Добавлен `GET /api/cards` (Bearer): все принты с `grade`, `trigger`,
|
||
`clan`, `rarity`, `isSentinel`, `isParallel`, `basePrintingId`.
|
||
Клиент объединяет каталог с коллекцией по `printingId`; правила для
|
||
фильтров/статистики не дублируются в клиенте.
|
||
- Новый `DeckBuilderView` в духе HS:
|
||
- список: сверху фильтры/сброс/создание, в центре сетка коллекции
|
||
(изображение-заглушка, количество), справа узкий вертикальный список
|
||
колод; клик по колоде открывает редактор;
|
||
- редактор: вместо создания — статистика по грейдам и общему числу,
|
||
справа вертикальный список карт колоды; клик по карте коллекции
|
||
добавляет, клик по карте колоды убирает одну копию;
|
||
- сортировка коллекции: клан → ранг → имя.
|
||
- Ограничения в UI: 50 карт, ≤4 по `nameId`, не больше owned-копий;
|
||
серверная валидация остаётся источником истины.
|
||
- Сцена **Collection** и отдельный `CollectionView` удалены; коллекция
|
||
теперь на экране **Коллекция** внутри `DeckBuilderView`.
|
||
- ПКМ по карте коллекции/колоды открывает оверлей: изображение-заглушка,
|
||
статы, текст способности, разбор в пыль и крафт (C/R/RR/RRR; SP и TD
|
||
нельзя).
|
||
- `MetaService.CraftAsync`; `GET /api/cards` отдаёт `power`, `shield`,
|
||
`critical`, `type`, `textEn/textRu` и флаги способностей.
|
||
- Проверки: серверные 132 + 45 на `ssh home`; Unity EditMode 2/2,
|
||
`SCENE_SMOKE success=True active=Login`; live `GET /api/cards` — 124
|
||
принта; live UI-проба: загрузка 50 карт, создание колоды,
|
||
добавление/удаление, ПКМ-оверлей, текст способности и разбор карты;
|
||
live API-проба крафта: разбор 4 C → 20 пыли → крафт C.
|
||
- `POST /api/game/practice` и `BotMatchService`: серверный случайный бот
|
||
подключается к своему месту через in-process snapshot-соединение,
|
||
выбирает легальный интент и играет casual-матч. Практика не пишет
|
||
историю, не меняет MMR и не выдаёт награды.
|
||
- Клиент: кнопка «С ботом» в редакторе колоды; `MetaService.StartPracticeAsync`,
|
||
переход в `Battle`. Серверный E2E-тест `PracticeBotE2ETests` проходит
|
||
полный матч против бота.
|
||
- Визуал: `CardArt` генерирует процедурные градиентные арты по клану и
|
||
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
||
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
||
действий. Коллекция использует те же арты.
|
||
- Реальные арты: `imageKey` в `/api/cards`, `CardImageService` читает
|
||
`Resources/CardArt/{imageKey}`; скачаны все 124 арта. Импорт:
|
||
`tools/import_card_art.py`.
|
||
- Boost UI: клик по своему `SUP`-слоту выбирает поддержку, затем цель.
|
||
- Event-фидбек: всплывающие атака/guard/drive/урон/heal/retire, не
|
||
блокирует ввод; очередь событий и кнопка «Пропустить анимации».
|
||
- Боевой UX: клик по карте/зоне, подсветка целей, отдельная группировка
|
||
действий, выбор силы/эффекта триггера, журнал последних событий и
|
||
подсказки выбора.
|
||
- Поле: `R V R` / `R R R` у каждого игрока, зона урона слева, колода и
|
||
дроп справа, зеркально у противника, зона защиты между полями.
|
||
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
||
live-практика с ботом на стенде.
|
||
|
||
## 2026-10-22 — перемещения карт и локализация RU/EN
|
||
|
||
- Перемещения карт в бою:
|
||
- при call/ride карта въезжает из руки (свой) или сверху (противник),
|
||
с scale-in;
|
||
- атакующий и цель пульсируют;
|
||
- карта, ушедшая из слота в дроп, оставляет призрак: летит вниз/вверх,
|
||
уменьшается и растворяется; для закрытой карты противника используется
|
||
рубашка, лицо не раскрывается;
|
||
- позиции карт снимаются после layout, поэтому анимации не зависят от
|
||
внешних твинов; при новом snapshot анимации пересобираются.
|
||
- Локализация:
|
||
- `Localization.cs`: `Loc` + `LocalizedText`; русская строка — ключ,
|
||
английский перевод — словарь + фразовые подстановки для динамических
|
||
строк; `да/нет` переводятся по границам слов;
|
||
- `UiKit.CreateText` автоматически вешает `LocalizedText`, поэтому
|
||
существующие экраны переведены без переписывания;
|
||
- переключатель `EN`/`RU` в Login и Lobby; выбор в `PlayerPrefs`
|
||
(`vanguard.ui.language`), применяется на лету;
|
||
- имена/тексты карт: `Loc.CardName` / `Loc.CardText` выбирают
|
||
`nameRu/nameEn` и `textRu/textEn`; карточные поля помечены literal,
|
||
чтобы словарь не портил переводы карт.
|
||
- Проверки: Unity EditMode **4/4** (экраны, импорт арта, перевод статики и
|
||
динамики, live-смена языка, рендер доски с уроном), `SCENE_SMOKE success=True active=Login`.
|
||
- Ограничения: звука пока нет; полноценные drive/damage-анимации карт и
|
||
стола — следующий блок; локализация сделана runtime-словарём, без
|
||
Unity Localization-таблиц (пакет остаётся подключён).
|
||
|
||
## 2026-10-22 — боевые карты, маллиган, урон
|
||
|
||
- Боевые карты переделаны: арт растянут на всю карту, сверху слот/грейд,
|
||
снизу плашка с именем и P/C/триггером; в руке — компактный вариант.
|
||
`CardImageService` проверен тестом: `td01_005` реально загружается.
|
||
- Rest теперь визуально поворачивает карту на бок; удалён старый текстовый
|
||
маркер `REST`. Позванные rear-guard'ы входят стоящими (`Call`,
|
||
`CallFromSoul`, `CallFromDeck` исправлены на `Rested = false`).
|
||
- Зона урона: отдельная сетка видимых карт на боку, до 6 штук; закрытые
|
||
показываются рубашкой, открытые — артом. При выборе карты урона (heal или
|
||
Counter Blast) карты подсвечиваются и кликаются.
|
||
- Heal: сервер больше не забирает последнюю карту автоматически, а создаёт
|
||
`ChoiceTargetKind.Damage`; клиент выбирает карту кликом. Counter Blast
|
||
принимает `DamageIndex` в `ActivateAbilityIntent`/`ResolveAbilityIntent`
|
||
и использует выбранную открытую карту (остаток суммы — авто).
|
||
- Маллиган: клиент выбирает карты в руке, подсвечивает их и отправляет
|
||
`MulliganIntent`; сервер возвращает выбранные в колоду, перемешивает и
|
||
добирает столько же. После обмена доступно «Закончить маллиган».
|
||
- Протокол `Vanguard.Shared` расширен (`DamageIndex`, `ChoiceTargetKind`),
|
||
DLL пересобран и обновлён в `client/Assets/Plugins/Vanguard.Shared`.
|
||
- Проверки: сервер **139** правил + **45** серверных; клиент EditMode
|
||
**4/4** (включая рендер доски с уроном и отдыхающей картой);
|
||
`SCENE_SMOKE success=True active=Login`; `validate` и
|
||
`translations check` — без ошибок.
|
||
|
||
## 2026-10-22 — конспект правил
|
||
|
||
- Источник: `https://www.cardfight.ru/avangard-pravila` (редакция 22.05.2019).
|
||
- Создан `docs/game-rules.md`: форматы, колода, поле, карта, старт/mulligan,
|
||
победа, фазы, бой, триггеры, стоимости, инварианты и отдельная таблица
|
||
сверки с текущим кодом.
|
||
- Расхождения из первой сверки исправлены:
|
||
урон входит открытым, Counter Blast переворачивает открытое в закрытое;
|
||
heal доступен при `>=`; mulligan — возврат в низ → добор → shuffle, один
|
||
раз, оба игрока; в первый ход атака запрещена; первый игрок случайный
|
||
(для тестов можно зафиксировать); `DeckValidator` требует ровно 16
|
||
триггеров, декбилдер показывает счётчик.
|
||
- Карты на поле (`V`, `R`) и в круге защиты принудительно отдаются
|
||
клиенту открытыми; в круге защиты дополнительно рисуется ряд карт.
|
||
- Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер
|
||
и Grade support.
|
||
|
||
## 2026-10-22 — 3D Playmat
|
||
|
||
- Боевое поле переведено в 3D: `Runtime/Board3D` (`Config`, `Data`,
|
||
`Logic`, `View`), HUD/кнопки остаются uGUI.
|
||
- Раскладка: локальный игрок всегда внизу; передний ряд `RC–V–RC`, задний
|
||
`RC–RC–RC`; у противника зеркально. Между половинами — общий круг защиты
|
||
(карты обоих игроков и текущий Guard). Душа — стопка под V, колода и дроп
|
||
сбоку, урон — две колонки по три карты на боку. Карты в V/R всегда
|
||
рисуются лицом вверх, даже если снимок пришёл с `FaceUp=false`.
|
||
- `PlaymatLayoutConfig` (ScriptableObject): размеры карт, позиции рядов,
|
||
высоты, рука, камера, палитра. Ассет:
|
||
`Runtime/Board3D/Config/PlaymatLayoutConfig.asset`.
|
||
- `PlaymatLayout` — чистая геометрия и поиск цели броска; `BoardPlacementRules`
|
||
— Ride только на V, Call только на пустой RC, Move только в своей колонке
|
||
(0↔3, 2↔4, занятый круг меняется местами), Guard — в общий круг.
|
||
- `Card3D` — тонкая коробка: отдельные лицо/рубашка/торец, коллайдер, арт из
|
||
`CardImageService`, переворот вокруг X, Rest вокруг Y по часовой стрелке,
|
||
урон — против часовой, плавный снап. Карты пулятся (`GameBoard3D`),
|
||
материалы общие, instancing включён. Рубашка — стандартная VF Vanguard
|
||
`Resources/CardArt/vanguard_back.png` (источник: Card Sleeve Gallery,
|
||
`Sleeve_06.png`), общая с 2D-UI; при отсутствии файла остаётся
|
||
процедурный fallback.
|
||
- `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики,
|
||
drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и
|
||
индикатором точки броска. Ошибка интента возвращает карту на место.
|
||
- Камера фиксирована по центру поля: позиция `(0, 4.2, -8)`, цель
|
||
`(0, 0, 0)`, FOV 46; наклон только по X, без бокового смещения, игрок её
|
||
не двигает. Рука не лежит на столе,
|
||
а закреплена в экране снизу по центру (`handViewportX/Y`,
|
||
`handViewDistance`) и всегда развёрнута лицом к игроку; уменьшена, чтобы
|
||
не перекрывать задний ряд RC. Драг из руки уходит на стол.
|
||
- Статистика карт вынесена в отдельные UI-панели: своя — снизу слева,
|
||
противника — сверху справа (`Рука/Колода/Дроп/Урон/Душа`); 3D-подписи
|
||
стопок и зон больше не дублируют числа.
|
||
- Колода и дроп поменяны местами: колода ближе к игроку. Пустой дроп не
|
||
рисует рубашку; верх дропа показывает последнюю положенную карту, ЛКМ
|
||
открывает список всего дропа.
|
||
- Душа — это юниты под картой VC, отдельной стопки на поле нет. ПКМ по
|
||
VC открывает список души; кнопка души из топ-бара убрана.
|
||
- Над каждой колодой — триггер-зона (`TRIGGER`), куда выкладывается карта
|
||
проверки эффекта/урона. Урон — один вертикальный ряд карт на боку.
|
||
Под картами V/R — динамические `P/C`, жёлтые при изменённой силе/крите.
|
||
- Справа от круга защиты — панель фаз: «Закончить муллиган» (авто после
|
||
обмена), «Пропустить смену», «В бой», «Закончить ход». Боевая фаза
|
||
завершается сама, если нет атак/активаций/выборов.
|
||
- Event-фидбек guard/drive/intercept показывает имя карты и держится
|
||
1.5–2 с; добавлены паузы, чтобы успеть прочитать защиту бота.
|
||
- Атака проходит при силе атакующего **>=** силы защищающегося.
|
||
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
||
проверяется `SceneSmoke.RunPlaymat`.
|
||
- Проверки: EditMode **22/22** (геометрия, зеркальность, правила, рендер
|
||
3D-карт, пул, рука в поле зрения, обмен колоды/дропа, триггер-зона),
|
||
`SCENE_SMOKE success=True`,
|
||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
||
модели карт/стола — процедурные примитивы.
|
||
|
||
## 2026-10-23 — UX Phase 1: единые списки карт и зоны
|
||
|
||
- Добавлен общий `CardListView` (Screen Space, горизонтальный ScrollRect,
|
||
порядок «последняя добавленная первой») и `CardDetailOverlay` (арт, имя,
|
||
клан/тип/грейд, P/Shield/C, триггер, текст). Навигация:
|
||
`OverlayNavigationService` — стек «список → детали → назад», закрытие
|
||
возвращает к списку.
|
||
- Зоны стали кликабельными блоками UI: `Рука / Колода / Дроп / Душа / Урон /
|
||
Защита / Триггер`; своя панель снизу слева, противника — сверху справа.
|
||
3D-стопки и правый клик по VC используют тот же `CardListView`.
|
||
- Скрытые данные: колода — только закрытые слоты по количеству; рука
|
||
противника — только рубашки; закрытый урон — «Закрытая карта». Рука
|
||
противника дополнительно рисуется веером закрытых карт у дальнего края
|
||
стола (вариант 1), identities не запрашиваются.
|
||
- Старые `SoulOverlay`/`ShowDrop`/`ShowSoul` удалены; единый оверлей
|
||
используется для Deck/Drop/Soul/Damage/Guardian/Trigger/Hand.
|
||
- Проверки: клиент EditMode **22/22** (включая `CardListView`/Detail и
|
||
зоны), `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||
- Дальше по плану: Phase 2 — `InputLockService` + `AnimationConfig` +
|
||
контекстное меню; Phase 3 — Power/Shield и стрелка; Phase 4 — Guardian
|
||
стопка, Trigger mirror, debug-подписи.
|
||
|
||
## 2026-10-23 — UX Phase 2–4: ввод, контекст, бой и раскладка
|
||
|
||
- `InputLockService` — reference-count по причинам (`feedback`, `board`,
|
||
`overlay`, `card-menu`); ввод возвращается только после последней причины.
|
||
`AnimationConfig` (ScriptableObject в `Resources/AnimationConfig.asset`):
|
||
скорости карт, длительности фидбека/флипа/retire, хвост блокировки и
|
||
стрелка. `Card3D` получает скорости из конфига, `GameBoard3D` блокирует
|
||
доску на время снапа/переворота, `BattleView` — на время очереди событий
|
||
и открытых оверлеев. Кнопки интентов/фаз и клики по столу не проходят,
|
||
пока идёт анимация; «Пропустить анимации» остаётся доступной.
|
||
- Контекстное меню `CardActionMenuView`: ЛКМ по своей карте V/R открывает
|
||
список действий, отфильтрованных по `LegalIntents` (атака, атака с
|
||
поддержкой, перемещение, способность, подробнее). Действия с целями
|
||
переводят в режим выбора цели; карты противника и зоны открывают
|
||
`CardDetailOverlay`/списки. Меню закрывается на новом снимке и держит
|
||
`card-menu` в InputLock.
|
||
- Боевая плашка `POWER X vs SHIELD Y` в верхней части экрана: сила
|
||
атакующего против силы защищающегося (с учётом перехватчика) плюс
|
||
`GuardShield`; пересчитывается на каждом снимке, `NULLIFIED` показывается
|
||
отдельно. `AttackArrowView` рисует затухающую 3D-стрелку с наконечником от
|
||
атакующего к цели; точки обновляются каждый кадр по трансформам карт,
|
||
поэтому стрелка едет за анимацией.
|
||
- Guardian-карты повёрнуты на 90° вокруг Y, уложены с перекрытием около 30%
|
||
(spacing 0.62 при высоте 0.88), новые сверху за счёт микро-подъёма.
|
||
Trigger-зона зеркальна по X и ориентации; 3D-подписи `DECK`/`DROP`/
|
||
`TRIGGER` скрыты флагом `showZoneLabels` (по умолчанию false).
|
||
- Проверки: клиент EditMode **26/26** (InputLock, контекстное меню, стрелка
|
||
атаки, загрузка `AnimationConfig`), `SCENE_SMOKE success=True`,
|
||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||
- Дальше: по желанию вынести overlay-экраны и меню в prefab-ассеты; проверить
|
||
итоговые позиции на реальном разрешении и, при необходимости, подкрутить
|
||
`AnimationConfig`/`PlaymatLayoutConfig`.
|
||
|
||
## 2026-10-23 — Overlay prefabs и проверка раскладки
|
||
|
||
- `CardListView`, `CardDetailOverlay`, `CardActionMenuView` и
|
||
`ZoneBlockStripView` теперь умеют брать статическую иерархию из
|
||
`Resources/Overlays/*.prefab`; при отсутствии prefab-а сохраняется
|
||
прежняя кодовая сборка. Динамические плитки/пункты/подписи по-прежнему
|
||
создаются кодом.
|
||
- `OverlayPrefabBuilder` (`Vanguard → Rebuild Overlay Prefabs`) собирает
|
||
prefab-ассеты из тех же view-классов; `SceneBuilder.BuildAll` вызывает его
|
||
автоматически. Runtime-загрузка — `OverlayPrefabs`.
|
||
- Добавлен `Battle_Layout_Fits_Reference_Resolution`: проверяет блоки зон,
|
||
панель фаз и боевую плашку на 1920×1080 и отсутствие пересечений; при
|
||
другом разрешении CanvasScaler масштабирует reference-раскладку.
|
||
- Проверки: клиент EditMode **30/30**, `SCENE_SMOKE success=True`,
|
||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||
- Дальше: только 3D-модели/наполнение сцен, анимации retire/ghost и
|
||
декбилдер; prefab-оверлеи и раскладка закрыты.
|
||
|
||
## 2026-10-23 — Windows standalone
|
||
|
||
- Добавлен `StandaloneBuild`: меню `Vanguard → Build Windows Standalone` и
|
||
CLI `-executeMethod Vanguard.Editor.StandaloneBuild.BuildWindowsCli`.
|
||
Собирает все включённые сцены в `client/Builds/Windows/VanguardReMaster.exe`
|
||
(папка в `.gitignore`).
|
||
- В `GraphicsSettings.m_AlwaysIncludedShaders` добавлены URP Lit/Unlit,
|
||
чтобы runtime-материалы `SceneEnvironment`/`BoardMaterials` не теряли
|
||
шейдеры при сборке. `Paint`/`CreateUnlit` теперь безопасны при
|
||
отсутствующем шейдере.
|
||
- Проверка сборки: Windows x64 build succeeded; при запуске с реальным
|
||
D3D12-устройством исключений нет. `-nographics` для этого приложения
|
||
не показателен: runtime-материалы требуют графического устройства.
|
||
- Сервер для standalone по умолчанию — `http://192.168.31.158:18080`;
|
||
его нужно поднять/держать доступным.
|
||
|
||
## 2026-10-23 — Исправления боя и камера
|
||
|
||
- Первый ход: `EnterBattleCommand` больше не попадает в легальные интенты,
|
||
пока действует правило первого хода; кнопка «В бой» не появляется.
|
||
Атака по-прежнему запрещена.
|
||
- Смена: в фазе Ride предлагается и карта того же ранга, и следующего
|
||
(раньше legal-команды содержали только `+1`, хотя движок разрешал same
|
||
grade). Кнопки «Сменить: карта» есть в панели действий и в «Фазах».
|
||
- Маллиган: кнопка динамическая — без выбора «Закончить муллиган», с
|
||
выбором «Заменить X карт».
|
||
- Способности: `ActivateAbilityIntent` группируются по карте; в UI одна
|
||
кнопка «Активировать способность». Если вариантов несколько — overlay
|
||
выбора способности; карта для сброса выбирается кликом по руке.
|
||
- Guard: варианты Perfect Guard/сброса показываются отдельным overlay;
|
||
случайный первый интент больше не выбирается. В фазе защиты есть
|
||
«Защититься» (при наличии допустимых карт) и «Не защищаться». Карта
|
||
сброса выбирается кликом по карте в руке.
|
||
- Правую панель действий можно временно скрыть/показать кнопкой в верхней
|
||
строке, чтобы видеть поле. При скрытии панель фаз и UI противника
|
||
сдвигаются вправо.
|
||
- Кнопка муллигана в панели «Фазы» динамическая: без выбора — «Закончить
|
||
муллиган», с выбором — «Заменить X карт». После обмена муллиган
|
||
завершается автоматически, кнопка «Закончить муллиган» скрывается и не
|
||
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
||
взводится повторно из снапшота.
|
||
- InputLock чистит зависшие причины, если owning-состояние уже неактивно
|
||
(нет reconnect/overlay/ability/card-menu/feedback). Это убирает блокировку
|
||
«Сменить» после хода противника, когда сервер уже прислал ride-интенты.
|
||
Отдельно исправлено накопление `board`-лока: `GameBoard3D` больше не
|
||
делает повторный `Acquire` на каждом снапшоте во время анимации.
|
||
- Опциональные AUTO-способности (`PendingAbility`, например Barcgal после
|
||
рейда на Marron) показываются модальным оверлеем с названием карты и
|
||
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
||
убраны. После подтверждения призыва карта из души не ставится в первый
|
||
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
|
||
клиент подсвечивает свободные RC и принимает выбор кликом по кругу, без
|
||
списка кнопок. Боковая панель пока оставлена для остальных действий.
|
||
- Зафиксировано правило взаимодействия: выбор карты/круга/зоны/цели — клик
|
||
или drag по объекту на поле; кнопки — только глобальные действия и
|
||
подтверждения (`AGENTS.md`, `docs/client-visual-direction.md`).
|
||
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
|
||
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
|
||
триггеров и опциональная способность. Промпт информационный, не
|
||
перехватывает клики; обновляется из снапшота и не показывается в ход
|
||
противника.
|
||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||
а не за дальним краем.
|
||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||
(+25%); drag сохраняет базовый масштаб.
|
||
- Рука центрируется при скрытии боковой панели (`handViewportX = 0.5`);
|
||
при показе возвращается к значению из layout-конфига.
|
||
- Камера боя: `(0, 4.2, -8)` → цель `(0, 0, 0)`, только наклон по X;
|
||
поле по центру, без бокового смещения.
|
||
- UI: reference-разрешение **1920×1080**, CanvasScaler `matchWidthOrHeight
|
||
= 0.5`; standalone принудительно `1920×1080 FullScreenWindow`,
|
||
`defaultIsNativeResolution = 0`.
|
||
- Потеря WebSocket: полноэкранный `ReconnectOverlay` блокирует бой,
|
||
автоматические попытки 0/2/10/30 с, после `ServerHello` overlay
|
||
скрывается; в overlay есть выход в меню. Игра работает в фоне
|
||
(`runInBackground = 1`), чтобы heartbeat не останавливался при потере
|
||
фокуса.
|
||
- Диагностика Ride: `MatchGrain` логирует создание/attach, каждый полученный
|
||
интент, результат, события и снапшот. В строке состояния есть active,
|
||
turn, phase, step, printing/ранг вэнгарда, рука с рангами и `rideReady`
|
||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||
- Боевые drag-жесты: задний ряд → свой передний круг/Авангард объявляет
|
||
поддержку и ограничивает следующую атаку только поддерживаемой картой;
|
||
передний круг → карта противника объявляет атаку. Поддерживать можно
|
||
передний RC и Авангард (центральный задний круг).
|
||
- Barcgal: добавлена ACT-способность `[Rest]` — поиск Llew/Flogal в колоде,
|
||
выбор круга и призыв; AUTO-призыв Barcgal из души после рейда сохранён.
|
||
Найденные карты показываются модальным оверлеем с карточными плитками
|
||
(арт + имя), выбор — кликом по карте, без списка кнопок.
|
||
- Morgana: после принятия опциональной способности карта для сброса
|
||
выбирается кликом по карте в руке; доступные карты подсвечиваются,
|
||
кнопок выбора в боковой панели нет.
|
||
- Проверки: сервер **144** правил + **45** серверных, `validate` и
|
||
`translations check` OK; клиент EditMode **36/36**.
|