VanguardReMaster/docs/HANDOFF.md

82 KiB
Raw Blame History

Передача сессии

Состояние проекта на 23 октября 2026. Документ нужен, чтобы новая сессия продолжила работу без повторного разбора истории. Краткая оперативная сводка — docs/SESSION-STATE.md; этот файл хранит подробную историю.


2026-10-23 — защита и оплата способностей

  • Панель «Фазы» переименована в «Действия»; защита перенесена туда.
  • Защита выбирается кликом/драгом по картам в руке и перехватчикам на поле. В панели одна кнопка: «Не защищаться» без выбора, «Защищаться» с выбранными картами. Щит выбранных карт сразу входит в POWER vs SHIELD; после подтверждения карты отправляются в Guardian, шаг защиты завершается.
  • Дубли карт в Guardian (player.Guardian + Battle.GuardCards) устранены дедупликацией по InstanceId.
  • Оплата активируемых способностей: сброс выбирается в руке, Counter Blast — кликом по урону. Доступные и выбранные карты цены подсвечиваются; оплата подтверждается кнопкой «Подтвердить оплату» в панели «Действия». Урон больше не подсвечивается вне режима оплаты. Правый клик отменяет выбор.
  • Тесты клиента: 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. Текущее состояние

Проверка Результат
Тесты сервера 189/189 (144 правил/контрактов + 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:

$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, пройдено 139     (Vanguard.Rules.Tests)
Пройден! : не пройдено 0, пройдено 45      (Vanguard.Server.Tests)
OK: ошибок 0, предупреждений 0
Переведено: 94/94
Партий: 1000, завершено: 1000, ошибок: 0

Клиент (Unity):

& '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

Сервер локально:

dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080
# GET http://localhost:5080/health

Тестовый стенд на ssh home (Linux, Docker):

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. docs/protocol.md — версия 1: кадры, handshake по тикету, intent с intentId и eventSeq, resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение, таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости.
  2. Vanguard.Shared: DTO интентов и событий, MessagePack-резолвер, проекция состояния на каждого игрока, фильтрация скрытых событий, маппинг интентов.
  3. Vanguard.Server — сетевой матч работает: ASP.NET Core + WebSocket, тикеты, IMatchGrain (in-process), матчмейкинг, MMR, идемпотентность, таймеры, реконнект, /health, Dockerfile и compose. Контракт HTTP — docs/server-api.md.
  4. Проекция состояния подключена к грану и проверена E2E-тестом: своя рука и свой выбор полностью, у противника только количества и открытые зоны, seed и RNG не уходят.
  5. Docker Compose — postgres, redis (AOF) и сервер; /health, порт через VANGUARD_PORT. Сервер запускается с Orleans:Enabled=true.
  6. Персистентность:
    • PostgreSQL — заголовок матча + журнал интентов, восстановление повтором команд после рестарта, MatchRecoveryService;
    • Redis — одноразовые тикеты (GETDEL) и очередь с перезагрузкой.

Осталось в серверном направлении:

  1. Мета-API — сделано: JWT, аккаунты, коллекция, колоды, магазин, экономика, онбординг, матчмейкинг по deckId, история и реплеи. playerId остался только как dev-режим при Game:AllowDevQueue=true.
  2. 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.