VanguardReMaster/docs/Development/IMPLEMENTATION_PLAN.md

447 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Implementation Plan
Активный журнал задач и планов изменений. Постоянные правила проекта хранятся
в Skill `vanguard-remaster`; история старых блоков — в `IMPLEMENTATION_CHECKLIST.md`,
`DEVELOPMENT_CHECKLIST.md` и `docs/HANDOFF.md`. Этот файл не заменяет их и не
является источником игровых правил.
## Процесс
1. **DeepSeek** анализирует пожелание, код и зависимости, задаёт вопросы и
создаёт/обновляет раздел задачи. Игровой код на этом этапе не меняется.
2. **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 выполнена
- **Проверка:** автотесты 46/46; ручная C10 — пользователь
- **Затронутые системы:** `GameBoard3D`, `Card3D`, `CardMoveAnimator`,
`BattleView`, `AnimationConfig`, EditMode-тесты.
#### A. Цель и требования
Пользователь сообщил: анимации движения карт «отрабатывают рвано»,
запускаются ненужные анимации, карты двигаются не из нужных позиций. Нужно
исправить именно презентационный слой, не меняя правила игры.
Ожидаемое поведение:
- анимация запускается только при реальном переходе карты между зонами/слотами
или при изменении позы, а не при каждом UI-рендере;
- источник анимации — фактическая старая зона/поза карты (рука, поле, Trigger,
Damage, Drop, Soul, Guardian, Deck), а не всегда колода;
- рубашка/лицо источника учитываются: карта из колоды начинает движение
рубашкой вверх, а не лицом;
- повторный снапшот или локальный `Render` не создаёт второй запрос на ту же
карту и не откатывает её в середину пути;
- скрытые карты (рука противника, закрытый урон) не пересоздаются и не летят
из колоды на каждом `Render`;
- существующие правила, серверные контракты, `InputLockService`, пул карт и
публичные визуальные эффекты не ломаются.
Ограничения:
- не менять `GameEngine`, правила, сервер, протокол и YAML-контент;
- не добавлять DOTween/UniTask/новые пакеты;
- не менять сцены/префабы без отдельного решения;
- не переписывать `CardMoveAnimator` на другой animation stack;
- геймплей вручную проверяет пользователь; автотесты показывают только
техническую корректность.
#### B. Текущее состояние и найденные причины
Проверены:
- `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs`;
- `client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs`;
- `client/Assets/Vanguard/Runtime/UI/BattleView.cs`;
- `client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs`;
- `client/Assets/Vanguard/Resources/AnimationConfig.asset`;
- `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
Факты:
1. **Скрытые карты с `InstanceId = 0` пересоздаются.** В
`GameBoard3D.AddOpponentHandSpecs` (`OH:player:i`) и
`GameBoard3D.AddDamageSpecs` (`D:player:i`) для скрытых карт
`BoardCardRef.InstanceId` равен `0`. В `GameBoard3D.Render` распределение
старых карт идёт только через `previousByInstance` и условие
`spec.Ref.InstanceId != 0`. Поэтому при каждом `Render` такие карты
арендуются заново, старые освобождаются, а новые получают `moved = true` и
анимацию из `SourcePose`.
2. **`BattleView.Render()` вызывается не только на снапшот.** Локальные
действия (выбор guard, меню карты, подтверждение, статус) тоже вызывают
`BattleView.Render` → `GameBoard3D.Render`. В сочетании с п.1 это даёт
«ненужные анимации» на каждый клик.
3. **`SourcePose` всегда использует колоду.** Метод
`GameBoard3D.SourcePose` возвращает `_layout.DeckPosition(localSeat)` и
базовый yaw без состояния лица. Новая карта, пришедшая из Soul, Drop,
Guardian, Trigger или Damage, начинает полёт из колоды; карта из колоды
начинает путь лицом вверх, хотя должна быть рубашкой вверх.
4. **У Damage ключ по индексу, а не по личности.** `D:player:i` остаётся тем
же при смене открытой карты или при открытии закрытой. Текущий branch
`sameInstance` освобождает старую карту и создаёт новую с `SourcePose`
(колода), хотя это может быть переворот/смена карты на месте.
5. **Нет coalescing в `CardMoveAnimator`.** `Enqueue` не отменяет старый
queued/active request той же `Card3D`. Если карта успевает сменить зону до
старта предыдущего запроса, второй запрос может стартовать из устаревшей
позиции; активная запись при этом может остаться в `_active`.
6. **`Card3D.SetPose` не является точкой начала движения.** Для существующей
карты `Render` вызывает `SetPose` (меняет target), затем `Enqueue`; для
новой карты `PlaceImmediate` + `SetPose`. При повторном `Render` target
может быть обновлён до/после старта, а старый arc не всегда
согласован с новым request.
7. **Конфиг анимаций не синхронизирован с кодом.** В
`AnimationConfig.asset` нет новых полей `moveDuration`,
`importantMoveDuration`, `moveDelayBetweenCards`, `arcHeight`, camera/arrow
полей, хотя они объявлены в `AnimationConfig.cs`. Нужно проверить
фактические значения после импорта Unity; нельзя считать asset
настроенным, пока поля не сериализованы.
8. **`UpdateHandView` каждый кадр делает `SetPose` для руки.** Это может
перетирать target во время движения карты в руку и мешать стабильности
arc; нужно проверить и, если подтверждено, не менять target у карты,
которая уже летит, либо синхронизировать target с move request.
9. **Пул карт и `InputLock` уже имеют защиту** (`CancelCard`, таймаут,
`CancelAll`); её нельзя удалять. Исправления должны сохранить отсутствие
вечного `card-move`-лока.
Что сохранить:
- `GameBoard3D` как единственный рендер-дифф и владелец пула;
- `CardMoveAnimator` как единый планировщик;
- `Card3D` как визуальную карту и arc-интерполяцию;
- `InputLockService` и существующие long-running последовательности;
- серверный snapshot как источник состояния.
#### C. Чек-лист изменений
- [x] **C1. `MODIFY` `GameBoard3D.Render`** — стабильная идентичность
анонимных карт.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Добавлена ветка reuse-by-key до поиска по `InstanceId`.
- Переиспользуется `Card3D` по `spec.Key`, если старая/новая карта анонимна
или совпадают реальные `InstanceId`; иначе используется `previousByInstance`
или `RentCard`.
- При смене `0→real` в том же слоте карта сохраняется и обновляется
`Bind`/`SetArt`/`SetPose` без анимации.
- Карты с исчезнувшими ключами освобождаются через `ReleaseCard`.
- Реализация: выполнена.
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
`Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone` — проходят.
- Зависит от: C2, C4.
- [x] **C2. `MODIFY` `GameBoard3D.AddOpponentHandSpecs` /
`AddDamageSpecs`** — стабильные ключи и явная политика анимации.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Ключи `OH:player:i` и `D:player:i` оставлены; `InstanceId=0` не считается
личностью.
- Скрытые карты при стабильном ключе не анимируются; новая скрытая карта в
конце анимируется из Deck.
- Для урона: смена `0↔real` или `real→real` в том же слоте обрабатывается
reuse-by-key, а не полётом из Deck.
- Реализация: выполнена.
- Проверка: см. C1 + `Board_Renders_Field_Hand_Guardian_Damage_And_Soul`.
- Зависит от: C1.
- [x] **C3. `MODIFY` `GameBoard3D` — карта источника из предыдущего
`MatchView`.**
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- Сохраняются `_previousView` и `_sourceByInstance` до замены `_view`.
- Построен `Dictionary<int, CardSourcePose>` для `Soul`, `Drop`,
`FaceUpDamage`; источники `Hand`/`Guardian`/`Trigger`/`Field` покрываются
существующими `Card3D` через `previousByInstance`.
- `ResolveSourcePose(spec)`: приоритет — `Card3D` (transfer), затем зона из
`_sourceByInstance`, затем `DeckPosition` с рубашкой вверх.
- Источники:
- Deck: `DeckPosition`, рубашка вверх (`baseYaw * X-flip 180°`);
- Soul: `SoulPosition`, лицом вверх;
- Drop: `DropPosition`, лицом вверх;
- FaceUpDamage: `DamagePosition`, лицом вверх и на боку;
- Hand/Guardian/Trigger/Field — через `previousByInstance`/transfer.
- Fallback Deck используется только с комментарием и покрывается тестами.
- `PlaceImmediate` получает rotation с учётом flip.
- Реализация: выполнена.
- Проверка: `New_Field_Unit_Starts_From_Hand_Not_Deck` — проходит.
- Зависит от: C1, C2.
- [x] **C4. `MODIFY` `CardMoveAnimator.Enqueue`** — один активный/queued
request на карту.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`.
- В начале `Enqueue` вызывается `CancelCard(card)`, что удаляет queued/
active request этой же карты, не трогая другие.
- Источник (`from*`) передаётся из `GameBoard3D` как текущая визуальная
позиция карты; target проверяется в `Card3D.StartArcMove`.
- `CancelCard`, `CancelAll`, timeout и снятие `card-move`-лока сохранены.
- Реализация: выполнена.
- Проверка: `CardMoveAnimator_Coalesces_Requests_For_Same_Card`,
`CardMoveAnimator_Releases_Input_Lock_When_Card_Is_Cancelled` — проходят.
- Зависит от: C1.
- [x] **C5. `MODIFY` `GameBoard3D.Render` + `BattleView.Render`** — явно
различать snapshot-рендер и локальный UI-рендер.
- Файлы: `GameBoard3D.cs`, `client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
- `GameBoard3D.Render` получил параметр `bool animateMoves = true` без
изменения DTO.
- `BattleView.Render(bool animateMoves = true)`; `OnSnapshot` вызывает
`Render()` (`true`); все локальные вызовы заменены на
`Render(animateMoves: false)`.
- При `animateMoves: false` move-анимации не запускаются; идентичность карт
не меняется. Появление реально новой карты логирует warning.
- Реализация: выполнена.
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
`ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards` — проходят.
- Зависит от: C1, C2.
- [x] **C6. `MODIFY` `GameBoard3D.UpdateHandView`** — не перетирать движение
руки.
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
- В `UpdateHandView` добавлено условие: пропускать карту, если
`card.IsMoving`, чтобы `SetPose` не менял target во время arc-перехода.
- `GetHandViewPose` и привязка руки к камере не изменены.
- Реализация: выполнена.
- Проверка: `Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player`,
`New_Field_Unit_Starts_From_Hand_Not_Deck` — проходят.
- Зависит от: C3, C4.
- [x] **C7. `MODIFY` `AnimationConfig.asset` + проверка значений.**
- Файл: `client/Assets/Vanguard/Resources/AnimationConfig.asset`.
- Добавлены/сериализованы все поля из `AnimationConfig.cs`:
`moveDuration: 0.5`, `importantMoveDuration: 0.9`,
`moveDelayBetweenCards: 0.18`, `arcHeight: 0.45`,
`cameraZoomDuration: 0.35`, `triggerRevealHold: 1.2`,
`cameraReturnDuration: 0.4`, `cameraRezoomDuration: 0.3`,
`growDuration: 0.35`, `dashScrollSpeed: 1.5`,
`boostGrowDuration: 0.3`, `boostThickness: 0.8`,
`arrowFadeDuration: 0.2`.
- Длительности feedback не менялись.
- Реализация: выполнена.
- Проверка: `CardOverlayTests.Animation_Config_Asset_Is_Loaded_From_Resources`,
поле `arcHeight` читается в `GameBoard3D.Render` — проходят.
- Зависит от: C5 (можно параллельно).
- [x] **C8. `TEST` `GameBoard3DTests`** — регрессии анимаций.
- Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
- Добавлены тесты:
1. `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`;
2. `Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone`;
3. `New_Field_Unit_Starts_From_Hand_Not_Deck`;
4. `CardMoveAnimator_Coalesces_Requests_For_Same_Card`.
- В `CardMoveAnimator` добавлены read-only seam-свойства:
`PendingCount`, `ActiveCount`, `IsCardQueued`, `IsCardActive`.
- Исправлен `ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards`
для вызова `Render` с новой сигнатурой через reflection.
- Реализация: выполнена.
- Проверка: все новые тесты + старые — 46/46.
- Зависит от: C1–C6.
- [x] **C9. `VERIFY` smoke / compile / EditMode.**
- Скопирован проект без `Library`;
`Unity 6000.6.6f1 -batchmode -nographics -runTests -testPlatform EditMode`.
- EditMode: **46/46 passed** (включая 15 `GameBoard3DTests`, 12
`ClientSmokeTests`, 6 `CardOverlayTests`, 12 `PlaymatLayoutTests`, 1
Addressables stub).
- `PLAYMAT_SMOKE success=True board=True cards=25 hand=5/5 fieldVisible=True`.
- `SCENE_SMOKE success=True active=Login env=True login=True`.
- Ошибок компиляции нет; warning CS8632 в `ClientSmokeTests` — pre-existing.
- Реализация: выполнена.
- Проверка: выполнена.
- Зависит от: C1–C8.
- [ ] **C10. `VERIFY` ручная проверка пользователем.**
- Пользователь проверяет: draw, call, ride, move/swap, guard, trigger,
drive/damage, retire, search, открытие скрытого урона, руку противника,
повторные клики по UI во время/после анимации.
- Ожидаемо: нет полёта скрытых карт из колоды при каждом клике; источник
совпадает с зоной; нет рывков/двойных стартов; ввод не зависает.
- Реализация: не применима.
- Проверка: остаётся на пользователе.
- Зависит от: C9.
#### D. Последовательность выполнения
1. C1 + C2: стабилизировать идентичность/ключи, убрать пересоздание скрытых
карт. Без этого остальные исправления будут маскировать симптом.
2. C3: добавить source map и корректные source pose/face. Проверить Deck,
Hand, Trigger, Damage, Soul/Drop/Guardian.
3. C4: coalescing в аниматоре и отмена старого request.
4. C5: разделить snapshot/UI-рендер, убрать лишние запуски.
5. C6: проверить hand target; не менять без подтверждённой необходимости.
6. C7: синхронизировать asset.
7. C8: тесты; C9: проверки; C10: ручная проверка пользователя.
#### E. Проверки
- Client EditMode: `GameBoard3DTests` + новые тесты из C8.
- Compile/Assembly: Unity batch mode в копии без `Library`.
- Smoke: `Vanguard.Editor.SceneSmoke.RunPlaymat`; `Run` только при изменении
сцен/сборки.
- `git diff --check`, просмотр `git diff`, отсутствие изменений в
Rules/Server/Shared/Content.
- Повторный `Render` с неизменным view: число карт и move-request не растёт.
- Повторный snapshot во время движения: нет двойного старта/сброса в
источник.
- Ручная проверка — пользователем; не заменять её автотестом.
Команды (подтверждены проектом):
```powershell
# клиентская копия без Library
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
-batchmode -nographics -projectPath <copy> `
-runTests -testPlatform EditMode -testResults <copy>\TestResults.xml
```
Серверные тесты не требуются, если не меняются Rules/Server/Shared; при
изменении контрактов — только `ssh home`/Docker.
#### F. Риски и неизвестные вопросы
- **Анонимные карты:** `InstanceId=0` не является уникальной личностью;
нельзя использовать его для глобального map. Нужны стабильные ключи по
зоне/индексу и отдельная политика для reveal.
- **Источник вне видимых зон:** Deck не содержит `InstanceId`; для карты из
колоды источник всегда Deck. Для карты из Soul/Drop/Guardian/Trigger/Damage
источник можно восстановить из previous view; иначе fallback Deck.
- **Смена открытого урона:** при перестановке face-up damage возможна
неоднозначность. Не анимировать перемещение между слотами урона, если это
не подтверждено; приоритет — не «телепорт из колоды».
- **`AnimationConfig.asset`:** после добавления полей Unity может сохранить
старые значения; проверить Inspector, не считать defaults автоматически
настроенными.
- **`UpdateHandView`:** если менять target каждый кадр, можно ухудшить
плавность; C6 требует подтверждения тестом/наблюдением, а не слепой правки.
- **Не менять** `CardMoveAnimator` timeout/`CancelCard`/`CancelAll` без
сохранения защиты от зависшего `InputLock`.
- **Не расширять** задачу на рефакторинг `BattleView` или замену animation
stack.
#### G. Результат
- **Реализовано:** C1–C8 (см. чек-лист).
- **Проверки:** EditMode 46/46 в копии без `Library`; компиляция без ошибок.
- **Не завершено/заблокировано:** C10 — ручная проверка пользователем.
- **Skill обновлён:** не требуется; изменения технические, не затрагивают
подтверждённые правила/архитектуру.
- **Git:** будут изменены `GameBoard3D.cs`, `CardMoveAnimator.cs`,
`BattleView.cs`, `AnimationConfig.asset`, `GameBoard3DTests.cs`,
`ClientSmokeTests.cs`, `IMPLEMENTATION_PLAN.md`, `docs/SESSION-STATE.md`,
`docs/HANDOFF.md`.