# Implementation Plan Активный журнал задач и планов изменений. Постоянные правила проекта хранятся в Skill `vanguard-remaster`; история старых блоков — в `IMPLEMENTATION_CHECKLIST.md`, `DEVELOPMENT_CHECKLIST.md` и `docs/HANDOFF.md`. Этот файл не заменяет их и не является источником игровых правил. ## Процесс 1. **DeepSeek** анализирует пожелание, код и зависимости, задаёт вопросы и создаёт/обновляет раздел задачи. Игровой код на этом этапе не меняется. 2. **Kimi** читает `AGENTS.md`, Skill и раздел задачи, сверяет план с текущим кодом, реализует, проверяет и обновляет статусы. 3. Передача — только через файлы репозитория и Git. Память модели и чат не считаются общим контекстом. 4. Если план устарел или требования изменились, Kimi останавливает зависимую часть, фиксирует причину и при необходимости возвращает задачу на анализ. 5. Нельзя удалять чужие задачи, невыполненные пункты и историю статусов. Подробный процесс — `docs/Development/AGENT-WORKFLOW.md`. ## Статусы - `[ ]` — не выполнено; - `[x]` — выполнено и подтверждено доступными проверками; - `[!]` — заблокировано или ждёт решения; - `[-]` — больше не требуется после согласованного изменения плана. Для каждого пункта храни отдельно: - **Реализация:** выполнена / не выполнена / не требуется. - **Проверка:** выполнена (что именно) / не выполнена (почему). `[x]` нельзя ставить, если реализация есть, а проверка не запускалась. ## Журнал задач | ID | Статус | Название | Фаза | Проверка | |---|---|---|---|---| | TASK-2026-001 | `[ ]` | Плавность карточных перемещений и корректные источники | DeepSeek: анализ готов; Kimi: ожидает | не выполнена | Новые задачи добавляются в конец файла отдельными разделами. Не используй один раздел для нескольких независимых задач. --- ## Шаблон задачи (не является активной задачей) ### TASK-YYYY-NNN — краткое название - **Статус:** `[ ]` - **Создано:** YYYY-MM-DD - **Анализ:** DeepSeek - **Реализация:** Kimi - **Затронутые системы:** ... #### 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, не начата - **Проверка:** не выполнена - **Затронутые системы:** `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. Чек-лист изменений - [ ] **C1. `MODIFY` `GameBoard3D.Render`** — стабильная идентичность анонимных карт. - Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`. - В `CardAssignment` добавить признак `ReuseByKey`/`StateOnly` (имя выбрать по стилю файла). - Если `_cards` уже содержит `spec.Key` — переиспользовать эту `Card3D` даже при `InstanceId == 0`; не вызывать `RentCard`. - Для `InstanceId == 0` не ставить `moved = true` только из-за отсутствия записи в `previousByInstance`. - При смене `InstanceId` 0→real в том же слоте (открытие урона) сохранить карту, обновить art/bind/face state; не отправлять её в полёт из колоды. - При исчезновении ключа освобождать карту как раньше. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C2, C4. - [ ] **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 в том же слоте — обновление карты/переворот, не новый полёт из Deck. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C1. - [ ] **C3. `MODIFY` `GameBoard3D` — карта источника из предыдущего `MatchView`.** - Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`. - Сохранять предыдущий `MatchView`/его зоны до присваивания нового `_view`. - Построить `Dictionary` для `Hand`, `Guardian`, `Soul`, `Drop`, `Trigger`, `FaceUpDamage` (и при необходимости field), где `SourcePose` содержит position/rotation/scale/faceUp. - `SourcePose(spec)`: приоритет — текущая `Card3D` (для transfer), затем previous-view зона по `InstanceId`, затем `DeckPosition`. - Источники: - Deck: `DeckPosition`, рубашка вверх (rotation с X-flip); - Soul: `SoulPosition`, лицом вверх; - Drop: `DropPosition`, лицом вверх; - Guardian: фактическая `Card3D` или `GuardianPosition`, +90°; - Trigger: фактическая `Card3D` или `TriggerPosition`, ±90°; - FaceUpDamage: `DamagePosition`, лицом вверх и на боку; - Hand: карта в руке, лицом вверх; - Field: фактическая `Card3D`/`CirclePosition`. - Не придумывать источник, если `InstanceId` нет в previous view и он не Deck: fallback Deck допустим только с комментарием/тестом. - `PlaceImmediate` для нового источника должен получать rotation с правильным flip, а не только yaw. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C1, C2. - [ ] **C4. `MODIFY` `CardMoveAnimator.Enqueue`** — один активный/queued request на карту. - Файл: `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`. - Перед постановкой нового request отменять/удалять старый request этой же `Card3D` (queued и active), не трогая другие карты. - Новый request должен начинаться с текущей визуальной позиции карты, а не со старого `FromPosition`; если target совпадает — не создавать запрос. - Не допускать дублей в `_active`; сохранить timeout/`CancelCard`/ `CancelAll` и снятие `card-move`-лока. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C1. - [ ] **C5. `MODIFY` `GameBoard3D.Render` + `BattleView.Render`** — явно различать snapshot-рендер и локальный UI-рендер. - Файлы: `GameBoard3D.cs`, `client/Assets/Vanguard/Runtime/UI/BattleView.cs`. - Добавить параметр `animateMoves` (или эквивалентный options-объект) без публичного изменения серверных DTO. - `OnSnapshot` → `animateMoves: true`. - Локальные `Render()` (guard selection, card menu, status, cost counter) → `animateMoves: false`: обновлять highlight/UI, но не запускать новые move-анимации и не менять идентичность карт. - Если при локальном Render карта реально новая (не должно происходить), поставить в target pose без анимации и записать warning в debug-лог. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C1, C2. - [ ] **C6. `MODIFY` `Card3D` / `GameBoard3D.UpdateHandView`** — не перетирать движение руки. - Файлы: `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`, `GameBoard3D.cs`. - Проверить сценарий «карта летит из Deck в Hand»: `UpdateHandView` вызывает `SetPose` каждый кадр; target должен совпадать с target request. - Если подтверждено расхождение — обновлять target request при изменении hand pose или не вызывать `SetPose` для карты, которая ещё в `CardMoveAnimator.IsBusy` для этого instance. - Не отключать руку от камеры и не менять `GetHandViewPose` без необходимости. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C3, C4. - [ ] **C7. `MODIFY` `AnimationConfig.asset` + проверка значений.** - Файл: `client/Assets/Vanguard/Resources/AnimationConfig.asset`. - Добавить/сериализовать `moveDuration`, `importantMoveDuration`, `moveDelayBetweenCards`, `arcHeight`, camera/grow/dash-поля из `AnimationConfig.cs`. - Зафиксировать разумные значения: движение ~0.4–0.6, важное ~0.8–1.0, stagger ~0.15–0.25, arc ~0.3–0.5; не менять длительности feedback без запроса. - Проверить в Unity Inspector, что значения не нулевые и совпадают с кодом. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C5 (можно параллельно). - [ ] **C8. `TEST` `GameBoard3DTests`** — регрессии анимаций. - Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`. - Добавить тесты: 1. повторный `Render` с тем же view/state не увеличивает число `Card3D` для opponent hand/face-down damage; 2. повторный `Render` не создаёт новый `CardMoveAnimator` request для стабильного ключа; 3. открытие face-down damage переиспользует `Card3D` и не отправляет его из Deck; 4. новый unit из hand сохраняет source-позицию руки, а не Deck; 5. `CardMoveAnimator` coalesce: второй Enqueue той же карты не оставляет дубль в active/queue; 6. `CancelCard` по-прежнему освобождает `card-move`-лок. - Для проверки source/queue при необходимости добавить **тестовый seam** (read-only snapshot последних запросов/позиций) в `CardMoveAnimator` или `GameBoard3D`, не меняя публичное поведение. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: C1–C6. - [ ] **C9. `VERIFY` smoke / compile / EditMode.** - Запустить EditMode в копии проекта без `Library`. - `PLAYMAT_SMOKE`; `SCENE_SMOKE` — если затронуты сцены/сборка. - Проверить отсутствие ошибок компиляции и предупреждений о missing serialized fields. - Реализация: не выполнена. - Проверка: не выполнена. - Зависит от: 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. Результат - **Реализовано:** не начато. - **Проверки:** не выполнены. - **Не завершено/заблокировано:** ожидает этапа Kimi; критических вопросов пользователю на данный момент нет. - **Skill обновлён:** не требуется до подтверждения новых постоянных правил. - **Git:** изменён только план/документация, код не менялся.