VanguardReMaster/docs/Development/IMPLEMENTATION_PLAN.md

453 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, не начата
- **Проверка:** не выполнена
- **Затронутые системы:** `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<int, SourcePose>` для `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 <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. Результат
- **Реализовано:** не начато.
- **Проверки:** не выполнены.
- **Не завершено/заблокировано:** ожидает этапа Kimi; критических вопросов
пользователю на данный момент нет.
- **Skill обновлён:** не требуется до подтверждения новых постоянных правил.
- **Git:** изменён только план/документация, код не менялся.