# 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-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` для `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 ` -runTests -testPlatform EditMode -testResults \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`, регэксп по `
  • `, триггеры/редкости из `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 --art-dir `. - Полный прогон в копию `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`; массовая замена артов на официальные PNG (решение пользователя). - **Skill обновлён:** не требуется. - **Git:** не коммитилось.