240 lines
17 KiB
Markdown
240 lines
17 KiB
Markdown
# Архитектура Vanguard ReMaster
|
||
|
||
**[C]** Ниже — фактическое устройство репозитория. Если задача требует
|
||
изменить границу систем, сначала проверь существующий код и спроси
|
||
пользователя; не вводи параллельную архитектуру.
|
||
|
||
## 1. Технологии и версии
|
||
|
||
- **Unity:** `6000.6.6f1` (Unity 6), URP `17.6.0`, Input System `1.20.0`,
|
||
uGUI `2.6.0`, Addressables `4.1.1`, Localization `1.5.13`, Newtonsoft.Json
|
||
`3.2.2`, VContainer `1.19.0`.
|
||
- **C#/сервер:** .NET SDK `10.0.100`, `TargetFramework net10.0`,
|
||
`LangVersion latest`, `Nullable enable`, implicit usings. Фактически это
|
||
современный C# (C# 14 toolchain); не используй preview-фичи без проверки.
|
||
- **Сеть:** WebSocket `/game`, бинарные MessagePack-кадры; контракты —
|
||
`Vanguard.Shared` (`netstandard2.1` для Unity + `net10.0` для сервера).
|
||
- **Сервер:** ASP.NET Core, EF Core/PostgreSQL, Redis, BCrypt, Orleans
|
||
(`Microsoft.Orleans.Server 10.4.0`), DTO/проекции.
|
||
- **Клиент:** сцены `Boot`, `Login`, `Lobby`, `DeckBuilder`, `Battle`;
|
||
3D-поле + uGUI HUD. DOTween/UniTask **не установлены**; анимации —
|
||
`AnimationConfig`, `Tick`, корутинные/сервисные последовательности.
|
||
|
||
## 2. Границы решения
|
||
|
||
```text
|
||
content/ → Vanguard.Content → Vanguard.Rules ← Vanguard.Server
|
||
↑ ↓
|
||
Vanguard.Shared ← Unity client
|
||
```
|
||
|
||
- **`Vanguard.Rules`** — чистая библиотека правил: карты, колоды, состояние,
|
||
`GameEngine`, события, реплеи. Не добавлять сеть, БД, Orleans, Unity,
|
||
`File`/`Directory` или UI.
|
||
- **`Vanguard.Content`** — загрузка YAML, каталог, валидация контента,
|
||
токенизация текста/переводы. Не содержит правил хода.
|
||
- **`Vanguard.Shared`** — DTO, MessagePack-контракты, union-типы, проекции
|
||
(`MatchProjection`), маппинг `GameCommand ↔ IntentPayload`. `netstandard2.1`
|
||
не должен ссылаться на `Vanguard.Rules`; проекции собираются в `net10.0`.
|
||
- **`Vanguard.Server`** — ASP.NET, auth/meta, matchmaking, тикеты, WebSocket,
|
||
`MatchGrain`, хранение и Orleans-adapter. Зависит от Rules/Shared/Content;
|
||
обратной зависимости быть не должно.
|
||
- **Unity client** — только представление и ввод; не является источником
|
||
истины и не повторяет правила сервера, кроме локальных UX-проверок.
|
||
|
||
## 3. Серверная игровая логика
|
||
|
||
### 3.1. `GameEngine`
|
||
|
||
- Единственный владелец изменения `GameState` в матче.
|
||
- Принимает `GameCommand`, проверяет фазу/шаг/зону/стоимость/цели,
|
||
порождает `GameEvent` и меняет состояние.
|
||
- `GameState`: игроки, зоны, фазы, `Step`, `BattleState`, pending trigger/
|
||
ability/choice/order/queue, RNG, `InTransit`, winner.
|
||
- `Phase`: `Mulligan, Stand, Draw, Ride, Main, Battle, End`.
|
||
- `Step`: `None, Guard, Drive, Trigger, Ability, Choice`.
|
||
- `CardInstance`: `Id`, `PrintingId`, `Rested`, `FaceUp`, модификаторы,
|
||
granted abilities, restrictions, overrides.
|
||
- `CardDefinition` — логическая карта по `nameId`; `CardPrinting` — принт/арт.
|
||
- `EffectivePower/EffectiveCritical` пересчитывают CONT-бонусы и модификаторы.
|
||
- `LegalCommands()` — список допустимых команд для текущего игрока; UI
|
||
использует проекцию, но сервер всё равно перепроверяет.
|
||
|
||
### 3.2. События, способности, порядок
|
||
|
||
- `AbilityTrigger`: `OnRideOver`, `OnPlace`, `OnBoost`, `OnPhaseStart`,
|
||
`OnAttackDeclared`, `OnAttackHit`, `OnBoosted`, `OnBoostedHit`,
|
||
`OnDriveCheck`, `OnGuard`, `OnRetireFromRC`, `OnOpponentRetire`,
|
||
`OnLeaveGuardian`, `OnDamageTaken`.
|
||
- `AbilityDefinition` (AUTO): `Trigger`, `Condition`, `Optional`,
|
||
`CanPay`, `Pay`, `Resolve`.
|
||
- `ActivatedAbility` (ACT): `CanActivate`, `RestsSelf`, `RetiresSelf`,
|
||
`RequiresDiscard`, `FromHand`, `FromDropZone`, `Resolve`.
|
||
- `ContinuousAbility` (CONT): `Applies`, `PowerBonus`, `CriticalBonus`;
|
||
исключения — `CardScript.CanBeBoosted` и `RestrictionType`.
|
||
- `TriggerFromCard`/`TriggerAll` собирают срабатывания; `RaiseBatch` либо
|
||
сразу предлагает одно, либо формирует `PendingOrder`.
|
||
- `_resolvingDepth` + `PendingQueue`: новые эффекты во время эффекта не
|
||
исполняются рекурсивно, а разрешаются после текущего.
|
||
- `PendingChoice` (`AskUnitChoice`, `AskUnitMultiChoice`,
|
||
`AskHandCardChoice`, `AskEmptyRearGuardChoice`, `AskDamageChoice`) — единый
|
||
механизм выбора; клиент не должен сам решать, что выбрано.
|
||
|
||
### 3.3. Матч, сеть, хранение
|
||
|
||
- `MatchGrain` — один матч, все входы через `SemaphoreSlim`; хранит
|
||
`GameEngine`, журнал идемпотентности `intentId`, таймеры, места.
|
||
- `GameWebSocketEndpoint` принимает соединение; `MatchConnectionRegistry`
|
||
связывает игрока с матчем; `MatchTicker` двигает часы/таймауты.
|
||
- `MatchActorGrain` — Orleans-обёртка: активация/восстановление, вызовы
|
||
`MatchGrain`; доменная логика остаётся в `MatchGrain`.
|
||
- `IMatchStore`/`EfMatchStore` — snapshot/header в PostgreSQL;
|
||
`MatchRecoveryService` восстанавливает; Redis используется для тикетов/
|
||
очереди/кластера, не как единственная истина матча.
|
||
- Интент: `ClientIntent { intentId, basedOnSeq, intent }` →
|
||
`MatchGrain.SubmitAsync` → проверка идемпотентности/очереди →
|
||
`IntentMapping.ToCommand` → `GameEngine.Execute` → события →
|
||
`ServerIntentResult` + `ServerEvents` + проекция.
|
||
- `MatchProjection.ForViewer` скрывает: руку противника, колоду, закрытый
|
||
урон, seed/RNG, чужие pending-выборы; показывает свои руку/урон/зоны.
|
||
- `eventSeq` общий и монотонный; скрытые события занимают номер, поэтому
|
||
пропуски в `ServerEvents` нормальны. Клиент обновляет `LastSeq` по
|
||
`toSeq`, а не по последнему видимому событию.
|
||
|
||
### 3.4. Правила потоков
|
||
|
||
- ASP.NET/HTTP — мета, auth, коллекция, колоды, магазин, история, тикеты.
|
||
- WebSocket — только матч.
|
||
- Все ошибки наружу — `ServerIntentResult`/`ServerError`; исключения не
|
||
должны ломать матч.
|
||
- Reconnect: `ClientHello/resumeFromSeq`; при невозможности — полный
|
||
snapshot. Матч не пересоздаётся из-за обрыва.
|
||
- Сервер не доверяет `slot`, `printingId`, `damageIndex`, целям; проверяет
|
||
их по своему состоянию.
|
||
|
||
## 4. Клиент Unity
|
||
|
||
### 4.1. Composition и сцены
|
||
|
||
- `VanguardApp` — persistent composition root: `VContainer`, `AppConfig`,
|
||
`TokenStore`, `PlayerState`, `ApiClient`, `AuthService`, `MetaService`,
|
||
`SceneFlow`.
|
||
- `SceneFlow` — единая точка переходов; `Boot` служебная, `Login`, `Lobby`,
|
||
`DeckBuilder`, `Battle` — отдельные сцены.
|
||
- `SceneEnvironment` строит 3D-фон; `SceneControllerBase` — доступ к
|
||
контейнеру/Canvas; `BattleSceneController` создаёт `BattleView`.
|
||
- UI в основном code-generated uGUI (`UiKit`), статические боевые overlay
|
||
prefab'ы лежат в `Resources/Overlays`.
|
||
- Reference-разрешение интерфейса — **1920×1080**, `CanvasScaler
|
||
matchWidthOrHeight = 0.5`; standalone открывается в 1920×1080 fullscreen.
|
||
- Сервисы не зависят от конкретной сцены.
|
||
|
||
### 4.2. Сеть и состояние
|
||
|
||
- `GameConnection`: connect, hello, send intents, read loop, ping, pending
|
||
intents, `LastSeq`, `CurrentView`, reconnect.
|
||
- UI не открывает сокет напрямую; `BattleView` читает `MatchView` и
|
||
отправляет `IntentPayload`.
|
||
- После snapshot/hello поле перерисовывается из snapshot, а не из догадок.
|
||
- UI узнаёт об изменениях через события `GameConnection`
|
||
(`SnapshotReceived`, `EventsReceived`, `ClockReceived`) и перерисовку
|
||
`BattleView.Render`; не строить отдельную клиентскую модель правил.
|
||
- Клиент не меняет авторитарное состояние; локальные правила
|
||
(`BoardPlacementRules`) только для подсветки/драга.
|
||
- Все сетевые операции — `async`/`CancellationToken`; не блокировать
|
||
Unity-поток.
|
||
- `TokenStore` сейчас хранит dev-токены в `PlayerPrefs`; для релиза нужен
|
||
защищённый store. Авторитарное состояние матча клиент не сохраняет —
|
||
источник истины и persistence на сервере.
|
||
|
||
### 4.3. Боевой вид
|
||
|
||
- `BattleView` — HUD, оверлеи, меню карт, выборы, стоимость, guard, trigger,
|
||
feedback; `Tick()` вызывается из сцены.
|
||
- `GameBoard3D` — 3D-поле, слоты, рука, стопки, пул `Card3D`, drag/drop,
|
||
диффы снапшота, `CardMoveAnimator`, `CameraDirector`, `ArrowDirector`.
|
||
- `Card3D` — карта: арт/рубашка/торец, `SetPose`, Rest/флип, arc move,
|
||
подсветка, stats; пул не уничтожает объект каждый кадр. Rest — +90° по
|
||
часовой вокруг Y; карта урона лежит на боку против часовой; Trigger Zone
|
||
своей стороны +90°, противника −90° относительно базы; флип — вокруг X.
|
||
Эффект кладёт карту в Trigger до разрешения, затем в руку/урон.
|
||
- `CardMoveAnimator` — очередь дуговых перемещений, stagger, `InputLock`
|
||
`card-move`, таймаут/`CancelCard`; не оставлять лок навсегда.
|
||
- `CameraDirector` — снимок pose, zoom/return/restore; `EnsureCamera` не
|
||
перезаписывает камеру, пока режиссёр активен.
|
||
- `TriggerCheckSequence` — zoom → reveal → return, цепочка, InputLock
|
||
`trigger-sequence`, возврат перед выбором цели.
|
||
- `ArrowDirector`/`AttackArrowView` — attack/boost стрелки, grow/dash,
|
||
якоря у края карт.
|
||
- `AnimationConfig` (ScriptableObject) — длительности, скорости, camera,
|
||
arrow, stagger; `PlaymatLayoutConfig` — геометрия/камера/рука/зоны.
|
||
- `InputLockService` — счётчик причин; `ReleaseAll` только по состоянию UI/
|
||
сбою, не для обхода законной блокировки.
|
||
|
||
## 4.4. C# и Unity-соглашения (фактические)
|
||
|
||
- **Сервер:** file-scoped namespaces, `Nullable enable`, implicit usings,
|
||
проекты `net10.0`; доменные типы — `sealed class`/`sealed record`,
|
||
`required` для обязательных полей, `init` для неизменяемых данных.
|
||
Исключения — только для инфраструктурных ошибок; игровые отказы —
|
||
`CommandResult.Reject`.
|
||
- **Клиент:** block-scoped namespaces; `MonoBehaviour` для сцен/компонентов;
|
||
`ScriptableObject` для `AnimationConfig`, `PlaymatLayoutConfig`,
|
||
`ArrowConfig`; приватные поля `_camelCase`, публичные свойства/методы
|
||
`PascalCase`, сериализуемые поля — `lowerCamelCase`.
|
||
- **События:** сервер — `GameEvent`/`TriggerFromCard`; клиент — C# `event
|
||
Action<T>` для UI/board. Не смешивай игровые события с UnityEvent, если
|
||
нужна чистая логика.
|
||
- **Асинхронность:** сеть/сервер — `async`/`Task`/`CancellationToken`;
|
||
клиентский UI — `Tick()` из сцены и сервисные таймеры. DOTween/UniTask
|
||
не установлены; не добавляй их без решения пользователя.
|
||
- **Производительность:** пул `Card3D`, общие материалы/шейдеры, не
|
||
`Instantiate/Destroy` каждый кадр; не создавай `Material`/`Texture2D` в
|
||
`Update`; кешируй `GetComponent`.
|
||
- **Сцены/префабы/метаданные:** сцены ссылаются на скрипты по GUID; при
|
||
копировании сцен/контроллеров копируй `.meta`; не открывай второй Unity
|
||
с тем же проектом; batch — копия без `Library`.
|
||
- **Ресурсы:** статический UI/карты — `Resources`/`Resources/Overlays`;
|
||
Addressables-пакет есть, но арт пока грузится через `CardImageService` из
|
||
`Resources/CardArt`. Не обещай Addressables-миграцию без задачи.
|
||
- **Логирование/ошибки:** сервер — `ILogger`; клиент — `Debug.Log`/
|
||
`Debug.LogException`; ошибки пользователю — статус/оверлей, не «сырой»
|
||
stack trace. Не логируй токены, seed и скрытые карты.
|
||
|
||
## 5. Данные и ресурсы
|
||
|
||
- Карты: `content/sets/*.yaml` (BT01/TD01/TD02), `content/translations/ru.yaml`,
|
||
`content/keywords.yaml`, `content/trial-decks.yaml`, `content/pack_odds.yaml`.
|
||
- `CardDefinition` — логика/статы; `CardPrinting` — конкретный арт.
|
||
- 94 карты / 124 принта; 10 кланов; SP — отдельные принты.
|
||
- Арт в клиенте сейчас через `Resources/CardArt/{imageKey}` и
|
||
`CardImageService`; Addressables-пакет установлен, но активная загрузка
|
||
арта ещё не переведена на него.
|
||
- Данные карт не дублировать в ScriptableObject: SO — визуал/конфиг, статы и
|
||
тексты — из каталога/сервера.
|
||
- Тексты карт хранятся шаблонами с токенами (`{auto}`, `{cb:2}`, `{vcrc}`,
|
||
`{clan:kagero}`); переводы не должны ломать токены. Ключевые слова/иконки —
|
||
`content/keywords.yaml`, правила — `docs/translation-rules.md`.
|
||
|
||
## 5.1. Как добавлять функционал
|
||
|
||
1. Сначала правило/механика в `Vanguard.Rules` + тест; не начинать с UI.
|
||
2. Если меняется сетевой контракт — `Vanguard.Shared`, проекция,
|
||
`IntentMapping`, пересборка DLL для Unity; версия/совместимость.
|
||
3. Сервер: `GameEngine`/`MatchGrain`/таймеры; не доверять клиенту.
|
||
4. Клиент: `GameConnection`/`BattleView`/`GameBoard3D`; только представление.
|
||
5. Проверки: rules/server tests, EditMode/smoke, `validate`, translations.
|
||
6. Обновить Skill, если решение долговременное; не дублировать в AGENTS.
|
||
|
||
## 6. Тесты и проверки
|
||
|
||
- `server/tests/Vanguard.Rules.Tests` — `GameEngine`, карты, колоды, протокол,
|
||
каталожные инварианты.
|
||
- `server/tests/Vanguard.Server.Tests` — auth/meta, E2E, matchmaking,
|
||
persistence, Orleans, practice bot, WebSocket.
|
||
- `client/Assets/Vanguard/Tests/Editor` — раскладка, 3D-поле, оверлеи,
|
||
локализация, smoke-инварианты.
|
||
- `SceneSmoke.Run` / `RunPlaymat` — загрузка сцен/песочницы;
|
||
`StandaloneBuild.BuildWindowsCli` — Windows-сборка.
|
||
- Серверные тесты/validate/translations — только `ssh home`/Docker.
|