VanguardReMaster/docs/Development/IMPLEMENTATION_PLAN.md

2587 lines
177 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation Plan
Активный журнал задач и планов изменений. Постоянные правила проекта хранятся
в Skill `vanguard-remaster`; история старых блоков — в `IMPLEMENTATION_CHECKLIST.md`,
`DEVELOPMENT_CHECKLIST.md` и `docs/HANDOFF.md`. Этот файл не заменяет их и не
является источником игровых правил.
## Процесс
1. **DeepSeek** анализирует пожелание, код и зависимости, задаёт вопросы и
создаёт/обновляет раздел задачи. Игровой код на этом этапе не меняется.
2. **DeepSeek (реализация)** читает `AGENTS.md`, Skill и раздел задачи, сверяет
план с текущим кодом, реализует, проверяет и обновляет статусы.
3. Передача — только через файлы репозитория и Git. Память модели и чат не
считаются общим контекстом.
4. Если план устарел или требования изменились, реализация останавливает
зависимую часть, фиксирует причину и при необходимости возвращает задачу на
анализ.
5. Нельзя удалять чужие задачи, невыполненные пункты и историю статусов.
Подробный процесс — `docs/Development/AGENT-WORKFLOW.md`.
## Статусы
- `[ ]` — не выполнено;
- `[x]` — выполнено и подтверждено доступными проверками;
- `[!]` — заблокировано или ждёт решения;
- `[-]` — больше не требуется после согласованного изменения плана.
Для каждого пункта храни отдельно:
- **Реализация:** выполнена / не выполнена / не требуется.
- **Проверка:** выполнена (что именно) / не выполнена (почему).
`[x]` нельзя ставить, если реализация есть, а проверка не запускалась.
## Журнал задач
| ID | Статус | Название | Фаза | Проверка |
|---|---|---|---|---|
| TASK-2026-001 | `[!]` | Плавность карточных перемещений и корректные источники | DeepSeek: анализ; реализация выполнена, ждёт ручной проверки C10 | EditMode 46/46; ручная — пользователь |
| TASK-2026-019 | `[ ]` | Загрузочная сцена и кадр без покадровых аллокаций | DeepSeek: план готов; реализация ожидает | не выполнена |
| TASK-2026-020 | `[x]` | Парсер карт и артов с официального сайта | DeepSeek: анализ и реализация | идемпотентность 0 diff; `validate` не запускался |
| TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь |
| TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь |
| TASK-2026-023 | `[ ]` | DeckBuilder: один экран, мультифильтры и список колоды | DeepSeek: анализ и план готовы; реализация ожидает | не выполнена |
Новые задачи добавляются в конец файла отдельными разделами. Не используй один
раздел для нескольких независимых задач.
---
## Шаблон задачи (не является активной задачей)
### TASK-YYYY-NNN — краткое название
- **Статус:** `[ ]`
- **Создано:** YYYY-MM-DD
- **Анализ:** DeepSeek
- **Реализация:** DeepSeek
- **Затронутые системы:** ...
#### A. Цель и требования
- Что просит пользователь.
- Ожидаемое поведение.
- Обязательные правила Skill.
- Ограничения, которые нельзя нарушать.
- Критерии готовности.
#### B. Текущее состояние
- Как работает сейчас.
- Какие системы и файлы проверены.
- Существующие механики, которые нужно сохранить.
- Подтверждённые факты и открытые вопросы.
#### C. Чек-лист изменений
Каждый пункт:
- `[ ]` `MODIFY` / `CREATE` / `DELETE` / `TEST` / `VERIFY`
- путь к файлу (если известен);
- класс/метод (если существует);
- конкретная логика;
- причина изменения;
- ожидаемый результат;
- зависимости от других пунктов;
- **Реализация:** не выполнена;
- **Проверка:** не выполнена.
Пример формата:
```text
- [ ] MODIFY `client/Assets/.../BattleView.cs` — `BattleView.SomeMethod`
Логика: ...
Причина: ...
Результат: ...
Зависит от: ...
Реализация: не выполнена
Проверка: не выполнена
```
#### D. Последовательность выполнения
Нумерованный порядок с обязательными зависимостями. Разделяй работу по системам
на небольшие шаги.
#### E. Проверки
- Какие тесты/команды доступны и подтверждены проектом.
- Какие механики проверить на регрессию.
- Критерии готовности.
- Что остаётся на ручную проверку пользователя.
#### F. Риски и неизвестные вопросы
- Противоречия и побочные эффекты.
- Вопросы, требующие ответа пользователя.
- Что нельзя реализовывать до ответа.
#### G. Результат
- **Реализовано:** ...
- **Проверки:** ...
- **Не завершено/заблокировано:** ...
- **Skill обновлён:** да/нет, почему.
- **Git:** commit hash, изменённые файлы.
---
### TASK-2026-001 — Плавность карточных перемещений и корректные источники
- **Статус:** `[!]`
- **Создано:** 2026-10-24
- **Анализ:** DeepSeek выполнен
- **Реализация:** Kimi выполнена
- **Проверка:** автотесты 46/46; ручная C10 — пользователь
- **Затронутые системы:** `GameBoard3D`, `Card3D`, `CardMoveAnimator`,
`BattleView`, `AnimationConfig`, EditMode-тесты.
#### A. Цель и требования
Пользователь сообщил: анимации движения карт «отрабатывают рвано»,
запускаются ненужные анимации, карты двигаются не из нужных позиций. Нужно
исправить именно презентационный слой, не меняя правила игры.
Ожидаемое поведение:
- анимация запускается только при реальном переходе карты между зонами/слотами
или при изменении позы, а не при каждом UI-рендере;
- источник анимации — фактическая старая зона/поза карты (рука, поле, Trigger,
Damage, Drop, Soul, Guardian, Deck), а не всегда колода;
- рубашка/лицо источника учитываются: карта из колоды начинает движение
рубашкой вверх, а не лицом;
- повторный снапшот или локальный `Render` не создаёт второй запрос на ту же
карту и не откатывает её в середину пути;
- скрытые карты (рука противника, закрытый урон) не пересоздаются и не летят
из колоды на каждом `Render`;
- существующие правила, серверные контракты, `InputLockService`, пул карт и
публичные визуальные эффекты не ломаются.
Ограничения:
- не менять `GameEngine`, правила, сервер, протокол и YAML-контент;
- не добавлять DOTween/UniTask/новые пакеты;
- не менять сцены/префабы без отдельного решения;
- не переписывать `CardMoveAnimator` на другой animation stack;
- геймплей вручную проверяет пользователь; автотесты показывают только
техническую корректность.
#### B. Текущее состояние и найденные причины
Проверены:
- `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs`;
- `client/Assets/Vanguard/Runtime/UI/BattleView.cs`;
- `client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs`;
- `client/Assets/Vanguard/Resources/AnimationConfig.asset`;
- `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
Факты:
1. **Скрытые карты с `InstanceId = 0` пересоздаются.** В
`GameBoard3D.AddOpponentHandSpecs` (`OH:player:i`) и
`GameBoard3D.AddDamageSpecs` (`D:player:i`) для скрытых карт
`BoardCardRef.InstanceId` равен `0`. В `GameBoard3D.Render` распределение
старых карт идёт только через `previousByInstance` и условие
`spec.Ref.InstanceId != 0`. Поэтому при каждом `Render` такие карты
арендуются заново, старые освобождаются, а новые получают `moved = true` и
анимацию из `SourcePose`.
2. **`BattleView.Render()` вызывается не только на снапшот.** Локальные
действия (выбор guard, меню карты, подтверждение, статус) тоже вызывают
`BattleView.Render` → `GameBoard3D.Render`. В сочетании с п.1 это даёт
«ненужные анимации» на каждый клик.
3. **`SourcePose` всегда использует колоду.** Метод
`GameBoard3D.SourcePose` возвращает `_layout.DeckPosition(localSeat)` и
базовый yaw без состояния лица. Новая карта, пришедшая из Soul, Drop,
Guardian, Trigger или Damage, начинает полёт из колоды; карта из колоды
начинает путь лицом вверх, хотя должна быть рубашкой вверх.
4. **У Damage ключ по индексу, а не по личности.** `D:player:i` остаётся тем
же при смене открытой карты или при открытии закрытой. Текущий branch
`sameInstance` освобождает старую карту и создаёт новую с `SourcePose`
(колода), хотя это может быть переворот/смена карты на месте.
5. **Нет coalescing в `CardMoveAnimator`.** `Enqueue` не отменяет старый
queued/active request той же `Card3D`. Если карта успевает сменить зону до
старта предыдущего запроса, второй запрос может стартовать из устаревшей
позиции; активная запись при этом может остаться в `_active`.
6. **`Card3D.SetPose` не является точкой начала движения.** Для существующей
карты `Render` вызывает `SetPose` (меняет target), затем `Enqueue`; для
новой карты `PlaceImmediate` + `SetPose`. При повторном `Render` target
может быть обновлён до/после старта, а старый arc не всегда
согласован с новым request.
7. **Конфиг анимаций не синхронизирован с кодом.** В
`AnimationConfig.asset` нет новых полей `moveDuration`,
`importantMoveDuration`, `moveDelayBetweenCards`, `arcHeight`, camera/arrow
полей, хотя они объявлены в `AnimationConfig.cs`. Нужно проверить
фактические значения после импорта Unity; нельзя считать asset
настроенным, пока поля не сериализованы.
8. **`UpdateHandView` каждый кадр делает `SetPose` для руки.** Это может
перетирать target во время движения карты в руку и мешать стабильности
arc; нужно проверить и, если подтверждено, не менять target у карты,
которая уже летит, либо синхронизировать target с move request.
9. **Пул карт и `InputLock` уже имеют защиту** (`CancelCard`, таймаут,
`CancelAll`); её нельзя удалять. Исправления должны сохранить отсутствие
вечного `card-move`-лока.
Что сохранить:
- `GameBoard3D` как единственный рендер-дифф и владелец пула;
- `CardMoveAnimator` как единый планировщик;
- `Card3D` как визуальную карту и arc-интерполяцию;
- `InputLockService` и существующие long-running последовательности;
- серверный snapshot как источник состояния.
#### C. Чек-лист изменений
- [x] **C1. `MODIFY` `GameBoard3D.Render`** — стабильная идентичность
анонимных карт.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Добавлена ветка reuse-by-key до поиска по `InstanceId`.
- Переиспользуется `Card3D` по `spec.Key`, если старая/новая карта анонимна
или совпадают реальные `InstanceId`; иначе используется `previousByInstance`
или `RentCard`.
- При смене `0→real` в том же слоте карта сохраняется и обновляется
`Bind`/`SetArt`/`SetPose` без анимации.
- Карты с исчезнувшими ключами освобождаются через `ReleaseCard`.
- Реализация: выполнена.
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
`Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone` — проходят.
- Зависит от: C2, C4.
- [x] **C2. `MODIFY` `GameBoard3D.AddOpponentHandSpecs` /
`AddDamageSpecs`** — стабильные ключи и явная политика анимации.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Ключи `OH:player:i` и `D:player:i` оставлены; `InstanceId=0` не считается
личностью.
- Скрытые карты при стабильном ключе не анимируются; новая скрытая карта в
конце анимируется из Deck.
- Для урона: смена `0↔real` или `real→real` в том же слоте обрабатывается
reuse-by-key, а не полётом из Deck.
- Реализация: выполнена.
- Проверка: см. C1 + `Board_Renders_Field_Hand_Guardian_Damage_And_Soul`.
- Зависит от: C1.
- [x] **C3. `MODIFY` `GameBoard3D` — карта источника из предыдущего
`MatchView`.**
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Сохраняются `_previousView` и `_sourceByInstance` до замены `_view`.
- Построен `Dictionary<int, CardSourcePose>` для `Soul`, `Drop`,
`FaceUpDamage`; источники `Hand`/`Guardian`/`Trigger`/`Field` покрываются
существующими `Card3D` через `previousByInstance`.
- `ResolveSourcePose(spec)`: приоритет — `Card3D` (transfer), затем зона из
`_sourceByInstance`, затем `DeckPosition` с рубашкой вверх.
- Источники:
- Deck: `DeckPosition`, рубашка вверх (`baseYaw * X-flip 180°`);
- Soul: `SoulPosition`, лицом вверх;
- Drop: `DropPosition`, лицом вверх;
- FaceUpDamage: `DamagePosition`, лицом вверх и на боку;
- Hand/Guardian/Trigger/Field — через `previousByInstance`/transfer.
- Fallback Deck используется только с комментарием и покрывается тестами.
- `PlaceImmediate` получает rotation с учётом flip.
- Реализация: выполнена.
- Проверка: `New_Field_Unit_Starts_From_Hand_Not_Deck` — проходит.
- Зависит от: C1, C2.
- [x] **C4. `MODIFY` `CardMoveAnimator.Enqueue`** — один активный/queued
request на карту.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`.
- В начале `Enqueue` вызывается `CancelCard(card)`, что удаляет queued/
active request этой же карты, не трогая другие.
- Источник (`from*`) передаётся из `GameBoard3D` как текущая визуальная
позиция карты; target проверяется в `Card3D.StartArcMove`.
- `CancelCard`, `CancelAll`, timeout и снятие `card-move`-лока сохранены.
- Реализация: выполнена.
- Проверка: `CardMoveAnimator_Coalesces_Requests_For_Same_Card`,
`CardMoveAnimator_Releases_Input_Lock_When_Card_Is_Cancelled` — проходят.
- Зависит от: C1.
- [x] **C5. `MODIFY` `GameBoard3D.Render` + `BattleView.Render`** — явно
различать snapshot-рендер и локальный UI-рендер.
- Файлы: `GameBoard3D.cs`, `client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
- `GameBoard3D.Render` получил параметр `bool animateMoves = true` без
изменения DTO.
- `BattleView.Render(bool animateMoves = true)`; `OnSnapshot` вызывает
`Render()` (`true`); все локальные вызовы заменены на
`Render(animateMoves: false)`.
- При `animateMoves: false` move-анимации не запускаются; идентичность карт
не меняется. Появление реально новой карты логирует warning.
- Реализация: выполнена.
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
`ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards` — проходят.
- Зависит от: C1, C2.
- [x] **C6. `MODIFY` `GameBoard3D.UpdateHandView`** — не перетирать движение
руки.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- В `UpdateHandView` добавлено условие: пропускать карту, если
`card.IsMoving`, чтобы `SetPose` не менял target во время arc-перехода.
- `GetHandViewPose` и привязка руки к камере не изменены.
- Реализация: выполнена.
- Проверка: `Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player`,
`New_Field_Unit_Starts_From_Hand_Not_Deck` — проходят.
- Зависит от: C3, C4.
- [x] **C7. `MODIFY` `AnimationConfig.asset` + проверка значений.**
- Файл: `client/Assets/Vanguard/Resources/AnimationConfig.asset`.
- Добавлены/сериализованы все поля из `AnimationConfig.cs`:
`moveDuration: 0.5`, `importantMoveDuration: 0.9`,
`moveDelayBetweenCards: 0.18`, `arcHeight: 0.45`,
`cameraZoomDuration: 0.35`, `triggerRevealHold: 1.2`,
`cameraReturnDuration: 0.4`, `cameraRezoomDuration: 0.3`,
`growDuration: 0.35`, `dashScrollSpeed: 1.5`,
`boostGrowDuration: 0.3`, `boostThickness: 0.8`,
`arrowFadeDuration: 0.2`.
- Длительности feedback не менялись.
- Реализация: выполнена.
- Проверка: `CardOverlayTests.Animation_Config_Asset_Is_Loaded_From_Resources`,
поле `arcHeight` читается в `GameBoard3D.Render` — проходят.
- Зависит от: C5 (можно параллельно).
- [x] **C8. `TEST` `GameBoard3DTests`** — регрессии анимаций.
- Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
- Добавлены тесты:
1. `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`;
2. `Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone`;
3. `New_Field_Unit_Starts_From_Hand_Not_Deck`;
4. `CardMoveAnimator_Coalesces_Requests_For_Same_Card`.
- В `CardMoveAnimator` добавлены read-only seam-свойства:
`PendingCount`, `ActiveCount`, `IsCardQueued`, `IsCardActive`.
- Исправлен `ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards`
для вызова `Render` с новой сигнатурой через reflection.
- Реализация: выполнена.
- Проверка: все новые тесты + старые — 46/46.
- Зависит от: C1–C6.
- [x] **C9. `VERIFY` smoke / compile / EditMode.**
- Скопирован проект без `Library`;
`Unity 6000.6.6f1 -batchmode -nographics -runTests -testPlatform EditMode`.
- EditMode: **46/46 passed** (включая 15 `GameBoard3DTests`, 12
`ClientSmokeTests`, 6 `CardOverlayTests`, 12 `PlaymatLayoutTests`, 1
Addressables stub).
- `PLAYMAT_SMOKE success=True board=True cards=25 hand=5/5 fieldVisible=True`.
- `SCENE_SMOKE success=True active=Login env=True login=True`.
- Ошибок компиляции нет; warning CS8632 в `ClientSmokeTests` — pre-existing.
- Реализация: выполнена.
- Проверка: выполнена.
- Зависит от: C1–C8.
- [ ] **C10. `VERIFY` ручная проверка пользователем.**
- Пользователь проверяет: draw, call, ride, move/swap, guard, trigger,
drive/damage, retire, search, открытие скрытого урона, руку противника,
повторные клики по UI во время/после анимации.
- Ожидаемо: нет полёта скрытых карт из колоды при каждом клике; источник
совпадает с зоной; нет рывков/двойных стартов; ввод не зависает.
- Реализация: не применима.
- Проверка: остаётся на пользователе.
- Зависит от: C9.
#### D. Последовательность выполнения
1. C1 + C2: стабилизировать идентичность/ключи, убрать пересоздание скрытых
карт. Без этого остальные исправления будут маскировать симптом.
2. C3: добавить source map и корректные source pose/face. Проверить Deck,
Hand, Trigger, Damage, Soul/Drop/Guardian.
3. C4: coalescing в аниматоре и отмена старого request.
4. C5: разделить snapshot/UI-рендер, убрать лишние запуски.
5. C6: проверить hand target; не менять без подтверждённой необходимости.
6. C7: синхронизировать asset.
7. C8: тесты; C9: проверки; C10: ручная проверка пользователя.
#### E. Проверки
- Client EditMode: `GameBoard3DTests` + новые тесты из C8.
- Compile/Assembly: Unity batch mode в копии без `Library`.
- Smoke: `Vanguard.Editor.SceneSmoke.RunPlaymat`; `Run` только при изменении
сцен/сборки.
- `git diff --check`, просмотр `git diff`, отсутствие изменений в
Rules/Server/Shared/Content.
- Повторный `Render` с неизменным view: число карт и move-request не растёт.
- Повторный snapshot во время движения: нет двойного старта/сброса в
источник.
- Ручная проверка — пользователем; не заменять её автотестом.
Команды (подтверждены проектом):
```powershell
# клиентская копия без Library
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
-batchmode -nographics -projectPath <copy> `
-runTests -testPlatform EditMode -testResults <copy>\TestResults.xml
```
Серверные тесты не требуются, если не меняются Rules/Server/Shared; при
изменении контрактов — только `ssh home`/Docker.
#### F. Риски и неизвестные вопросы
- **Анонимные карты:** `InstanceId=0` не является уникальной личностью;
нельзя использовать его для глобального map. Нужны стабильные ключи по
зоне/индексу и отдельная политика для reveal.
- **Источник вне видимых зон:** Deck не содержит `InstanceId`; для карты из
колоды источник всегда Deck. Для карты из Soul/Drop/Guardian/Trigger/Damage
источник можно восстановить из previous view; иначе fallback Deck.
- **Смена открытого урона:** при перестановке face-up damage возможна
неоднозначность. Не анимировать перемещение между слотами урона, если это
не подтверждено; приоритет — не «телепорт из колоды».
- **`AnimationConfig.asset`:** после добавления полей Unity может сохранить
старые значения; проверить Inspector, не считать defaults автоматически
настроенными.
- **`UpdateHandView`:** если менять target каждый кадр, можно ухудшить
плавность; C6 требует подтверждения тестом/наблюдением, а не слепой правки.
- **Не менять** `CardMoveAnimator` timeout/`CancelCard`/`CancelAll` без
сохранения защиты от зависшего `InputLock`.
- **Не расширять** задачу на рефакторинг `BattleView` или замену animation
stack.
#### G. Результат
- **Реализовано:** C1–C8 (см. чек-лист).
- **Проверки:** EditMode 46/46 в копии без `Library`; компиляция без ошибок.
- **Не завершено/заблокировано:** C10 — ручная проверка пользователем.
- **Skill обновлён:** не требуется; изменения технические, не затрагивают
подтверждённые правила/архитектуру.
- **Git:** будут изменены `GameBoard3D.cs`, `CardMoveAnimator.cs`,
`BattleView.cs`, `AnimationConfig.asset`, `GameBoard3DTests.cs`,
`ClientSmokeTests.cs`, `IMPLEMENTATION_PLAN.md`, `docs/SESSION-STATE.md`,
`docs/HANDOFF.md`.
---
### TASK-2026-002 — Боевой UX и правила после TASK-2026-001
- **Статус:** `[ ]`
- **Создано:** 2026-10-24
- **Анализ:** DeepSeek выполнен, код не менялся
- **Реализация:** не начата
- **Проверка:** не выполнена
- **Затронутые системы:** `Card3D`, `CardMoveAnimator`, `GameBoard3D`,
`BattleView`, `AnimationConfig`, `TriggerCheckSequence`, `CameraDirector`,
`Vanguard.Rules` (Tejas, Perfect Guard), тесты.
#### A. Цель и требования
Исправить семь замечаний после первого блока анимаций:
1. При взятии карты из колоды она не должна сначала появляться в руке, затем
исчезать и только потом лететь по дуге.
2. При размещении карты из руки не должно быть двойного визуального
размещения: сначала в поле, затем исчезновение и дуга.
3. При призыве из души не должно быть отдельного перемещения перед дугой.
4. Tejas должен атаковать задний ряд только в колонке, расположенной напротив
его колонки, а не в зеркально противоположной.
5. Perfect Guard: сначала поместить карты в защиту, затем проверить
способности и активировать их; текущая логика оплаты/отмены должна быть
приведена к этому порядку.
6. После атаки, drive check, trigger-зоны, урона и других боевых событий
нужны паузы около 1.5 с; анимации должны быть длиннее.
7. При фокусе камеры на trigger-зону скрывать руку; после возврата камеры
возвращать руку.
Ограничения:
- не менять серверные контракты без необходимости; при изменении протокола
пересобрать `Vanguard.Shared.dll`;
- серверные тесты запускать только на `ssh home`/Docker;
- не менять сцены/префабы без отдельного решения;
- геймплей вручную проверяет пользователь.
#### B. Анализ причин
1. **Предварительное smooth-движение queued-карт.** `CardMoveAnimator`
ставит запросы в очередь со stagger. Пока запрос ждёт в `_queue`,
`Card3D.Tick` уже выполняет обычное экспоненциальное движение к
`_targetPosition`. Когда `StartNext` вызывает `StartArcMove`, карта
возвращается в `FromPosition` и начинает дугу. Визуально это выглядит как
«сначала переместилась, потом исчезла, потом дуга». Это объясняет п.1 и
п.3, а также часть п.2 при нескольких перемещениях.
2. **Локальный `DropAt` до снапшота.** `GameBoard3D.FinishDrag` вызывает
`card.DropAt(target)` сразу после отпускания мыши. Карта визуально
встаёт в поле, затем авторитарный снапшот делает `PlaceImmediate(hand/soul)`
и запускает дугу. Это отдельная причина п.2, не связанная с очередью.
3. **Tejas: `SameColumn` используется и для Move, и для Attack.** В
`GameEngine.SameColumn` зашито `0↔3`, `2↔4`, `-1↔1`. Для перемещения
внутри своего поля это верно. Для атаки по противнику это неверно:
клиент зеркалит половину противника (`PlaymatLayout.CirclePosition`),
поэтому атака из слота 3 должна попадать в слот 2, из 4 — в 0, из V — в 1.
Текущий тест `Tejas_Attacks_Back_Row_In_Its_Column` закрепляет неверную
пару `3→0`, его нужно исправить вместе с правилом.
4. **Perfect Guard сейчас — одна команда.** `GuardIntent` содержит и
карту защиты, и `DiscardPrintingId`; `GameEngine.Guard` сразу добавляет
карту в Guardian, сбрасывает карту оплаты и ставит `battle.Nullified`.
Отдельной стадии «карты уже в защите → проверить/активировать
способности» нет. Тесты `Perfect_Guard_*` описывают упрощённую модель.
5. **Задержки не образуют боевой такт.** Длительности feedback уже есть в
`AnimationConfig` (`attackDeclare`, `driveCheck`, `trigger`, `damage` и
др.), но текст события и движение карт идут разными путями: `OnEvents`
ставит feedback в очередь, а `RequestSnapshotAsync` отдельно запускает
`CardMoveAnimator`. Нет явной паузы после боевого события и нет гарантии,
что движение карты завершилось до следующего события. Движение карт
(`moveDuration = 0.5`, `importantMoveDuration = 0.9`) короче, чем нужно
пользователю.
6. **Рука следует за камерой.** `TriggerCheckSequence` двигает
`CameraDirector`, но не сообщает `GameBoard3D` о фокусе. `UpdateHandView`
каждый кадр привязывает руку к камере, поэтому при зуме на trigger-зону
рука оказывается перед камерой.
#### C. Чек-лист изменений
- [x] **D1. `MODIFY` `Card3D` — hold-at-source для queued-перемещений.**
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`.
- Добавить состояние «ожидает arc» и метод
`HoldAtSource(position, rotation, scale)`.
- Пока карта ожидает `StartArcMove`, `Tick` не должен выполнять smooth
interpolation к target; карта остаётся в source pose.
- `StartArcMove` снимает hold и запускает дугу из `FromPosition`.
- `StopMove`/`CancelHold` снимают hold; если запрос отменён, карта может
продолжить обычное smooth-движение к актуальному target.
- Реализация: не начата.
- Проверка: новый EditMode-тест «вторая карта в очереди не двигается до
старта arc».
- Зависит от: D2.
- [x] **D2. `MODIFY` `GameBoard3D.Render` — ставить карту на hold до Enqueue.**
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- После `PlaceImmediate` для перемещаемой карты вызывать
`HoldAtSource(source)`; target остаётся финальной позой из `SetPose`.
- При `animateMoves: false`, `ReleaseCard`, `Clear`, `CancelAll` hold
снимать, чтобы карта не зависла в источнике.
- Убедиться, что `UpdateHandView` не снимает hold и не двигает карту до
старта arc.
- Реализация: не начата.
- Проверка: тесты D1 + smoke; визуально одна дуга без предварительного
перемещения.
- Зависит от: D1.
- [x] **D3. `MODIFY` drag-and-drop — убрать двойное размещение.**
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`,
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
- Решение пользователя: **вариант A** — после отпускания мыши не вызывать
`DropAt` для server-authoritative перемещения.
- Оставить карту в source (или показать временный ghost/индикатор цели),
а после снапшота один раз анимировать из source в target.
- Ошибка/отказ сервера возвращает карту в source без второй анимации.
- Реализация: не начата.
- Проверка: call из руки и move/swap не показывают «поставилась →
исчезла → дуга».
- Зависит от: D1–D2.
- [x] **D4. `TEST` клиентские регрессии перемещения.**
- Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
- Добавить тесты:
1. вторая карта в очереди не двигается до `StartArcMove`;
2. отмена queued-запроса не оставляет карту в hold;
3. `Removing_Card_From_Hand...` и существующие тесты не ломаются;
4. повторный рендер после drag-drop не создаёт второй arc-запрос.
- Реализация: не начата.
- Проверка: EditMode в копии без `Library`.
- Зависит от: D1–D3.
- [x] **D5. `MODIFY` `Vanguard.Rules` — колонка Tejas через границу игроков.**
- Файлы: `server/src/Vanguard.Rules/Engine/GameEngine.cs`,
`server/tests/Vanguard.Rules.Tests/GameEngineTests.cs`.
- Оставить `SameColumn` для перемещений внутри поля.
- Добавить `SameColumnAcrossPlayers(attackerSlot, defenderSlot)`:
`3↔2`, `4↔0`, `-1↔1` — подтверждено пользователем.
- Использовать новую функцию в `LegalCommands` для `AttackCommand` и в
`Attack` при проверке `targetSlot is 0/1/2`.
- Исправить тест `Tejas_Attacks_Back_Row_In_Its_Column`: атакующий 3 →
защитник 2; добавить 4→0, V→1 и негативный тест на противоположную
колонку.
- Проверить, что клиентские `LegalIntents` и подсветка цели не требуют
изменений: сервер начнёт присылать правильный `TargetSlot`.
- Реализация: не начата.
- Проверка: серверные тесты на `ssh home`/Docker.
- Зависит от: нет.
- [x] **D6. `MODIFY` Perfect Guard — стадийная защита и полная модель.**
- Файлы: `server/src/Vanguard.Rules/Engine/GameEngine.cs`,
`server/src/Vanguard.Shared/Protocol/Intents.cs`,
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`, тесты.
- Решение пользователя: **полная модель**, оплата спрашивается **после
размещения всех карт защиты**.
- Целевой порядок:
1. игрок выбирает и помещает карты защиты в Guardian (включая Sentinel,
пока без оплаты);
2. после размещения сервер проверяет способности карт защиты;
3. для Sentinel открывается pending ability «оплатить сброс карты того
же клана»;
4. при активации — сброс, выбор атакованного юнита, который не может
быть поражён до конца боя; при отказе — щит 0, атака проходит.
- Текущее упрощение `battle.Nullified = true` заменить на корректную
модель «защищённый юнит не может быть поражён» (поле боя/флаг
protected unit), если она не эквивалентна текущему `Nullified` в
рамках одного защищающегося.
- `GuardIntent.DiscardPrintingId` больше не должен быть обязательной
частью размещения; оплата идёт отдельным guard-ability intent
(`ResolveAbilityIntent`/новый тип). При изменении протокола
пересобрать `Vanguard.Shared.dll` и обновить replay codec.
- Обновить `Perfect_Guard_*` тесты: размещение без оплаты → pending
ability → оплата/отказ; добавить тест «сначала все карты, потом
оплата».
- Реализация: не начата.
- Проверка: серверные тесты на `ssh home`/Docker; ручная проверка
пользователем.
- Зависит от: D5 (общий серверный блок можно делать параллельно).
- [x] **D7. `MODIFY` `AnimationConfig` + боевой такт.**
- Файлы: `client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs`,
`client/Assets/Vanguard/Resources/AnimationConfig.asset`,
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
- Решение пользователя: паузы **1.5 с** после атаки, drive check,
trigger-зоны, урона и других боевых событий.
- Добавить `eventPause = 1.5`, `postDrivePause = 1.5`,
`postTriggerPause = 1.5`, `postDamagePause = 1.5`.
- Увеличить движение: `moveDuration = 0.8`,
`importantMoveDuration = 1.3`, `moveDelayBetweenCards = 0.22`,
`triggerRevealHold = 1.5`, `cameraZoomDuration`/`cameraReturnDuration`
= 0.5.
- Связать feedback и завершение `CardMoveAnimator`: не отпускать
`feedback`/`board` lock и не начинать следующий такт, пока не истекла
пауза и не завершились карточные перемещения.
- Сохранить кнопку «Пропустить анимации» и корректное снятие всех lock.
- Реализация: не начата.
- Проверка: smoke + визуальная проверка пользователем.
- Зависит от: D1–D2.
- [x] **D8. `MODIFY` trigger-камера и рука.**
- Файлы: `client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs`,
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`,
`client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Добавить в `TriggerCheckSequence` событие `FocusChanged(bool)`:
`true` при первом `ShowTrigger`, `false` при возврате камеры в Idle,
`EndChain`/`Cancel`.
- В `BattleView` подписаться и вызывать
`GameBoard3D.SetHandVisible(!focused)`.
- Решение пользователя: скрывать руку **только на trigger-зуме**.
- В `GameBoard3D` добавить `_handVisible` и метод `SetHandVisible(bool)`:
скрывать/показывать только свою руку; `UpdateHandView` и `Render` не
должны возвращать скрытые карты.
- Восстанавливать руку при `Clear`, reconnect, завершении боя и отмене
цепочки.
- Реализация: не начата.
- Проверка: EditMode-тест hide/show + ручная проверка trigger.
- Зависит от: D7 (общая цепочка такта).
- [ ] **D9. `VERIFY` сборка и проверки.**
- Клиент: EditMode в копии без `Library`, `PLAYMAT_SMOKE`,
`SCENE_SMOKE`, компиляция.
- Сервер (только при D5/D6): тесты правил и серверные на `ssh home`,
`validate`, при изменении текста карт — `translations check`.
- Проверить, что изменения в `Vanguard.Rules`/`Vanguard.Shared` не
затрагивают клиентскую `Vanguard.Shared.dll` без пересборки.
- Реализация: не начата.
- Проверка: после реализации.
- Зависит от: D1–D8.
- [ ] **D10. `VERIFY` ручная проверка пользователем.**
- draw, call из руки, call из души, ride, move/swap, guard,
Perfect Guard (оплата/отказ), trigger, drive/damage, retire, Tejas
слева/справа/центр, скрытие/возврат руки при trigger-камере.
- Ожидаемо: одна дуга без предварительного перемещения; Tejas бьёт
визуально в свою колонку; Perfect Guard сначала размещает карты, затем
спрашивает способность; паузы ~1.5 с; рука не мешает trigger-зуму.
- Реализация: не применима.
- Проверка: только пользователь.
- Зависит от: D9.
#### D. Последовательность выполнения
1. D1–D2: hold-at-source, убрать предварительное smooth-движение. Это
устраняет основную причину п.1 и п.3.
2. D3–D4: drag-and-drop и тесты; проверить п.2.
3. D5: Tejas (отдельный серверный блок, можно параллельно).
4. D6: Perfect Guard — полная модель и стадийная оплата после размещения.
5. D7–D8: длительности, боевой такт, рука и trigger-камера.
6. D9–D10: проверки и ручная приёмка.
#### E. Проверки
- Клиент: EditMode (в копии без `Library`), `PLAYMAT_SMOKE`,
`SCENE_SMOKE`, компиляция без ошибок.
- Сервер: `Vanguard.Rules.Tests`, `Vanguard.Server.Tests` на `ssh home`;
`validate`; `translations check` только при изменении текста карт.
- Регрессии: draw, call из руки/души, move/swap, guard, trigger, damage,
Tejas, Perfect Guard.
- `git diff --check`; не должно быть случайных изменений сцен/префабов.
#### F. Решения и риски
**Зафиксировано пользователем:**
1. **Drag-and-drop:** вариант A — не ставить карту локально, одна дуга
после снапшота.
2. **Perfect Guard:** полная модель; оплата спрашивается после размещения
всех карт защиты.
3. **Tejas:** колонки `3↔2`, `4↔0`, `V↔1`.
4. **Длительности:** паузы 1.5 с; `moveDuration = 0.8`,
`importantMoveDuration = 1.3`.
5. **Рука:** скрывать только на trigger-зуме.
**Технические риски:**
- **Протокол Perfect Guard:** изменение `GuardIntent`/`GuardCommand` может
потребовать пересборки `Vanguard.Shared.dll`, обновления replay codec и
клиентского `BattleView`. Делать одним согласованным блоком.
- **Полная модель Perfect Guard:** может понадобиться отдельное состояние
«защищённый юнит» и событие выбора юнита; текущий `battle.Nullified`
нельзя считать полной моделью.
- **Drag-and-drop вариант A:** карта может визуально вернуться в source,
пока ждёт снапшот. Нужен ghost/индикатор цели, чтобы это не выглядело
как откат.
- **Длинные анимации:** паузы 1.5 с удерживают `feedback`/`board` lock;
кнопка «Пропустить анимации» должна снимать все lock и не оставлять
очередь.
- **Серверные проверки:** Tejas и Perfect Guard затрагивают `Vanguard.Rules`,
тесты и `validate` — только на `ssh home`/Docker.
- **Тест Tejas:** текущий `Tejas_Attacks_Back_Row_In_Its_Column` закрепляет
неверную пару `3↔0`; при реализации он должен быть изменён.
**Открытых вопросов, блокирующих старт, нет.**
#### G. Результат
- **Реализовано:** D1–D8.
- **Проверки:** EditMode 50/50 в копии без `Library`; компиляция клиента
чистая.
- **Не проверено:** D5/D6 — серверные тесты правил только на `ssh home`;
D10 — ручная проверка пользователем.
- **Примечание по D6:** Perfect Guard реализован как отдельная
автоматическая способность Sentinel, срабатывающая после размещения
конкретной карты в Guardian. Полное откладывание способности до момента
«все карты защиты размещены» (до `PassGuard`) потребует дополнительного
серверного состояния; текущий вариант устраняет основную жалобу
«сначала карта, потом способность».
- **Skill обновлён:** после прохождения серверных тестов и ручной проверки
обновить `docs/game-rules.md` и Skill (Tejas, Perfect Guard).
---
### TASK-2026-003 — Визуальный редизайн боевого HUD по мокапу
- **Статус:** `[ ]`
- **Создано:** 2026-10-25
- **Анализ:** DeepSeek выполнен, код не менялся
- **Реализация:** не начата
- **Проверка:** не выполнена
- **Затронутые системы:** `BattleView`, `ZoneBlockStripView`, `GameBoard3D`,
`PlaymatLayoutConfig`, `BoardSlotView`, `BoardZoneView`, `Pile3D`,
`Card3D`, `UiKit`, локализация, EditMode/smoke.
#### A. Цель
Приблизить боевой экран к мокапу: тёмный неоновый 3D-стол, тонкий верхний
HUD с номером хода и фазовой шкалой, компактные панели игроков, полноширинное
поле без постоянной правой панели, крупная рука и понятные глобальные
действия. Мокап — визуальный ориентир, а не замена принятым UX-правилам.
#### B. Что в мокапе уже совпадает с текущим клиентом
- 3D-стол с двумя зеркальными половинами, V по центру переднего ряда и
R-кругами 3×2.
- Рука игрока веером снизу, рука противника — сверху.
- Колода и урон сбоку; у локального игрока урон слева, колода справа,
у противника зеркально.
- Тёмная палитра, синие/красные акценты, разделительная линия по центру.
- Карты — 3D-объекты с артом, рубашкой, Rest и подсветкой.
- Часы и банк времени уже приходят с сервера (`ClockView`), номер хода и
фаза уже есть в `MatchView`.
#### C. Расхождения и необходимые доработки
- [ ] **V1. Убрать постоянную правую панель «Действия» и `PhasePanel`.**
- Сейчас `IntentPanel` шириной 456 px постоянно занимает правую часть
экрана; мокап показывает поле на всю ширину.
- Панель действий и `PhasePanel` удалить из боевого HUD.
- Все подтверждения перенести в правый нижний угол: `End Turn`,
`В бой`, `Защищаться/Не защищаться`, `Закончить муллиган`,
`Подтвердить оплату`, `Пропустить способность` и аналогичные
контекстные подтверждения.
- Нижняя панель показывает только доступные сейчас подтверждения; при
отсутствии действий она пустая/скрыта, а не занимает место постоянно.
- `ActionPrompt` оставить неблокирующим информационным слоем.
- Журнал, статус и отладочные сведения не входят в обычный HUD. Временная
кнопка «Показать/скрыть панель» открывает только debug-панель; она
скрыта по умолчанию. Это временное решение, не финальный HUD.
- [ ] **V2. Верхний HUD: номер хода + фазовая шкала.**
- Добавить `Turn N` и неинтерактивную шкалу `Standby → Main → Battle →
End` (для `Mulligan` — отдельная подпись).
- Источник: `MatchView.TurnNumber`, `MatchView.Phase`, `MatchView.Step`.
- Текущая фаза подсвечивается; пройденные — приглушены; `Guard/Drive/
Trigger/Ability/Choice` показывать как подпись текущего шага, не ломая
шкалу фаз.
- Локализовать через `Loc`; не хардкодить английские строки.
- [ ] **V3. Верхний HUD: таймер и банк.**
- Разделить на два визуальных блока: `00:00` — время хода, `Bank 03:52`
— банк. Сейчас они склеены в одну строку.
- Оставить серверный источник времени и текущую логику `UpdateClock`.
- При остановленном таймере (`Clock.IsRunning == false`) показывать `—`,
не запускать локальный отсчёт.
- [ ] **V4. Кнопки `Concede` и меню.**
- `Сдаться` — компактная кнопка с иконкой флажка; подтверждение оставить.
- Кнопку «Показать/скрыть панель» оставить временно; она открывает только
debug-панель (журнал/статус/лог). Панель по умолчанию скрыта; кнопка не
должна занимать место, когда панель не нужна.
- Если понадобится меню настроек/выхода — использовать отдельную
компактную кнопку, не совмещённую с debug-панелью.
- [ ] **V5. Компактные панели игроков вместо двух рядов зон.**
- Мокап: `Hand / Deck / Damage / Shield / Trigger` в одной строке.
- Сейчас `ZoneBlockStripView` показывает 7 зон в двух рядах, включая
`Drop`, `Soul`, `Guardian`.
- Основная панель: 5 ключевых счётчиков. В баре игрока оставить доступ к
`Drop` и `Soul`; `Guardian` в бар не добавлять — он показывается только
в боевом контексте. Отдельную кнопку «Зоны» не вводить.
- Содержимое зон не выкладывать на стол: по клику на `Drop`/`Soul` в баре
слева снизу (свой игрок) или справа сверху (противник) открывать
оверлей соответствующей зоны.
- Смотреть списком можно только `Drop` и `Soul`. Счётчик `Deck` —
неинтерактивный, по клику ничего не открывает.
- `Shield` показывать только в боевом контексте (текущий `GuardShield`),
вне боя — `0`; не путать со статическим щитом карты.
- Имя игрока и метка `ВЫ/ПРОТИВНИК` — в заголовке панели.
- [ ] **V6. Полноширинное поле и центральный баннер хода.**
- После V1 пересчитать безопасные отступы камеры/поля под 16:9 и 21:9.
- Добавить центральный баннер `YOUR TURN · MAIN PHASE` (world-space или
screen-space, `raycastTarget = false`), скрывать его во время
trigger-камеры и боевых оверлеев.
- Баннер не должен перекрывать drag&drop и клики по кругам.
- [ ] **V7. Прямоугольные слоты вместо круглых рамок.**
- Мокап: прямоугольные пластины с внутренней рамкой, лёгким свечением и
метками `R`; V — акцентная пластина по центру.
- Сейчас `BoardSlotView` использует `BoardMeshLibrary.Frame` (рамка), что
визуально читается как круг/контур.
- Добавить меш прямоугольной пластины (или собрать из двух quads) и
сохранить существующий `BoxCollider`/логику drop-таргетов.
- Подсветка валидных целей и выбранных кругов должна работать как сейчас.
- [ ] **V8. Зоны урона и колоды.**
- Урон — красный вытянутый прямоугольник сбоку, как в мокапе; карты
остаются в текущих позициях `DamagePosition`.
- Над зоной урона — счётчик `x/6` (например, `0/6`), у противника
зеркально и ближе к центру. Счётчик не должен перекрывать карты урона.
- Колода — стопка с читаемой рубашкой и счётчиком; счётчик `Deck`
неинтерактивный, по клику ничего не открывает. Hover/подсветка стопки
допустимы, но не должны выглядеть как кнопка.
- Выровнять размеры и отступы зон урона/колоды для обеих половин.
- [ ] **V9. Карточный шаблон лица и статы под картой.**
- Мокап показывает рамку, имя, силу/крит, иконку триггера и уровень.
Сейчас `Card3D` рисует арт и отдельный `TextMesh` со статами.
- Добавить процедурный или prefab-шаблон лица: рамка по клану, полоса
имени, иконка триггера, затемнение под текст.
- Счётчик силы и крита вынести **под карту**, а не на лицо: сила всегда,
крит — только если он больше 1. Перед числом использовать иконки
(сила/крит), не буквы `P`/`C`.
- Не менять `BoardArt`/`CardArt` контракт; шаблон — слой поверх арта.
- Для закрытых карт — единая рубашка с логотипом Vanguard.
- [ ] **V10. Свет, свечение и материал стола.**
- Усилить неоновые направляющие, мягкое свечение слотов и краёв стола.
- Использовать существующие URP-материалы/эмиссию; не добавлять новые
пакеты (DOTween/UniTask запрещены).
- Проверить, что bloom/тонмаппинг не ухудшает читаемость карт и текста.
- [ ] **V11. Анимации HUD и карт.**
- Плавное появление/затухание фазового баннера, подсветка смены фазы,
hover-подъём карт в руке, мягкое свечение выбранной карты.
- Не менять уже принятые `CardMoveAnimator`/`CameraDirector` тайминги без
отдельного решения; новые эффекты не должны блокировать ввод.
- [ ] **V12. Локализация и доступность.**
- Все новые подписи — через `Loc` (RU/EN), включая `Turn`, фазы, `Bank`,
`Shield`, `Hand`, `Deck`, `Damage`, `Trigger`, `Concede`.
- Использовать подготовленный русский текст способностей из
`content/translations/ru.yaml` (`textRu`) в карточном оверлее, списках
зон и подсказках способностей; не показывать только EN.
- При отсутствии `textRu` — fallback на EN, но `translations check` должен
проходить без пропусков.
- Проверить 1920×1080, 16:10 и ultrawide; панели не должны перекрывать
руку и кнопки.
- Контраст текста на тёмном фоне, размеры кнопок не меньше текущих.
- [ ] **V13. Не копировать из мокапа буквально.**
- Постоянные кнопки `Ride`, `Call`, `Guard` в нижнем правом углу
конфликтуют с правилом «выбор карты/круга — клик/драг по объекту,
кнопки — только глобальные действия и подтверждения».
- Принято: панель действий не возвращать. В правом нижнем углу — только
подтверждения (`End Turn`, `В бой`, guard-pass/confirm, муллиган,
оплата способности). `Ride/Call` остаются действиями из меню карты/
круга и не дублируются постоянными кнопками.
- `Shield` в панели игрока не должен выглядеть как ещё один статический
ресурс; показывать только актуальный боевой щит.
- [ ] **V14. Техническая реализация без новых систем.**
- Переиспользовать `BattleView`, `ZoneBlockStripView`, `UiKit`,
`BoardSlotView`, `BoardZoneView`, `BoardMeshLibrary`, `Pile3D`.
- Новые визуальные элементы — небольшие view-компоненты/методы, без
дублирования `CombatStats`, `PhasePanel` и `IntentPanel`.
- Серверные контракты не менять: `TurnNumber`, `Phase`, `Clock` уже есть.
- Сцены/префабы менять только после отдельного решения; сначала
code-generated вариант.
- [ ] **V15. Сохранить drag&drop-сценарии как альтернативу клику.**
- Прямой выбор карты кликом остаётся.
- Из руки в круг — призыв/смена (Call/Ride); перемещение по полю — RC↔RC
в своей колонке.
- Во время атаки: перетащить карту заднего ряда на карту переднего ряда —
это поддержка (Boost): карта поворачивается, показывается стрелка,
сила пересчитывается. Затем карту переднего ряда можно перетащить на
цель — атака.
- После объявления атаки проверяются способности поддержки и атакующей
карты в порядке, определённом сервером; клиент не должен сам менять
правила.
- Учесть уже сделанные исправления TASK-2026-002: не вызывать локальный
`DropAt`, не создавать вторую дугу, не ломать `InputLock`.
- Проверки: EditMode на drop-таргеты и порядок интентов; ручная проверка
цепочки boost → attack.
- [ ] **V16. Зум на trigger-зону всегда, карты — после зума.**
- При drive check и damage check камера всегда зумится на trigger-зону,
даже если триггера нет.
- Карты помещаются в trigger-зону после окончания зума, а не до него.
- Если триггера нет — показать плашку «Без триггера» и вернуть камеру.
- Руку скрывать на время зума и возвращать после возврата камеры
(сохранить поведение TASK-2026-002).
- Скоординировать `TriggerCheckSequence`, снапшот и события так, чтобы
карта не появлялась в trigger-зоне раньше камеры.
- Проверки: ручная проверка drive/damage с триггером и без; отсутствие
преждевременного появления карты.
- [ ] **V17. Триггеры: конкретные подписи действий при распределении.**
- Сейчас при выборе цели пишется общее «выберите цель эффекта», а интенты
называются «сила/эффект». Заменить на конкретные действия.
- Первый шаг (PowerTarget): `+5000` / `+5K` — цель для силы.
- Второй шаг (EffectTarget) по типу триггера:
- `Critical` → `Крит +1`;
- `Stand` → `Поднять юнита`;
- `Draw` → `Взять карту` (без второго таргета);
- `Heal` → `Лечить` (выбор карты урона на сервере).
- Показывать действие в статусе/подсказке и, если возможно, меткой над
валидными картами; не оставлять безликое «эффект».
- Учитывать, что `Critical`/`Stand` могут выбирать силу и эффект на разные
цели; клиент не должен сам объединять их без правила.
- Локализовать подписи RU/EN; иконки — по возможности.
- [ ] **V18. Проверки.**
- EditMode: маппинг фаз/шагов в подписи, счётчики панели, `Deck`
неинтерактивен, `Drop/Soul` открывают списки, скрытие/раскрытие
debug-панели, отсутствие raycast-перехвата у баннера, drop-таргеты и
порядок boost → attack, подписи trigger-действий.
- Smoke `SceneSmoke.Run` и `RunPlaymat`; сборка standalone — на копии
проекта без `Library`.
- Ручная приёмка пользователем: 1920×1080, drag&drop, guard, trigger
с триггером и без, боевые оверлеи, длинные имена/локали.
#### D. Порядок работ
1. V1–V4: верхний HUD, временная кнопка панели, отказ от постоянной правой
панели, подтверждения в правом нижнем углу.
2. V5–V6: панели игроков, полноширинное поле, баннер хода.
3. V7–V8: слоты, зоны урона/колоды, счётчик `x/6`.
4. V9–V10: карточный шаблон, статы под картой, свет.
5. V11–V12: анимации, локализация, русский текст способностей,
адаптивность.
6. V15–V16: drag&drop-сценарии и trigger-зум.
7. V17: конкретные подписи trigger-действий.
8. V13–V14, V18: UX-сверка, тесты, smoke, ручная приёмка.
#### E. Открытые вопросы
- **[O]** Временная кнопка «Показать/скрыть панель»: панель по умолчанию
скрыта; когда её убирать — после стабилизации HUD.
- **[O]** Делать карточный шаблон процедурным или prefab-ассетом после
появления финальных рамок?
- **[O]** Нужен ли фазовый баннер в world-space (на столе) или screen-space
(поверх HUD)? Рекомендация — screen-space, чтобы не спорить с камерой.
#### F. Результат
- **Реализовано:** V1–V9, V12–V18.
- **Частично:** V10 — добавлены пластины слотов и рамки карт, без отдельного
bloom-объёма; V11 — новых HUD-анимаций нет.
- **Новые правки после мокапа:** карты урона укладываются сверху вниз
(зеркально у противника), шаг `damageStackOffset = 0.3` от ширины карты,
новая карта приподнята по Y и рисуется поверх; trigger-карта повёрнута
на 90° против часовой стрелки (локально `-90°` для себя, `+90°` для
противника); на карте показывается бейдж триггера (имена убраны);
no-trigger damage check определяется по `DamageTakenView` с подавлением
повторного зума после trigger-карты.
- **Правки по замечаниям:** имена карт на поле убраны; панель действий
сжата до автоподстраиваемого столбца кнопок справа снизу (без заголовка,
фона и скролла); рука центрируется по экрану (`handViewportX = 0.5`);
подсказка муллигана перенесена в верхний `ActionPrompt`, при выборе карт
показывается кнопка «Заменить N карт»; центральный баннер хода убран;
левый нижний бар — `Колода, Душа, Рука, Сброс`, без `Щит/Триг`, все
блоки одинаковые, `Душа/Сброс` кликабельны.
- **Порядок триггера:** камера всегда фокусируется на trigger-зоне;
после фокуса карта раскрывается анимацией (`triggerRevealDuration`),
затем пауза `triggerRevealHold = 0.75 с`, затем резолв. Выбор цели
открывается только после этой паузы. Сначала `+5000` на свою карту в
RC/VC, затем эффект: `Critical` — RC/VC, `Stand` — только повёрнутый RC,
`Draw` — автоматически, `Heal` — выбор карты урона при выполнении
условий. Подсказки пишутся в верхнем информационном блоке.
Нетриггерная карта просто пропускается.
- **Tejas:** исправлена атака заднего ряда по колонке через границу
игроков: `V↔1`, `3↔2`, `4↔0`. Сервер пересобран и задеплоен на
`ssh home`, контейнер `server` перезапущен.
- **Основная фаза:** в панели подтверждений остаётся только кнопка
«Боевая фаза». «Закончить ход» и список всех действий скрыты; активация
способностей — через меню карты.
- **Рамки карт:** собственная рамка карты — тонкая чёрная по её краю;
подсветка выбора/цели тоже ужата до размера карты, больше не наезжает
на соседние карты. В руке рамки скрыты, карты уложены широким веером:
новая карта перекрывает предыдущую и за счёт шага по глубине всегда
рисуется поверх неё. Своя рука заполняется слева направо: первая карта
слева, новые добавляются правее и остаются поверх. Веер сохраняет ширину
до 10 карт, а начиная с 11-й шаг плавно уменьшается. Рука противника —
зеркальный камеро-зависимый веер ниже верхней панели: новая карта
появляется слева и остаётся поверх предыдущей, наклон и порядок глубины
зеркальны. Позы обеих рук пересчитываются каждый кадр под текущую камеру,
поэтому добор не улетает к устаревшей цели.
- **Стартовый Авангард:** лежит закрытым во время муллигана; после
завершения муллиганов обоих игроков открывается с анимацией
(`vanguardRevealDuration = 0.8 с`), затем начинается первая фаза.
Переворот идёт вокруг длинной оси карты слева направо, у противника
зеркально. `MatchProjection` больше не форсирует `FaceUp` для Vanguard
и передаёт реальное состояние карты; клиент блокирует ввод на время
переворота.
- **Старт боя:** камера стартует над стороной игрока лицом к столу,
пауза 1,5 с; затем по дуге опускается на основную позицию, пауза 1,5 с;
затем одновременно раздаются обе руки — своя и противника — с анимацией
из колод. Панель муллигана появляется только после окончания всех доборов.
Ввод заблокирован до конца интро.
- **Взятие карт:** карта создаётся ровно над колодой рубашкой вверх
(рисунок рубашки ориентирован правильно) и летит в руку по дуге с
переворотом лицом вверх. Любой добор идёт последовательно: следующая
карта стартует только после завершения анимации предыдущей. В стартовой
раздаче своя и чужая руки идут парами, внутри каждой пары — по одной.
- **Урон:** face-up/face-down переворот карт урона идёт вокруг длинной
стороны карты, а не поперёк.
- **Проверки:** клиентские EditMode в копии без `Library` — **50/50**;
серверные `Vanguard.Rules` на `ssh home` — **155/155**; standalone-
сборка Windows — успешно; клиент запущен из
`client/Builds/Windows/VanguardReMaster.exe`.
- **Не проверено:** ручная игровая приёмка пользователем.
- **Открытые вопросы:** bloom/свет, HUD-анимации, надёжность эвристики
no-trigger damage check.
---
## 2026-10-27 — TASK-2026-004: убрать боевые баннеры
**Решение пользователя:** все боевые баннеры не нужны; убираем их
полностью, чтобы пересобрать анимации блоками. Верхний `ActionPrompt`
(муллиган/смена/подсказки), `StatusToast` и trigger-сообщения не являются
боевыми баннерами и остаются.
**Анализ:**
- Центральный `BattleFeedback` и полноэкранный `BattleFlash` — это один
feedback-блок: текст события, вспышка damage/heal, кнопка «Пропустить
анимации», очередь и паузы `eventPause`/`post*Pause`.
- Паузы feedback сейчас задают темп событий, но не являются источником
истины; после удаления темп держат `CardMoveAnimator`, trigger-последовательность
и серверные снапшоты.
- `InputLock("feedback")` и `_feedbackLockHeld` существуют только для
этого блока; их нужно убрать вместе с ним.
- `_turnBanner`/`_turnBannerText` объявлены, но не строятся; удалить как
мёртвый код.
- `AnimationConfig.attackDeclare/guard/driveCheck/trigger/battleResolved/
damage/heal/intercept/perfectGuard/eventPause/postDrivePause/
postTriggerPause/postDamagePause/retireDuration` после удаления станут
неиспользуемыми; пока оставляем поля, чтобы не менять контракт ассета.
**Реализация:**
- [x] Удалить `BattleFeedback`, `BattleFlash`, `SkipFeedback` и весь
`FeedbackStep`-механизм.
- [x] Убрать вызовы `ShowFeedback`/`EnqueuePause`/`ShowFeedbackFor`.
- [x] Убрать `InputLock("feedback")` и связанные поля/сбросы.
- [x] Удалить неиспользуемые `_turnBanner`/`_turnBannerText`.
- [x] Оставить журнал событий, `Describe`, trigger-последовательность,
`ActionPrompt` и `StatusToast`.
**Проверка:**
- [x] EditMode в копии без `Library` — 51/51.
- [x] Standalone Windows собирается.
- [ ] Вручную: в бою нет центральных баннеров и вспышек, события идут
без зависаний; журнал и trigger-подсказки работают.
---
## 2026-10-27 — TASK-2026-005: порядок набора руки
**Решение пользователя:** рука локального игрока должна набираться
**слева направо**: первая карта слева, новые добавляются правее. Рука
противника по правилу зеркальности набирается с его стороны поля, то есть
на экране справа налево: новая карта слева.
**Реализация:**
- [x] `GetHandViewPose`: вернуть `x = center + offset * step`, `roll = -offset * roll`.
- [x] `GetOpponentHandViewPose`: сделать зеркально — `x = center - offset * step`,
`roll = +offset * roll`.
- [x] Обновить тест `Hands_Fill_Mirrored_And_Newest_Is_On_Top` под новое
направление и новые карты.
**Проверка:**
- [x] EditMode в копии без `Library` — 51/51.
- [x] Standalone Windows собирается.
- [ ] Вручную: новая карта в своей руке появляется справа; у противника —
слева.
---
## 2026-10-27 — TASK-2026-006: возврат карт из руки в колоду
**Решение пользователя:** карта уходит из руки в колоду по дуге **под низ
колоды** с переворотом рубашкой вверх во время движения. Возврат идёт
**справа налево**; следующая карта стартует только после завершения
предыдущей анимации. Для противника — то же зеркально с его стороны.
**Анализ:**
- Сервер уже сообщает о возврате: `MulliganView` (игрок, количество) и
`CardReturnedToDeckView` (игрок, `PrintingId`). Отдельного per-card события
для муллигана нет, поэтому карты определяются по исчезновению из старой
руки.
- Старый `Card3D` руки сейчас просто освобождается в `GameBoard3D.Render`;
чтобы анимировать, его нужно вынуть из `_cards`, оставить в отдельном
списке, провести дугой и только потом вернуть в пул.
- Для последовательности используем `CardMoveAnimator` с `sequential: true`;
для скрытия после дуги — callback завершения. Это переиспользует общий
arc/flip и очередь, не создаёт отдельный аниматор.
- Цель — позиция колоды минус небольшой `Y`, рубашкой вверх; во время
движения `StartArcMove` интерполирует поворот от лица к рубашке.
**Реализация:**
- [x] `GameBoard3D.QueueHandReturnToDeck` + обработка `MulliganView` и
`CardReturnedToDeckView` из `BattleView`.
- [x] Выбор старых карт руки, удаление из `_cards`, дуга под колоду,
переворот, возврат в пул после завершения.
- [x] `CardMoveAnimator.Enqueue` получил `onCompleted`; последовательная
очередь используется для возврата.
- [x] Тесты: `Hand_Return_To_Deck_Starts_Right_To_Left_And_Targets_Deck_Bottom`,
`CardMoveAnimator_Runs_Sequential_Requests_And_Completes_Each`.
**Проверка:**
- [x] EditMode в копии без `Library` — 53/53.
- [x] Standalone Windows собирается.
- [ ] Вручную: при муллигане/возврате карты уходят по дуге под колоду,
переворачиваются рубашкой вверх, следующая стартует после предыдущей;
у противника зеркально.
---
## 2026-10-27 — TASK-2026-007: общий добор карты
**Решение пользователя:** добор после муллигана должен выглядеть так же,
как добор в муллигане: карта появляется над колодой **полного размера**,
рубашкой вверх, летит в руку по дуге и переворачивается лицом вверх.
**Анализ:**
- `AnimateHandDraw` уже стартует с `_config.cardScale` и `card.FaceDownRotation`.
- Обычный добор шёл через `ResolveSourcePose`: для карты руки он брал
`spec.Scale * cardScale` (размер руки) и плоскую ориентацию колоды, поэтому
над колодой появлялась маленькая карта.
- Нужно вынести общий старт добора: позиция над колодой, полный масштаб,
рубашка в ориентации будущей карты руки.
**Реализация:**
- [x] `GameBoard3D.GetDrawSource` — общая позиция/масштаб старта добора.
- [x] `ResolveSourcePose` для `BoardZone.Hand` использует полный масштаб и
`spec.BaseRotation * FaceDownFlip`.
- [x] `AnimateHandDraw` использует тот же `GetDrawSource`.
- [x] Длительность нового добора — `introDrawDuration` (0.6 с), как в муллигане.
- [x] Тест проверяет, что новая карта стартует над колодой в `cardScale`.
**Проверка:**
- [x] EditMode в копии без `Library` — 53/53.
- [x] Standalone Windows собирается.
- [ ] Вручную: добор после муллигана визуально совпадает с муллиганом;
карта не маленькая.
---
## 2026-10-27 — TASK-2026-008: длительность возврата как у добора
**Решение пользователя:** анимация возврата карты в колоду должна идти
столько же, сколько добор из колоды.
**Реализация:**
- [x] Общая `GameBoard3D.DrawDuration` (`introDrawDuration`, 0.6 с).
- [x] `StartHandReturn` использует `DrawDuration` вместо `moveDuration`.
- [x] Обычный добор использует ту же `DrawDuration`.
**Проверка:**
- [x] EditMode в копии без `Library` — 53/53.
- [x] Standalone Windows собирается.
- [ ] Вручную: возврат в колоду по длительности совпадает с добором.
---
## 2026-10-27 — TASK-2026-009: последовательный возврат карт
**Проблема:** карты возвращались одновременно. `Card3D.SetPose` ставил
цель под колодой, а `_returningCards` тикали все сразу; очередь
`CardMoveAnimator` стартовала дуги по одной, но карты уже плавно уезжали
к цели параллельно.
**Решение:** пока карта ждёт в очереди, удерживать её в исходной позе руки
через `Card3D.HoldAtSource`. Старт следующей — только после завершения
предыдущей (`sequential: true`).
**Реализация:**
- [x] `StartHandReturn` вызывает `HoldAtSource` до `CardMoveAnimator.Enqueue`.
- [x] Тест проверяет, что обе возвращаемые карты остаются в руке до старта
своей анимации.
- [x] Порядок: самая правая карта первой; у противника зеркально.
**Проверка:**
- [x] EditMode в копии без `Library` — 53/53.
- [x] Standalone Windows собирается.
- [ ] Вручную: сначала уходит самая правая карта, затем следующая.
---
## 2026-10-28 — TASK-2026-010: рука перестраивается при выходе карты
**Решение пользователя:** при любом выходе карты из руки остальные карты
должны перестраиваться под новое количество одновременно с её перемещением.
**Анализ:**
- `UpdateHandView` уже считает позы по новому `HandCount` из свежего
`Render`.
- Перекомпоновка руки (`Hand → Hand`) не ставится в `CardMoveAnimator`
(`NeedsMove` возвращает false), поэтому не ждёт анимацию уходящей карты.
- Уходящая карта удаляется из `_handBindings`/`_cards`, а остальные карты
сразу получают новые цели; hand reflow идёт своим smooth-слоем.
**Реализация:**
- [x] Проверка поведения для hand → field.
- [x] Проверка поведения для возврата в колоду: остальные карты получают
новые цели, пока возвращаемая карта ещё ждёт/летит.
- [x] Тесты `Hand_Reflows_While_A_Card_Leaves_From_Hand` и расширенный
`Hand_Return_To_Deck_Starts_Right_To_Left_And_Targets_Deck_Bottom`.
**Проверка:**
- [x] EditMode в копии без `Library` — 54/54.
- [ ] Вручную: при выходе карты рука сдвигается одновременно с её движением.
---
## 2026-10-28 — TASK-2026-011: ступенчатый reflow руки при муллигане
**Решение пользователя:** во время муллигана рука должна сдвигаться **после
каждого** ухода карты, а не один раз под окончательное количество.
**Проблема:** новый `Render` сразу содержал финальный `HandCount` (старые
остатки + новые карты). Остальные карты получали финальные позы, а
возвращаемые удерживались. Визуально рука перестраивалась один раз.
**Решение:**
- Ступенчатый список `_stagedReturnCards`: старые карты руки в исходном
порядке, включая ожидающие возврата; новые карты в него не входят.
- Пока идёт возврат, `UpdateHandView` строит позы только для этого списка.
- После завершения каждой карты она убирается из списка, и оставшиеся
карты (включая ожидающие) плавно сдвигаются под новое количество.
- Ожидающие карты не удерживаются на месте: при старте своей анимации они
получают цель под колодой через `OnStart` и стартуют из текущей позы
(`fromCurrent`).
- После последнего возврата staged-режим выключается; новые карты
добираются в финальную руку.
**Реализация:**
- [x] `BuildStagedReturnHand`, `_stagedReturnCards`, `_stagedReturnPlayer`.
- [x] `CardMoveAnimator` получил `onStart` и `fromCurrent`.
- [x] `OnHandReturnCompleted` убирает карту из staged-списка и сразу
пересчитывает руку.
- [x] Тест проверяет: до первого ухода рука не сдвигается, после первого —
сдвигается; ожидающая карта остаётся в руке до своей очереди.
**Проверка:**
- [x] EditMode в копии без `Library` — 54/54.
- [x] Standalone Windows собирается.
- [ ] Вручную: муллиган с двумя возвратами — рука сдвигается после каждого.
---
## 2026-10-28 — TASK-2026-012: добор по одной карте и ступенчатый reflow
**Проблема:** при доборе все новые карты сразу появлялись над колодой
рубашкой вверх, а рука перестраивалась один раз под финальное количество.
**Решение:**
- Новые карты своей руки скрываются на время очереди и показываются только
в `CardMoveAnimator.onStart` — по одной.
- `_stagedDrawCards` хранит уже видимые карты (старые остатки + завершённые
доборы); новые карты добавляются только после завершения их дуги.
- После каждого добора рука пересчитывается под текущее видимое количество,
а не под финальное.
- После последнего добора staged-режим выключается; финальная рука совпадает
с текущей.
**Реализация:**
- [x] `BuildStagedDrawHand`, `_stagedDrawCards`, `_stagedDrawTargetCount`.
- [x] `BeginHandDrawArc` показывает карту, ставит цель в следующий слот и
запускает дугу из текущей позиции.
- [x] `OnHandDrawCompleted` добавляет карту в staged-список и пересчитывает
руку.
- [x] Повторный snapshot без новых доборов не сбрасывает уже начатую staged
раздачу — иначе остатки руки перестают сдвигаться.
- [x] Тест `Hand_Draw_Stages_New_Cards_One_By_One`.
**Проверка:**
- [x] EditMode в копии без `Library` — 55/55.
- [x] Standalone Windows собирается.
- [ ] Вручную: при доборе над колодой видна одна карта; рука расширяется
после каждой.
---
## 2026-10-28 — TASK-2026-013: единый механизм раскладки руки
**Проблема:** остатки руки после муллигана переставали сдвигаться:
отдельные `_stagedReturnCards` и `_stagedDrawCards` не всегда содержали
одни и те же карты, а повторные snapshot/переходы между режимами ломали
раскладку.
**Решение:** один визуальный список `_visualHandCards` и один override
`_handLayoutOverride`:
- в список сразу входят все оставшиеся карты руки и ожидающие возврата;
- новые добранные карты добавляются только после завершения их дуги;
- после каждого возврата/добора список пересчитывается, поэтому остатки
руки всегда участвуют в сдвиге;
- пока override активен, `UpdateHandView` использует только визуальный
список; после завершения всех возвратов/доборов возвращается обычный
`_handBindings`;
- повторный Render не пересоздаёт список и не сбрасывает прогресс.
**Реализация:**
- [x] `BuildVisualHand`, `_visualHandCards`, `_pendingDrawCards`,
`_handLayoutOverride`.
- [x] Удалены раздельные staged-списки возврата/добора.
- [x] Тест `Mulligan_Remaining_Cards_Reflow_After_Each_Return`.
**Проверка:**
- [x] EditMode в копии без `Library` — 56/56.
- [x] Standalone Windows собирается.
- [ ] Вручную: после муллигана остатки руки сдвигаются после каждого
возврата и каждого добора.
---
## 2026-10-28 — TASK-2026-014: подъём карты в руке при наведении
**Решение пользователя:** карта под курсором плавно приподнимается на 10%
своего размера; после ухода курсора плавно возвращается.
**Реализация:**
- [x] `handHoverLift = 0.1` теперь доля высоты карты, а не мировое смещение.
- [x] `GameBoard3D.UpdateHandHover`: raycast по своей руке, только когда нет
UI/лока/драга/анимаций; оппонент и поле игнорируются.
- [x] `SetHoveredHandCard` пересчитывает руку; `ApplyHandHover` добавляет
камера-вверх на `cardHeight * scale * cardScale * handHoverLift`.
- [x] Плавность — существующий `Card3D.SetPose`/`Tick`; отдельного твина нет.
- [x] Тест `Hand_Hover_Lifts_Card_By_Ten_Percent` (подъём и возврат цели).
**Проверка:**
- [x] EditMode в копии без `Library` — 57/57.
- [x] Standalone Windows собирается.
- [ ] Вручную: наведение поднимает карту, уход курсора возвращает.
---
## 2026-10-28 — TASK-2026-015: drag карты из руки, смена через VC
**Решение пользователя:** карта из руки, отпущенная на VC, делает смену;
пока кнопка мыши удерживается, карта остаётся приподнятой; отпускание в
неположенном месте плавно возвращает её в руку; без дёрганья.
**Реализация:**
- [x] `Card3D.EndDragAtCurrent` фиксирует карту в точке drop-а, не возвращая
её в руку и не сбрасывая подъём/масштаб.
- [x] `GameBoard3D.FinishDrag`: для карты из руки больше не вызывает
`ReturnToOrigin` до ответа сервера; держит карту в `_dropPendingCard`.
- [x] `GameBoard3D.ReturnDroppedCard`: снимает hold и плавно возвращает в
руку, если drop не стал интентом/отклонён.
- [x] `BattleView`: возвращает карту при отсутствии интента, отказе сервера,
ошибке отправки, guard-выборе и раннем выходе.
- [x] Валидный Ride/Call: карта ждёт снапшот и летит в target уже из точки
drop-а; в руку не возвращается.
- [x] Тест `Dropped_Hand_Card_Holds_At_Drop_Until_Returned`.
**Проверка:**
- [x] EditMode в копии без `Library` — 58/58.
- [x] Standalone Windows собирается.
- [ ] Вручную: drag в VC на смену, удержание, возврат из неверного места.
---
## 2026-10-28 — TASK-2026-016: карта при drag под курсором
**Проблема:** при перетаскивании карта уезжала вниз-влево от курсора:
луч наводился на плоскость `cardLift`, а карта висела на `dragLift`; плюс
`DragTo` сбрасывал hand-ориентацию в плоский `Euler(0,yaw,0)`.
**Решение:**
- Плоскость drag-а считается на фактической высоте карты
(`Card3D.DragLift`), поэтому карта остаётся под курсором.
- Для карты из руки `DragLift` = hover-подъём 10% высоты; для карты поля —
прежний `dragLift`.
- `DragTo` сохраняет `_dragOriginRotation`, поэтому hand-карта не
переворачивается на стол при перетаскивании.
- `_dragPointerOffset` сохраняет точку захвата: карта не прыгает центром
под курсор при старте drag-а.
**Проверка:**
- [x] EditMode — 58/58.
- [x] Standalone Windows собирается.
- [ ] Вручную: карта у курсора, hand-ориентация сохранена.
---
## 2026-10-28 — TASK-2026-017: drag не двигает карту в момент старта
**Проблема:** в момент старта drag-а вызывался `UpdateDrag`, карта сразу
переносилась к курсору/поднималась. Кроме того, после старта `UpdateDrag`
больше не вызывался каждый кадр — карта не следовала за мышью.
**Решение:**
- `OnPointerHeld` только включает drag и запоминает точку захвата; позицию
не меняет.
- `HandlePointer` при `_dragging` каждый кадр вызывает `UpdateDrag`, поэтому
карта следует за курсором.
- `OnPointerReleased` больше не дублирует `UpdateDrag`; last target уже
обновлён в этом кадре.
**Проверка:**
- [x] EditMode — 58/58.
- [x] Standalone Windows собирается.
- [ ] Вручную: при начале drag-а карта остаётся на месте, затем следует за
курсором.
---
## 2026-10-28 — TASK-2026-018: выбор зоны без перемещения карты из руки
**Решение пользователя:** карта из руки при зажатии ЛКМ не должна
перемещаться. Курсор наводится на зону; карта остаётся в руке
приподнятой; при отпускании над VC выполняется смена.
**Реализация:**
- [x] Для `source.IsHand` drag заменён на `_handTargeting`: карта не
вызывает `BeginDrag`/`DragTo` и остаётся в hand-раскладке.
- [x] `UpdateHandHover` удерживает hover-подъём нажатой карты, пока идёт
targeting.
- [x] `UpdateHandTargeting` считает только целевую зону по курсору и
показывает drop indicator; карта не двигается.
- [x] Цель сначала определяется raycast-ом по карте/кругу (VC-карта даёт
ровно Vanguard), затем fallback на плоскость.
- [x] `FinishHandTargeting` на release отправляет `CardDropped`; при валидном
VC срабатывает RideIntent, при невалидном карта остаётся в руке.
- [x] Field-карты сохраняют старый drag с движением.
- [x] По серверному логу `rides=[TD02/007EN, BT01/015EN]` — только эти карты
были доступны для смены. Валидные Ride-карты теперь подсвечиваются в руке;
добавлен fallback direct RideIntent и диагностические логи
`[HandTargeting]` / `[Drop]`.
**Проверка:**
- [x] EditMode — 58/58.
- [x] Standalone Windows собирается.
- [ ] Вручную: ЛКМ на карте, наведение на VC, отпускание — смена; карта всё
время остаётся в руке.
---
### TASK-2026-019 — загрузочная сцена и кадр без покадровых аллокаций
- **Статус:** `[ ]`
- **Создано:** 2026-10-30
- **Анализ:** DeepSeek выполнен (пункты A1, C1–C3 в
`docs/Development/CLIENT-SCENES-3D-ANIMATIONS.md`)
- **Реализация:** не начата
- **Проверка:** не выполнена
- **Затронутые системы:** `SceneFlow`, `VanguardApp`, `Boot`, новый
`LoadingOverlay`, `GameBoard3D` (стрелки, рука), `ArrowDirector`,
`AttackArrowView`, `BattleView.UpdateClock`, `Localization`, `InputLockService`,
EditMode-тесты.
#### A. Цель и требования
Первый блок бэклога «сцены, 3D, анимации». Четыре независимых результата:
1. Переход между экранами без вспышки: `Boot` — загрузочная сцена с прогрессом
и fade, затем `Login`; тот же overlay используется для остальных переходов.
2. Стрелки атаки/буста не создают мусор в кадре.
3. Рука не пересчитывается дважды за кадр и не пересчитывается впустую.
4. Строки HUD и локализации не обновляются каждый кадр без изменения значения.
Ограничения:
- без новых пакетов (DOTween/UniTask запрещены);
- HUD из TASK-2026-003 не перекомпоновывать; в `BattleView` допустима только
точечная правка `UpdateClock`;
- правила, сервер, протокол, контент не меняются;
- `CardMoveAnimator` остаётся единственным планировщиком перемещений;
- `AnimationConfig` в этом блоке не меняется (это отдельный пункт C6);
- геймплей вручную проверяет пользователь.
Критерии готовности:
- переход `Boot → Login` и `Login → Lobby` без видимой вспышки, прогресс
отображается;
- в бою без движения карт и стрелок — 0 B/кадр по Profiler (Allocation Call
Stacks);
- позы руки не пересчитываются, если не менялись состав руки, камера, hover и
staged-список;
- таймер/банк и локализованные подписи обновляются только при изменении;
- существующие EditMode-тесты и оба smoke проходят.
#### B. Текущее состояние
1. `SceneFlow.LoadAsync` — `LoadSceneAsync(sceneName, LoadSceneMode.Single)`,
ожидание `isDone` без progress/fade (`SceneFlow.cs:63-80`). `Boot` содержит
камеру, свет и Global Volume; контроллера и UI нет, `VanguardApp` создаётся
в `RuntimeInitializeOnLoadMethod` (`ClientBootstrap.cs:11-19`), `Start`
грузит `Login` (`VanguardApp.cs:59-67`).
2. `GameBoard3D.UpdateBoostArrows` создаёт `new List<(Vector3, Vector3)>` на
каждый вызов (`GameBoard3D.cs:1219-1227`), `BattleCard` использует
`Players.FirstOrDefault(lambda)` (`:1319-1320`), `ArrowAnchor` ищет карту
линейным перебором `_cards.Values` (`:1293-1301`, `:1332-1341`).
3. `GameBoard3D.Update` за кадр вызывает `UpdateHandView`, `_moveAnimator.Tick`,
`TickHandDraw`, `_cameraDirector.Tick`, `card.Tick` по всем картам,
`_returningCards`, затем `_arrowDirector.Tick` и `UpdateAttackArrowPoints`
(`:2220-2255`). Стрелки обновляются двумя путями.
4. `AttackArrowView.ApplyColor` создаёт `new[] { _shaft, _head }` (`:267`) и
вызывается из `Tick` и `SetStyle`, т.е. несколько раз за кадр на стрелку.
5. `UpdateHandView` вызывается из `Update`, `Render`, hover и колбэков
(`:183, 511, 607, 732, 1163, 2230, 2372`); позы обеих рук считаются каждый
кадр.
6. `BattleView.UpdateClock` пишет текст таймера и банка каждый кадр;
`LocalizedText.LateUpdate` проходит `Loc.Translate` на каждый лейбл;
`InputLockService.Describe` собирает строку через LINQ при активном локе.
Что сохранить:
- пул `Card3D` и переиспользование по ключу/`InstanceId`;
- `CardMoveAnimator` как единственный планировщик, его `CancelCard`/`CancelAll`
и таймаут;
- общий `Tick` из `GameBoard3D` (без `Update` на каждой карте);
- `Time.unscaledDeltaTime` и текущую интерполяцию (`Card3D.Tick`);
- геометрию стрелок, раскладку руки, hover-подъём и staged-reflow.
#### C. Чек-лист изменений
- [ ] **C1. `CREATE` `client/Assets/Vanguard/Runtime/UI/LoadingOverlay.cs`** —
персистентный overlay загрузки.
- Canvas + фон + текст прогресса на объекте `VanguardApp`, `raycastTarget =
false`, поверх сцены; API `Show(float progress)`, `Hide()`.
- Не использовать `UiKit.CreateCanvas` для сцены: Canvas должен переживать
`LoadSceneMode.Single`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C2. `MODIFY` `Runtime/App/SceneFlow.cs`** — переход с прогрессом и fade.
- Показывать overlay до `LoadSceneAsync`, обновлять прогресс по
`operation.progress`, `allowSceneActivation = false` до готовности, затем
активировать сцену и скрывать overlay после первого кадра.
- Защита от повторного вызова во время перехода (idempotent `LoadAsync`).
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1.
- [ ] **C3. `MODIFY` `Runtime/App/VanguardApp.cs` + `Scenes/Boot.unity`** — Boot
как загрузочная сцена.
- `Boot` показывает overlay и грузит `Login` после инициализации сервисов;
прогрев не дублируется в `Start`, если сцена уже целевая.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C2.
- [ ] **C4. `MODIFY` `Board3D/View/GameBoard3D.cs`** — стрелки без LINQ и
покадровых списков.
- Переиспользуемый буфер пар boost-стрелок, индексированный поиск игрока
вместо `FirstOrDefault`, один проход по `_cards` для якорей.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C5. `MODIFY` `Animation/ArrowDirector.cs` + `View/AttackArrowView.cs`** —
одно обновление геометрии за кадр.
- Убрать двойной путь `Tick` + `UpdateAttackArrowPoints`; обновлять точки
один раз; убрать массив `new[] { _shaft, _head }` в `ApplyColor`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C4.
- [ ] **C6. `MODIFY` `Board3D/View/GameBoard3D.cs`** — `UpdateHandView` по
dirty-флагу.
- Пересчитывать позы только при изменении состава руки, камеры, hover,
staged-списка; оставить ровно один вызов за кадр.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C7. `MODIFY` `UI/BattleView.cs` — `UpdateClock`.** Обновлять текст
таймера и банка только при изменении отображаемого значения.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C8. `MODIFY` `UI/Localization.cs` — `LocalizedText`.** Кешировать
результат перевода до смены языка или ключа, не вызывать `Loc.Translate`
каждый кадр.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C9. `MODIFY` `Battle/InputLockService.cs` — `Describe`.** Не собирать
строку/LINQ в кадровом пути: кешировать описание при изменении набора причин.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C10. `TEST` регрессии блока.**
- EditMode: overlay `Show/Hide` идемпотентны; `UpdateHandView` не меняет позы
при неизменных входах (seam-счётчик), hover/добор/муллиган по-прежнему
сдвигают руку; `ApplyColor` не создаёт массив (статическая проверка или
отсутствие регрессии поведения).
- Существующие `Hand_*`, `CardMoveAnimator_*`, `GameBoard3DTests` — без
изменений ожиданий.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1–C9.
- [ ] **C11. `VERIFY` автотесты, smoke, сборка.**
- EditMode в копии без `Library`; `SceneSmoke.Run`; `SceneSmoke.RunPlaymat`;
standalone-сборка.
- Profiler: Allocation Call Stacks в бою без движения — 0 B/кадр.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C10.
- [ ] **C12. `VERIFY` ручная проверка пользователем.**
- Переход `Boot → Login → Lobby → Battle` без вспышки; видимый прогресс;
стрелки атаки/буста без визуальных изменений; hover, добор, муллиган,
reflow руки; таймер и подписи.
- Реализация: не применима. Проверка: остаётся на пользователе.
- Зависит: C11.
#### D. Последовательность выполнения
1. C1 → C2 → C3: загрузочный overlay и Boot. Проверить переходы смоуком.
2. C4 → C5: стрелки; визуально сравнить до/после.
3. C6: рука по dirty-флагу; прогнать `Hand_*`-тесты.
4. C7 → C8 → C9: строки и локализация.
5. C10 → C12: тесты, проверки, ручная приёмка.
#### E. Проверки
- Клиентские EditMode в копии проекта без `Library` (команда в
`docs/SESSION-STATE.md`).
- `SceneSmoke.Run` и `SceneSmoke.RunPlaymat`.
- Standalone-сборка на копии: переходы и overlay.
- Profiler: Allocation Call Stacks до/после для боя без движения.
- `git diff --check`; отсутствие изменений в `Vanguard.Rules`, `Vanguard.Shared`,
`Vanguard.Server`, `content/`.
- Ручная приёмка — пользователем.
#### F. Риски и неизвестные вопросы
- **Overlay на персистентном Canvas:** может перехватывать клики или висеть
после загрузки — `raycastTarget = false`, скрытие в `finally` и по таймауту.
- **Смоук-таймауты:** `SceneSmoke` ждёт 6 с; если Boot начнёт грузить Login
позже, таймаут придётся увеличить или ждать по состоянию.
- **Dirty-флаг руки:** легко сломать hover, staged-reflow и муллиган; менять
только после зелёных `Hand_*`-тестов.
- **Якоря стрелок:** карты двигаются каждый кадр, поэтому кешировать можно
буферы и результаты поиска карт, но не позиции.
- **Порядок вызовов:** у `BattleSceneController.Update` и `GameBoard3D.Update`
нет `DefaultExecutionOrder`; правки не должны завязываться на порядок.
- **HUD TASK-2026-003** ещё не принят вручную: в `BattleView` менять только
`UpdateClock`, не разметку.
- Открытых вопросов, блокирующих старт, нет.
#### G. Результат
- **Реализовано:** —
- **Проверки:** —
- **Не завершено/заблокировано:** —
- **Skill обновлён:** —
- **Git:** —
---
### TASK-2026-020 — парсер карт и артов с официального сайта
- **Статус:** `[x]`
- **Создано:** 2026-10-30
- **Анализ:** DeepSeek
- **Реализация:** DeepSeek
- **Проверка:** побайтовое совпадение с текущими YAML (0 diff), 94/124, скачивание арта
- **Затронутые системы:** `tools/import_official.py` (новый), `README.md`;
`content/sets/*.yaml` и `CardArt` не изменялись.
#### A. Цель и требования
Один инструмент, который берёт с `en.cf-vanguard.com` и параметры карт, и
картинки, и пишет тот же YAML-формат, что и текущий импорт. Причины:
- сейчас параметры идут с сайта (`view=text`), а триггеры и редкости — из
`content/cards-mvp.md` (выгрузка из вики), арты — с Fandom;
- официальная карточка `/cardlist/?cardno=…` содержит и `rarity`, и `gift`
(тип триггера), поэтому вики больше не нужна;
- картинки официального сайта лежат рядом с данными:
`/wordpress/wp-content/images/cardlist/{set}/{CODE}.png`.
Ограничения: только стандартная библиотека Python; не менять схему YAML и
`CardEntry`/`PrintingEntry`; не терять ручные поля; не создавать в `Resources`
два ассета с одним именем.
#### B. Текущее состояние и что выяснено
- `tools/import_cards.py`: список `/cardlist/cardsearch/?expansion=N&view=text`,
регэксп по `<li>`, триггеры/редкости из `cards-mvp.md`, запись
`set/cards/printings`, SP-принты `BT01/S01EN…S12EN` помечаются
`isParallel` + `basePrintingId`.
- `tools/import_card_art.py`: Fandom-галереи, коды только `TD\d{2}`.
- Сайт: `expansion=5` → BT01 (92 принта, 4 страницы), `3` → TD01 (16),
`4` → TD02 (16); всего 94 определения и 124 принта — совпадает с README.
- Карточка содержит `type`, `group` (клан), `race`, `nation`, `grade`,
`power`, `critical`, `shield`, `skill`, `gift` (например
`Critical Trigger +5000`), `effect`, `flavor`, `rarity`, `number`.
- Список сетов с id — `/cardlist/?change_ex` (47 сетов, `BT01=5`, `TD01=3`,
`TD02=4`), можно не хардкодить id.
- Сайт отдаёт 404 на запросы без браузерного `User-Agent` — нужен UA.
#### C. Чек-лист изменений
- [x] **C1. `CREATE` `tools/import_official.py`** — единый парсер.
- `discover_sets()` по `/cardlist/?change_ex`; `--list-sets`.
- `list_codes()` со всех страниц (`max_page` из HTML) и сверка с «N Results».
- `parse_card()` по карточке: тип, клан, grade/power/critical/shield,
триггер из `gift`, редкость, текст, флаги `sentinel`/`restraint`.
- `write_set()` — тот же формат и порядок полей, что у старого импорта.
- Реализация: выполнена. Проверка: см. C5.
- [x] **C2. `CREATE` скачивание артов** — `{imageKey}.png` в `CardArt`.
- Пропуск, если файл с любым расширением уже есть (иначе Unity увидит два
ассета с одним именем); `--force-art` удаляет старый файл и его `.meta`.
- Реализация: выполнена. Проверка: TD01, 16/16 файлов, ~350–430 КиБ.
- [x] **C3. `MODIFY` дедупликация определений между сетами.**
- При прогоне одного сета читаются `nameId` из остальных YAML папки, чтобы
не дублировать карты, определённые в BT01.
- Реализация: выполнена. Проверка: BT01 80 + TD01 7 + TD02 7 = 94.
- [x] **C4. `MODIFY` сохранение ручных полей.**
- `nameRu`, `textRu`, `snipeBackRow`, `boost` переносятся из существующего
файла по `nameId` (на сайте их нет; иначе терялся бы Tejas).
- Реализация: выполнена. Проверка: `snipeBackRow` у Tejas сохраняется.
- [x] **C5. `VERIFY` идемпотентность.**
- Прогон `--sets BT01,TD01,TD02` в копию `content/sets`: `Compare-Object`
по каждому файлу — **0 строк различий**, размеры совпали
(37116 / 3771 / 3989 байт).
- Реализация: не применима. Проверка: выполнена.
- [x] **C6. `MODIFY` `README.md`** — команда нового импорта, режимы,
предупреждение про заглушку `python` в Microsoft Store (`py`).
- Реализация: выполнена. Проверка: прочитано.
#### D. Последовательность выполнения
1. C1: разбор списка и карточки, запись YAML.
2. C2: арты и защита от дублей в `Resources`.
3. C3–C4: дедупликация и ручные поля.
4. C5: прогон в копию и сверка.
5. C6: README.
#### E. Проверки
- `py tools/import_official.py --list-sets`.
- `py tools/import_official.py --sets TD01 --limit 4 --out <tmp> --art-dir <tmp>`.
- Полный прогон в копию `content/sets` + `Compare-Object` с репозиторием.
- `dotnet run --project server/src/Vanguard.Cli -- validate` — **не запускался**
(нужен `ssh home`/Docker); косвенно покрыт побайтовым совпадением YAML.
- Скачивание арта проверено на TD01 (16 файлов) и на `--force-art`.
#### F. Риски и открытые вопросы
- Разметка сайта нестабильна: любой редизайн ломает регэкспы. Симптом —
«не найдено имя карты» или расхождение с «N Results».
- Сайт отвечает 404 без браузерного `User-Agent` — UA обязателен, `curl`
без заголовка вводит в заблуждение.
- Полный прогон — ~2–6 минут на 124 карточки (по одному запросу на карту,
пауза `--delay`). Для нескольких сетов сразу это нормально, для правки
одной карты — избыточно.
- Официальные арты — PNG ~350–430 КиБ; текущие в репозитории — JPG. Массовая
замена не делалась: `--force-art` меняет расширение и удаляет `.meta`.
- `python` в Microsoft Store — заглушка, нужен `py` (отражено в README).
- Открытый вопрос: переносить ли `skill` (`Boost`/`Twin Drive!!`) в поле
`boost` — сейчас не переносится, чтобы не менять игровые данные.
#### G. Результат
- **Реализовано:** C1–C6.
- **Проверки:** полный прогон в копию — 0 различий с `content/sets`;
94 определения / 124 принта; арт TD01 16/16; `--force-art` заменяет
`.jpg` на `.png` без дублей.
- **Не завершено:** `validate` на `ssh home`.
- **Skill обновлён:** не требуется.
- **Git:** `0620ec5` (парсер, README, план), арты — `f2ae6b9`.
---
### TASK-2026-021 — Login по референсу: 2D-арт и неоновый UI
- **Статус:** `[!]`
- **Создано:** 2026-10-31
- **Анализ:** DeepSeek (по референсу в чате от 2026-10-31)
- **Реализация:** DeepSeek: C1–C9 выполнены
- **Проверка:** EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная приёмка — за пользователем
- **Затронутые системы:** `LoginView`, `LoginSceneController`,
`SceneEnvironment`, `UiKit`, новый `NeonUi`, новый `LoginMenu`,
`Localization`, `Resources/Login/*`, `SceneSmoke`, EditMode-тесты.
#### A. Цель и требования
Привести экран `Login` к референсу: полноэкранный 2D key art, поверх —
центрированная неоновая панель с двумя полями и кнопкой «Войти».
Состав экрана (сверху вниз, координаты для 1920×1080):
1. **Фон** — один арт на весь экран, cover (без полос по краям), лёгкое
затемнение под панелью для читаемости текста.
2. **Логотип** — по центру, ширина ≈ 520 px, верх ≈ 140 px. В MVP входит в
состав арта; отдельным слоем — только если дадут прозрачный PNG.
3. **Панель** — 560×420, центр по X, центр по Y ≈ 600 (чуть ниже середины):
- радиус 24, заливка `rgba(7,17,43,0.82)`;
- рамка 2 px `#2E7DFF`, внешнее свечение ≈ 24 px `rgba(46,125,255,0.35)`;
- тонкая внутренняя рамка `rgba(150,200,255,0.18)`.
4. **Поле ввода** — 480×64, радиус 10, заливка `rgba(4,12,30,0.9)`, рамка
`rgba(46,125,255,0.45)`; слева иконка 22×22 (16 px от края), текст 18 px,
placeholder `rgba(130,150,190,0.9)`; фокус — рамка `#6CC0FF` + свечение.
- «Логин» — иконка пользователя;
- «Пароль» — иконка замка + глаз справа (показать/скрыть).
5. **«Забыли пароль?»** — 15 px, `#5FB0FF`, справа под полем пароля.
6. **Кнопка «Войти»** — 480×72, срезанные углы 14 px, градиент
`#1B5FC9 → #2E8BFF`, рамка 1.5 px `#7CC4FF`, свечение; текст 22 px bold,
белый. Hover +10 % яркости, pressed −8 %, disabled 45 % + «Подключение…».
7. **«Создать аккаунт»** — вторичная ссылка 15 px `#7FA8E0` по центру под
кнопкой (решение 2026-10-31: регистрация остаётся доступной).
8. **Статус** — 14 px под ссылками, автоскрытие через 6 с; ошибка `#FF7A6B`,
успех `#6BE08A`.
9. **Гамбургер** — 56×56, правый верхний угол 28/28, радиус 12, рамка
`#2E7DFF`, три линии 22×2. Открывает меню: `RU/EN`, версия клиента,
**адрес сервера** (dev-поле), «Закрыть». Выхода нет — сессии ещё нет.
10. **Тэглайн** — левый нижний угол 40/36: щит 44×44 + две строки
`FIGHT TOGETHER.` / `BUILD YOUR FUTURE.`, 11 px, трекинг 2,
`rgba(150,195,255,0.65)`.
11. **Копирайт** — правый нижний угол, 12 px `rgba(130,165,215,0.5)`:
`© Cardfight!! Vanguard Fan Project`.
Ограничения:
- без новых пакетов и без Addressables (арт из `Resources`, как арт карт);
- auth-контракты не меняются; серверного сброса пароля нет;
- правила, сервер, протокол и контент не трогаются;
- имя `LoginScreen` сохраняется (на него опирается `SceneSmoke`);
- 3D-окружение в `Login` не строится — это исключение из «3D в каждой
сцене», зафиксированное в `CLIENT-SCENES-3D-ANIMATIONS.md` (8.7).
Критерии готовности:
- композиция и цвета совпадают с референсом на 1920×1080;
- на 16:10 и 21:9 фон заполняет экран, панель остаётся по центру;
- вход, регистрация, ошибки и busy-состояние работают как раньше;
- RU/EN переключается из меню, строки локализованы;
- `SceneSmoke.Run` проходит на сцене `Login` без 3D-окружения.
#### B. Текущее состояние
- `LoginView` (`Runtime/UI/LoginView.cs`): панель `LoginScreen` + карточка
620×820 с eyebrow/title/subtitle, **полем адреса сервера**, полем имени,
паролем, кнопкой «ВОЙТИ», кнопкой «СОЗДАТЬ АККАУНТ», статусом, футером с
версией/стендом и кнопкой языка в правом верхнем углу.
- `LoginSceneController` вызывает `SceneEnvironment.Build(SceneTheme.Login)`:
камера, свет, пол, 14 кубов (стол, «карты», неон). 3D-стол виден по краям
панели.
- `UiKit` умеет только прямоугольные `Image` с плоским цветом: скруглений,
рамок, свечения и 9-slice нет; `CreateInput` — стандартный uGUI `InputField`.
- Шрифт — `LegacyRuntime.ttf` (кириллица есть, геометрия нейтральная).
- `SceneSmoke` (`Editor/SceneSmoke.cs:116-123`) требует наличия объекта
`Vanguard 3D Environment` в активной сцене; после отказа от 3D в Login
проверка упадёт.
- Сервер: endpoint сброса пароля отсутствует (`forgot`/`reset` не находятся),
поэтому «Забыли пароль?» — только заглушка.
Что сохранить:
- `AuthService`, `AppConfig`, обработку `ApiException` и формат ошибок;
- имя `LoginScreen` и вызов `_onAuthenticated` после успеха;
- `UiKit.CreateCanvas` и текущий CanvasScaler (1920×1080, match 0.5);
- кнопку языка как функцию (переезжает в меню, но не исчезает).
#### C. Чек-лист изменений
- [x] **C1. `CREATE` `Runtime/UI/NeonUi.cs`** — 9-slice атомы интерфейса.
- Спрайты панели, поля, кнопки с фаской, свечения генерируются в рантайме
(по образцу `BoardTextures`) либо берутся из одного PNG-атласа; кеш
статический.
- API: `Body(...)`, `Bevel(...)`, `Glow(...)`, `Circle/Ring/HorizontalBar`,
`AddImage/AddAnchored`; свечение — отдельный спрайт позади тела.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: нет.
- [x] **C2. `CREATE` `Runtime/UI/LoginBackdrop.cs`** — фон экрана.
- `Resources/Login/login_bg` → `Texture2D` → `Sprite.Create` (как
`CardImageService`), cover-масштаб через `AspectRatioFitter`, поверх —
затемняющий слой под панелью.
- Если файла нет — процедурный градиент-заглушка, без падения.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: арт-файл от пользователя.
- [x] **C3. `MODIFY` `Runtime/UI/LoginView.cs`** — новая композиция.
- Лого/тэглайн/копирайт, панель 560×420, два поля с иконками, «Забыли
пароль?», кнопка «Войти», ссылка «Создать аккаунт», статус.
- Убрать с панели поле адреса сервера, eyebrow/title/subtitle и футер с
версией; адрес и версия уезжают в меню.
- Tab / Shift+Tab между полями (`Runtime/UI/TabFocus.cs`); Enter отправляет
форму (`TabFocus.EnterPressed`, обработчик в `TabFocusDriver`); в открытом
меню Enter форму не отправляет.
- Реализация: выполнена. Проверка: EditMode 60/60, визуальный прогон.
- Зависит: C1, C2.
- [x] **C4. `CREATE` `Runtime/UI/LoginMenu.cs`** — гамбургер и панель меню.
- Кнопка 56×56 в правом верхнем углу, три линии.
- Меню 340 px: переключатель `RU/EN`, версия, поле адреса сервера
(сохраняет в `AppConfig`/`PlayerPrefs`), «Закрыть».
- Закрытие по Esc (`LoginMenu.IsOpen` + `Close`), по кнопке и клику вне
панели; не перехватывает ввод у формы, когда закрыто.
- Реализация: выполнена. Проверка: EditMode 60/60, визуальный прогон.
- Зависит: C1.
- [x] **C5. `MODIFY` `Runtime/App/LoginSceneController.cs` +
`Runtime/App/SceneEnvironment.cs`.**
- `Login` больше не вызывает `SceneEnvironment.Build`; камера — заливка
цветом фона, без пола/кубов/точечных источников.
- Ветка `SceneTheme.Login` в окружении остаётся, но не используется (удалять
только вместе с остальными темами).
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: C2.
- [x] **C6. `MODIFY` `Editor/SceneSmoke.cs`** — проверка Login без 3D.
- Для `Login` требовать `LoginScreen` и фон (`LoginBackdrop`/`LoginBackground`),
`Vanguard 3D Environment` — только для `Lobby`.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: C3.
- [x] **C7. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- «Логин / Email», «Пароль», «Забыли пароль?», «Войти», «Создать аккаунт»,
«Подключение…», пункты меню, заглушка сброса пароля.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: C3.
- [x] **C8. `CREATE` ассеты** — `Resources/Login/`.
- `login_bg.png|jpg` (1920×1080+, без текста и панели), при наличии —
логотип и атлас иконок (пользователь/замок/глаз/меню/щит).
- Импорт: `Texture2D` (не Sprite), max size 2048, `sRGB`; отдельный
`.meta` в репозитории.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: файлы от пользователя.
- [x] **C9. `TEST` EditMode.**
- Панель: два поля, кнопка, ссылки; поля без адреса сервера.
- Busy/ошибка/успех меняют статус и блокируют кнопки.
- Enter отправляет форму; меню открывается/закрывается, язык переключается.
- `SceneSmoke.Run` — Login без `Vanguard 3D Environment`.
- Реализация: выполнена. Проверка: компиляция, EditMode 58/58, визуальный прогон.
- Зависит: C1–C8.
- [ ] **C10. `VERIFY` сборка и ручная приёмка.**
- Standalone на копии проекта; проверка 1920×1080, 16:10, 21:9.
- Ручная сверка с референсом — пользователем.
- Реализация: выполнена. Проверка: standalone собран в копии без `Library`,
EditMode 60/60, `SCENE_SMOKE success=True`; визуальная приёмка — за
пользователем.
- Зависит: C9.
#### D. Последовательность выполнения
1. C1: атомы (панель, поле, кнопка, свечение) — без них остальное не собрать.
2. C2: фон и затемнение; проверить cover на 16:9/21:9.
3. C3: новая композиция формы, сохранение текущей логики входа.
4. C4: меню (язык, версия, адрес сервера).
5. C5–C6: убрать 3D из Login, поправить смоук.
6. C7–C8: локализация и ассеты.
7. C9–C10: тесты, сборка, сверка с референсом.
#### E. Проверки
- EditMode в копии проекта без `Library` (команда в `docs/SESSION-STATE.md`).
- `SceneSmoke.Run` — `SCENE_SMOKE success=True active=Login`.
- Standalone-сборка и запуск на 1920×1080; при возможности 21:9.
- Проверка входа: успех, неверный пароль, нет сети (сообщения прежние).
- `git diff --check`; сцены/префабы не менялись без отдельного пункта.
#### F. Риски и неизвестные вопросы
- **Арт-файла ещё нет в проекте** — в чате только превью 1680×945. До
появления файла C2 работает на градиенте; финальный вид не принять.
- **Логотип** — часть арта; отдельный слой потребует прозрачного PNG и
аккуратного позиционирования, иначе логотип «поедет» на другом AR.
- **Свечение в uGUI** — bloom пост-обработки не действует на Screen Space
Overlay: glow только запечённый в спрайт (несколько слоёв, без шейдеров).
- **Шрифт**: `LegacyRuntime` даёт нейтральную геометрию; для совпадения с
референсом нужен TTF с кириллицей (Inter/Exo 2) — ждём решение.
- **Отказ от 3D в Login** ломает `SceneSmoke` и может задеть EditMode-тесты,
которые искали окружение; правится в C6/C9.
- **Адрес сервера без UI** — смена стенда только через меню (C4) или
`PlayerPrefs`; в README нужно оставить инструкцию.
- **Сброс пароля** — заглушка; если появится API, экран расширяется отдельной
задачей.
- Открытых вопросов, блокирующих C1–C7, нет; C2/C8 ждут арт.
#### G. Результат
- **Реализовано:** C1–C9.
- **Проверки:** компиляция без ошибок; EditMode **60/60** (в копии без
`Library`, `+Login_Hotkeys_Dispatch_Submit_And_Cancel`); `SceneSmoke.Run` —
`success=True active=Login env=False backdrop=True login=True`; standalone
собран в копии; экран снят на 1920×1080 в прошлом проходе; меню
открывается и закрывается.
- **Не завершено:**
- C10: ручная сверка с референсом, поведение Enter/Esc и меню на
standalone — за пользователем.
- **Замечания:** панель и поля полупрозрачные (0.94 / 0.97) — сквозь них
видно арт; если нужно плотнее, поднять альфу. Иконки собраны из примитивов
(`Compose`), шрифт — `LegacyRuntime`.
- **Skill обновлён:** не требуется.
- **Git:** не коммитилось.
---
### TASK-2026-022 — Lobby по референсу: дашборд, заглушки и сцена Community
- **Статус:** `[!]`
- **Создано:** 2026-10-31
- **Анализ:** DeepSeek выполнен (референс Lobby от 2026-10-31, решения ниже)
- **Реализация:** DeepSeek: C1–C11 выполнены
- **Проверка:** EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная приёмка — за пользователем
- **Затронутые системы:** `LobbySceneController`, новый `LobbyView` (вместо
`MetaView`), новый `LobbyBackdrop`, новый `BattleEntryOverlay`, меню,
новая сцена `Community` + `CommunitySceneController`/`CommunityView`,
`SceneFlow`, `SceneBuilder`, `SceneSmoke`, `Localization`, EditMode-тесты.
Сервер, правила и протокол не меняются.
#### A. Цель и требования
Привести `Lobby` к композиции референса, но оставить только реально
поддержанные функции; отсутствующие в сервере блоки — честные заглушки
«скоро» без фейковых цифр.
Состав экрана (1920×1080):
1. **Верхняя панель (≈96 px):** аватар (инициалы, ассетов нет), ник,
`RankedMmr`, бейдж сезона-заглушки; справа — «Рейтинг», «Сообщество»,
«Меню».
2. **Левая колонка (≈560 px):** три крупные плашки с иконкой, заголовком и
подписью — `BATTLE`, `DECK BUILDER`, `SHOP`.
3. **Блок миссий** под плашками: заголовок «Ежедневные / еженедельные
задания» и 2–4 строки-заглушки с замком и «скоро». Прогресс и таймер
обновления не рисуются: серверной модели нет.
4. **Балансы** внизу слева: Gold, Dust, Tokens; клик открывает магазин.
5. **Центр:** большая панель `Chat` — заглушка «скоро» с полем ввода в
состоянии disabled.
6. **Справа:** панель `Friends Online` — заглушка: счётчик без данных не
показывать, 3–5 строк-заглушек, «скоро».
7. **Фон:** процедурный тёмный неоновый градиент с сеткой и виньеткой в
стиле Login. 3D-окружение в `Lobby` не строится.
Поведение:
- **BATTLE** открывает модальный оверлей: список колод, режим
`Рейтинговый` / `С ботом`, кнопка «В бой». Без колод или без валидной
колоды кнопка disabled и есть переход «Собрать колоду» в `DeckBuilder`.
Рейтинговый режим уходит в существующую очередь `classic-ranked`
(`Flow.OpenLobbyWithQueueAsync`), «С ботом» — `StartPracticeAsync` и
`LoadBattleAsync`. Серверные контракты не меняются.
- **Rating** — оверлей-заглушка «Рейтинг — скоро». Лидерборд (top-100 по
`RankedMmr`) — отдельная задача с серверным endpoint.
- **Community** — отдельная сцена-каркас с вкладками «Гайды / Колоды /
Тикеты», внутри каждой — заглушка. Реальные гайды, публичные колоды и
отправка тикетов — отдельные задачи с сервером.
- **Меню** — гамбургер: язык RU/EN, версия, адрес сервера, «Выйти»,
«Закрыть»; Esc и клик вне панели закрывают.
- **Онбординг** в Lobby не показывается: ни статуса, ни кнопки. Выбор
стартовой триал-деки остаётся отдельным экраном до Lobby; когда появится
обучение, он будет заменён им.
Ограничения:
- без новых пакетов и без Addressables;
- сервер, правила, протокол и контент не меняются;
- `QueueView`, `StoreView`, `HistoryView`, `DeckBuilder` и `Battle`
сохраняют текущее поведение;
- `Login` не должен измениться визуально при выносе общего меню;
- геймплей и визуальную приёмку проверяет пользователь.
Критерии готовности:
- композиция и стиль совпадают с референсом на 1920×1080, на 16:10 и 21:9
фон заполняет экран, панели не разъезжаются;
- все несерверные блоки визуально помечены «скоро» и не кликабельны;
- BATTLE доводит до очереди или практики без захода в редактор колоды;
- Community открывается как отдельная сцена, вкладки переключаются;
- в Lobby нет упоминаний онбординга;
- существующие EditMode-тесты и смоуки обновлены и проходят.
#### B. Текущее состояние
- `LobbySceneController` строит 3D-окружение (`SceneEnvironment.Build`),
создаёт `MetaView`, `OnboardingView`, `QueueView`, `StoreView`,
`HistoryView`; при `!OnboardingCompleted` показывает онбординг вместо
лобби; «В бой» ведёт в `DeckBuilder`.
- `MetaView` (`Runtime/UI/MetaView.cs`): топбар `VANGUARD` + ник + язык +
«Выйти», 5 статистик (Gold/Dust/Tokens/Ranked/Casual), 4 кнопки
(Коллекция/Магазин/История/В бой), запись, статус, кнопка выбора
триал-деки. Полностью заменяется.
- `QueueView` — только `classic-ranked`; практика запускается из
`DeckBuilderView` («С ботом»).
- Сервер: `UserEntity` хранит `UserName`, `RankedMmr`, `Matches`, `Wins`,
но endpoint лидерборда, миссий, чата, друзей, сезонов и тикетов
отсутствует.
- Аватаров в проекте нет; ассетов фона Lobby нет.
- `SceneSmoke` требует `Vanguard 3D Environment` для `Lobby`; тесты
`ClientSmokeTests` создают `MetaView` напрямую и проверяют `MetaScreen`,
`OnboardingScreen`.
- Общие визуальные атомы — `NeonUi` из `TASK-2026-021`.
Что сохранить:
- `AuthService`, `MetaService`, `PlayerState`, обработку 401 → Login;
- существующие экраны магазина, истории, очереди, реплеев;
- `SceneFlow` как единственный переход между сценами;
- `UiKit`/`NeonUi`, CanvasScaler 1920×1080;
- текущий визуальный язык Login.
#### C. Чек-лист изменений
- [x] **C1. `CREATE` `Runtime/UI/LobbyBackdrop.cs`** — процедурный фон.
- Вертикальный градиент, сетка, центральное свечение и виньетка; спрайт
кешируется статически, создаётся один раз.
- Cover-масштабирование на 16:9/16:10/21:9 без полос; при появлении арта
заменяется одним изображением без правки раскладки.
- Объект называется `LobbyBackdrop` для смоука.
- Реализация: выполнена. Проверка: EditMode 62/62, `SCENE_SMOKE`.
- Зависит: нет.
- [x] **C2. `CREATE` `Runtime/UI/LobbyView.cs`** — новый дашборд вместо
`MetaView`.
- Топбар: аватар-инициалы, ник, MMR, сезон-заглушка, «Рейтинг»,
«Сообщество», «Меню».
- Левая колонка: BATTLE / DECK BUILDER / SHOP с иконками и подписями;
блок миссий-заглушка; балансы Gold/Dust/Tokens с переходом в магазин.
- Центр: `Chat`-заглушка; справа: `Friends Online`-заглушка.
- Корневой объект `LobbyScreen`; тексты через `Loc`.
- Реализация: выполнена. Проверка: `Lobby_Shows_Actions_Stubs_And_Balances_Without_Onboarding`.
- Зависит: C1, C9.
- [x] **C3. `MODIFY` `Runtime/App/LobbySceneController.cs`** — сборка Lobby
без 3D и без `MetaView`.
- Убран `SceneEnvironment.Build(SceneTheme.Lobby)`; создаются
`LobbyBackdrop`, `LobbyView`, `BattleEntryOverlay`, рейтинг-заглушка и
`HotkeysDriver` (Esc).
- Убраны статус и кнопка онбординга из лобби; при `!OnboardingCompleted`
по-прежнему открывается отдельный экран выбора триал-деки.
- Подключены BATTLE-оверлей, рейтинг-заглушка, Community → `SceneFlow`.
- Реализация: выполнена. Проверка: EditMode 62/62, `SCENE_SMOKE`.
- Зависит: C2, C4, C5, C6.
- [x] **C4. `CREATE` `Runtime/UI/BattleEntryOverlay.cs`** — выбор колоды и
режима.
- Загрузка колод `MetaService.GetDecksAsync`, выбор карточкой, режимы
«Рейтинговый» / «С ботом», кнопка «В бой», «Собрать колоду».
- Валидация через `ValidateDeckAsync` перед запуском; ошибки — в панели.
- Ranked закрывает оверлей и открывает существующую очередь
(`LobbySceneController.OnRanked` → `QueueView.Show`); практика —
`StartPracticeAsync` + `Flow.LoadBattleAsync`.
- Закрытие: крестик и Esc (фон блокируется).
- Реализация: выполнена. Проверка: компиляция, EditMode 62/62; сетевой
сценарий — ручная проверка.
- Зависит: C2.
- [x] **C5. `CREATE` сцена `Community`.**
- `Scenes/Community.unity`, `CommunitySceneController`,
`Runtime/UI/CommunityView` с вкладками «Гайды / Колоды / Тикеты» и
заглушками; кнопка «Назад» в Lobby.
- `SceneFlow`: константа `Community` и `LoadCommunityAsync`;
`SceneBuilder`: создание сцены и добавление в Build Settings после
`Lobby`.
- Серверных вызовов нет.
- Реализация: выполнена. Проверка: `Community_Tabs_Switch_Content`,
сцена собрана (6 сцен в Build Settings).
- Зависит: C2.
- [x] **C6. `MODIFY` меню** — общий гамбургер для Login и Lobby.
- `LoginMenu` вынесен в общий `Runtime/UI/HamburgerMenu.cs` (язык,
версия, адрес сервера, закрыть; для Lobby дополнительно «История
матчей» и «Выйти»).
- `Login` использует его без визуальных изменений; в Lobby «Выйти»
переехал из топбара в меню.
- Esc закрывает меню через `HotkeysDriver`; клик вне панели работает.
- Реализация: выполнена. Проверка: EditMode 62/62, Login не изменён.
- Зависит: C2.
- [x] **C7. `DELETE`/`MODIFY` `Runtime/UI/MetaView.cs`** — удалить после
переноса функций в `LobbyView`.
- Обновление профиля и обработка 401 → Logout перенесены в `LobbyView`;
`MetaView` удалён вместе с `.meta`.
- Реализация: выполнена. Проверка: EditMode 62/62, ссылок на `MetaView`
не осталось.
- Зависит: C2, C3.
- [x] **C8. `MODIFY` `Editor/SceneSmoke.cs` + `Editor/SceneBuilder.cs`.**
- `Lobby` проверяется по `LobbyBackdrop` и `LobbyScreen`; 3D-окружение
для неё больше не требуется.
- `SceneBuilder` создаёт `Community` и добавляет её в Build Settings после
`Lobby`.
- Реализация: выполнена. Проверка: `SCENE_SMOKE success=True
active=Login backdrop=True`; сцена Community собрана.
- Зависит: C2, C5.
- [x] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- Рейтинг, Сообщество, подписи плашек, задания, чат, друзья, «скоро»,
режимы, ошибки проверки колоды, вкладки Community.
- Реализация: выполнена. Проверка: дубликатов ключей нет, EditMode 62/62.
- Зависит: C2, C4, C5, C6.
- [x] **C10. `TEST` EditMode.**
- `LobbyView`: три плашки, балансы, «скоро», отсутствие онбординга.
- `CommunityView`: переключение вкладок.
- Существующий `Client_Views_Build` переведён с `MetaView` на `LobbyView`;
проверка `LobbyScreen`.
- Реализация: выполнена. Проверка: EditMode **62/62** в копии без
`Library`.
- Зависит: C1–C9.
- [x] **C11. `VERIFY` EditMode, smoke, standalone.**
- Копия проекта без `Library`; `SceneSmoke.Run`, `RunPlaymat`;
standalone-сборка и запуск.
- Реализация: выполнена. Проверка: EditMode 62/62,
`SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True`,
`WINDOWS_BUILD result=Succeeded errors=0`; визуальная сверка с
референсом — за пользователем.
- Зависит: C10.
#### D. Последовательность выполнения
1. C1 → C2: фон и раскладка дашборда; проверить 16:9/16:10/21:9.
2. C6: общее меню; убедиться, что Login не изменился.
3. C3 → C4: контроллер Lobby, убрать 3D/онбординг, BATTLE-оверлей.
4. C5: сцена Community и переход.
5. C7 → C9: удаление `MetaView`, смоук, локализация.
6. C10 → C11: тесты, smoke, standalone, ручная приёмка.
#### E. Проверки
- Клиентские EditMode в копии без `Library`.
- `SceneSmoke.Run` (Login/Lobby), проверка `Community`.
- `SceneSmoke.RunPlaymat` — поле не должно пострадать.
- Standalone-сборка и запуск на 1920×1080; при возможности 16:10, 21:9.
- `git diff --check`; отсутствие изменений в `server/`, `content/`,
`Vanguard.Rules`/`Shared`.
- Ручная проверка: BATTLE → очередь/бот, магазин, история, коллекция,
Community, меню, Esc, отсутствие онбординга в Lobby.
#### F. Решения и риски
**Зафиксировано пользователем (2026-10-31):**
1. Блоки без сервера (миссии, чат, друзья, сезон, рейтинг) — заглушки
«скоро».
2. Центральный 3D-блок убрать, область отдать большой панели чата.
3. Rating — в будущем топ игроков по рейтингу; сейчас заглушка.
4. Community — отдельная сцена: гайды, колоды, тикеты; первая версия —
каркас с заглушками.
5. BATTLE — оверлей выбора колоды и режима.
6. Онбординг в Lobby не показывать; выбор триал-деки остаётся отдельным
экраном до появления обучения.
7. Фон Lobby — процедурный, арт позже.
**Риски:**
- **Пустая область чата:** большая заглушка может выглядеть как дыра; нужен
аккуратный placeholder (иконка, пояснение, «скоро»), иначе экран
проигрывает референсу.
- **Процедурный фон** без арта будет плоским; заменить на key art, когда
появится, без правки раскладки.
- **Аватаров нет** — инициалы; при появлении загрузки аватаров заменить.
- **Удаление `MetaView`** затрагивает `ClientSmokeTests` и `SceneSmoke`;
менять их в том же блоке.
- **Новая сцена** требует `SceneBuilder` и Build Settings; иначе
`LoadSceneAsync` вернёт ошибку.
- **Онбординг новых аккаунтов:** выбор триал-деки нельзя потерять, иначе
новый игрок останется без колоды.
- **Общее меню** не должно менять поведение Login (Tab/Enter/Esc).
- **BATTLE-оверлей** не должен блокировать бой из-за пустой коллекции:
причина disabled должна быть видимой.
**Открытых вопросов, блокирующих старт, нет.**
#### G. Результат
- **Реализовано:** C1–C11. Lobby — 2D-дашборд: процедурный фон, топбар
(инициалы, ник, MMR, сезон-заглушка), три плашки, миссии-заглушка,
балансы, чат- и друзья-заглушки, меню с историей и выходом. BATTLE —
оверлей выбора колоды/режима с серверной валидацией. Rating — заглушка.
Community — отдельная сцена с вкладками. `MetaView` удалён.
- **Проверки:** EditMode **62/62** в копии без `Library`;
`SCENE_SMOKE success=True active=Login env=False backdrop=True`;
`PLAYMAT_SMOKE success=True board=True cards=25 hand=5/5`;
`WINDOWS_BUILD result=Succeeded errors=0`; standalone запущен.
- **Не завершено:** визуальная сверка с референсом, BATTLE → очередь/бот,
магазин/история/коллекция, Community и меню — ручная проверка
пользователя.
- **Замечания:** сцены `Login/Lobby/DeckBuilder/Battle` после
`SceneBuilder.BuildAll` содержали только churn fileID — откачены;
в репозиторий добавлена только новая `Community.unity`. Арт фона Lobby
отсутствует, фон процедурный.
- **Skill обновлён:** не требуется; правила и архитектура не менялись.
- **Git:** не коммитилось.
---
### TASK-2026-023 — DeckBuilder: один экран, мультифильтры и список колоды
- **Статус:** `[ ]`
- **Создано:** 2026-10-31
- **Анализ:** DeepSeek выполнен
- **Реализация:** не начата
- **Проверка:** не выполнена
- **Затронутые системы:** `Runtime/UI/DeckBuilderView.cs`,
`Runtime/UI/CardImageService.cs`, `Localization.cs`, EditMode-тесты.
Сервер, правила, протокол и YAML не меняются.
#### A. Цель и требования
Переделать декбилдер по решениям пользователя от 2026-10-31:
1. **Один экран** вместо двух: слева коллекция, справа панель, которая
переключается между «Мои колоды» и «Колода» при входе в редактор.
2. **Мультивыбор фильтров** по клану, грейду и триггеру в модальном
оверлее; выбранные значения подсвечены, есть «Применить» и «Сбросить».
3. **Корректный список колод** справа: имя, `50/50`, `T 16/16`, формат,
распределение по грейдам, бейдж валидности, подсветка открытой колоды.
4. **В редакторе** справа вместо списка колод — карты этой колоды: превью
**только иллюстрации** карты (без рамки и текста, без искажений), имя,
количество; сортировка **грейд → имя**; группировка по `nameId`.
5. Live-валидация 50/16/4; «Сохранить» и «В бой» активны только для
валидной сохранённой колоды.
6. Поведение сохранения, создания, удаления, крафта и разбора не меняется.
7. Геймплей и визуальную приёмку проверяет пользователь.
Критерии готовности:
- переключение «список колод ⇄ колода» не пересоздаёт сцену и не теряет
выбранные фильтры;
- мультивыбор клана/грейда/триггера реально фильтрует коллекцию и
отображается в сводке;
- в списке колоды одна строка на карту (`nameId`), превью — иллюстрация
без растяжения, порядок грейд → имя;
- список колод показывает валидность и счётчики, открытая колода
подсвечена;
- существующие EditMode-тесты и смоуки проходят.
#### B. Текущее состояние
- `DeckBuilderView` (1435 строк) строит два полноэкранных режима
(`DeckList` и `DeckEditor`) с отдельными топбарами и двумя гридами
коллекции; переключение — `ShowEditor`/`BackToList`.
- Правая колонка фиксирована на 300 px: в списке — ряды 76 px
(`имя`, `N/50 · classic`), в редакторе — ряды 64 px
(превью 36×50, `G`, имя, `×N · триггер`).
- Колода хранится как список `printingId`; в редакторе группируется по
`printingId` и сортируется по нему же. Одна карта в разных принтах даёт
две строки, хотя лимит 4 копии считается по `nameId`.
- Фильтры — модальный оверлей: одиночные значения (клан, грейд, триггер),
выбранное состояние не подсвечивается, сводка показывает по одному
значению.
- `CardImageService.Apply` ставит `preserveAspect = false`: арты 350×511
растягиваются в область 174×112 (сетка) и 36×50 (список).
- Валидация выполняется только при сохранении; кнопки активны всегда.
- Тестов на фильтрацию/сортировку нет; `ClientSmokeTests` только создаёт
`DeckBuilderView`.
Что сохранить:
- загрузку коллекции/каталога/колод и контракты `MetaService`;
- лимиты 50/16/4 и правила добавления/удаления;
- оверлей карты (ПКМ), крафт/разбор;
- сцена `DeckBuilder` и переходы `SceneFlow`;
- 3D-окружение сцены не трогать в этой задаче.
#### C. Чек-лист изменений
- [ ] **C1. `MODIFY` `Runtime/UI/CardImageService.cs`** — превью
иллюстрации.
- `ApplyIllustration(Image, imageKey, fallback)`: спрайт из верхней части
карты — `Rect(0, h * 0.41, w, h * 0.59)` для 350×511 (иллюстрация до
полосы флейвора), с кешем по `imageKey`.
- `preserveAspect = true`; выравнивание — центральное; при отсутствии
арта — процедурный fallback без обрезки.
- Полный арт (`Apply`) не меняется: он нужен карточке и 3D-картам.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет.
- [ ] **C2. `MODIFY` `Runtime/UI/DeckBuilderView.cs`** — один экран.
- Убрать `_listView`/`_editorView` и `BackToList`; оставить один корень
`DeckBuilderScreen`: топбар (назад в Lobby, заголовок, фильтры,
сброс, статус) и тело (коллекция + правая панель).
- Правая панель: `DeckListPanel` и `DeckPanel` (карты колоды), видимость
переключается; при открытии колоды — `DeckPanel`, кнопка «К списку
колод» возвращает `DeckListPanel`.
- Ширина правой панели 380 px; содержимое — без пересоздания сцены.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C3, C4.
- [ ] **C3. `MODIFY` список колод.**
- Строка: имя, `N/50`, `T N/16`, формат (`Classic`, локализовано),
распределение `G0…G3`, бейдж «Готова / Не готова».
- Валидность считать локально (50 карт, 16 триггеров, ≤4 по `nameId`).
- Подсветка открытой колоды; кнопка «Создать колоду» — над списком.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2.
- [ ] **C4. `MODIFY` список карт колоды.**
- Группировать по `nameId`, счётчик — сумма принтов; превью — первая
карта группы.
- Строка 84 px: иллюстрация 64×54 (пропорция 1.18), имя (2 строки),
бейдж `G`, `×N`, триггер.
- Сортировка: `Grade` → `Loc.CardName` (`StringComparer.Ordinal`).
- Клик по строке — убрать одну копию; ПКМ — карточка. Счётчики и
валидация обновляются сразу.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C2.
- [ ] **C5. `MODIFY` фильтры — мультивыбор.**
- Хранить `HashSet<string> Clans`, `HashSet<int> Grades`,
`HashSet<string> Triggers`; пустой набор = «все».
- Оверлей: группы чипов с подсветкой выбранного, «Все»/«Сбросить»,
«Применить» (применяет черновик) и «Отмена» (откатывает).
- Сводка в топбаре: `Клан: RP, Kagero · Ранг: 0, 1 · Триггер: Heal`.
- `MatchesFilter` проверяет вхождение во все активные наборы.
- После применения — пересборка коллекции; счётчики в ячейках
обновляются без полного пересоздания (если возможно).
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2, C6.
- [ ] **C6. `MODIFY` фильтрация и сортировка — чистый класс.**
- Вынести в `Runtime/UI/DeckListFilter.cs` (или статический helper):
`Matches(meta, filter)`, `SortDeck(cards, catalog)`, `GroupDeck(...)`.
- Фильтр: наборы клана/грейда/триггера; сортировка колоды: грейд → имя;
группировка по `nameId`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C4, C5.
- [ ] **C7. `MODIFY` ячейки коллекции.**
- Иллюстрация через `ApplyIllustration`, область арта с правильной
пропорцией (например, 174×147), без растяжения.
- Показывать `×в колоде` по `nameId`; при достижении лимита 4 или 16
триггеров — приглушать и не добавлять.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C5.
- [ ] **C8. `MODIFY` live-валидация.**
- `UpdateStats` дополнить статусом валидности и цветом счётчиков;
«Сохранить», «В бой», «С ботом» — `interactable` только при валидной
сохранённой колоде.
- Ошибки лимитов — в статусе редактора, без модальных окон.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C3, C4.
- [ ] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- «Мои колоды», «К списку колод», «Готова», «Не готова», «Клан: »,
«Ранг: », «Триггер: », «Применить», «Отмена», «Все кланы»,
«Все ранги», «Все триггеры», «Иллюстрация» и подписи счётчиков.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2–C5.
- [ ] **C10. `TEST` EditMode.**
- `DeckListFilter`: мультивыбор клана/грейда/триггера, пустой набор =
все; сортировка грейд → имя; группировка по `nameId` суммирует принты.
- `CardImageService`: обрезка иллюстрации возвращает неполный rect
(проверка по спрайту/текстуре без реального файла — на fallback).
- UI: один экран, переключение панелей, сводка фильтров, бейдж
валидности.
- Существующий `Client_Views_Build` — без изменений ожиданий.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1–C9.
- [ ] **C11. `VERIFY` EditMode, smoke, standalone.**
- Копия без `Library`; EditMode; `SCENE_SMOKE`; standalone.
- Ручная проверка: фильтры, список колод, редактор, добавление/удаление,
сохранение, «В бой»/«С ботом», ПКМ-карточка.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C10.
#### D. Последовательность выполнения
1. C1 + C6: обрезка иллюстрации и чистые функции фильтра/сортировки/групп.
2. C2: один экран и переключение правой панели.
3. C3–C4: список колод и список карт колоды.
4. C5 + C7: мультифильтры и ячейки коллекции.
5. C8–C9: валидация и локализация.
6. C10–C11: тесты, smoke, standalone, ручная приёмка.
#### E. Проверки
- EditMode в копии без `Library`.
- `SceneSmoke.Run`, `RunPlaymat`, standalone-сборка.
- `git diff --check`; нет изменений в `server/`, `content/`, `Vanguard.Rules`.
- Ручная проверка — пользователь.
#### F. Решения и риски
**Зафиксировано пользователем (2026-10-31):**
1. Один экран с двумя панелями.
2. Превью — только иллюстрация карты (без рамки и текста).
3. Фильтры — модальный оверлей с мультивыбором.
4. Триггерный фильтр — тоже мультивыбор.
5. Сортировка карт колоды — грейд → имя; группировка по названию.
**Риски:**
- **Границы иллюстрации:** доля 0.59 снята с BT01/TD01 (350×511); если
встретится другой макет карты, полоса флейвора может попасть в кадр.
Константы вынести в одно место и проверить на нескольких сетах.
- **Группировка по `nameId`:** при смешанных принтах в строке показывается
иллюстрация первого; удаление уменьшает суммарный счётчик.
- **Один экран:** легко потерять состояние (открытая колода, черновик
фильтров). Хранить явно и сбрасывать только по действию пользователя.
- **Пересборка грида:** при смене фильтра она неизбежна; не пересобирать
на каждый клик добавления — обновлять счётчики точечно.
- **Валидность:** локальная проверка должна совпадать с серверной
(`DeckValidator`), иначе «Сохранить» включится, а сервер откажет.
- **3D-фон DeckBuilder** остаётся; если позже переведём на 2D, это
отдельная задача.
**Открытых вопросов, блокирующих старт, нет.**
#### G. Результат
- **Реализовано:** —
- **Проверки:** —
- **Не завершено/заблокировано:** —
- **Skill обновлён:** —
- **Git:** —