VanguardReMaster/docs/3d-ux-backlog.md

261 lines
18 KiB
Markdown
Raw Permalink 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.

# 3D UX backlog
Анализ требований по UX-долгам 3D-поля и статус реализации.
Дата анализа: **2026-10-24**.
## Статус реализации (2026-10-24)
- **P0 готово:** `CardMoveAnimator` + дуга/scale/rotation, stagger и
`InputLock`; diff по `InstanceId` с переносом пула (включая обмен двух
кругов без освобождения/дублей); таймаут/`CancelCard` не дают зависнуть
`card-move`-локу; `AnimationConfig` расширен; ориентация Trigger Zone
±90°; `DropZoneFaceDown`.
- **P0 готово:** `CameraDirector` и `TriggerCheckSequence` (zoom → hold →
return, цепочка, восстановление pose).
- **P1 частично:** `ArrowConfig`, общий `ArrowDirector`, красная attack и
синяя boost-стрелка, grow/arc/dash (UV-tile fallback), якоря у края карт.
- **P1 частично:** multi-target Counter Blast доведён до сервера
(`DamageIndexes` в `ActivateAbilityIntent`) и клиентского выбора; текущий
счётчик в панели «Действия» — промежуточная реализация. Отдельный
`CostPaymentOverlay` требуется (решение 2026-10-24), но ещё не сделан.
- **Не сделано:** multi-target Soul Blast и другие типы стоимости,
`EffectConfig`, отдельный `CostPaymentOverlay`, `AttackResolutionQueue`,
world-space Power/Shield, world-space dash shader.
## 0. Состояние до реализации (историческая часть)
Промпт предполагает наличие `CardMoveAnimator`, `CameraDirector`,
`TriggerCheckSequence`, `OnCardMoved`/`OnTargetChanged`, `CostPaymentController`,
`ArrowDirector`, `AttackResolutionQueue` и `DOTween/UniTask`. В текущем коде
их нет или они называются иначе:
| Компонент | Сейчас |
|---|---|
| `InputLockService` | есть: `Runtime/Battle/InputLockService.cs` |
| `AnimationConfig` | есть: `Runtime/Battle/AnimationConfig.cs` + `Resources/AnimationConfig.asset`; полей из нового ТЗ нет |
| Карты | `Card3D` + пул в `GameBoard3D`; движение — экспоненциальное сглаживание, не дуга, длительность не задаётся |
| События поля | `CardClicked`, `CardRightClicked`, `CardDropped`, `SlotClicked`, `PileClicked`; `OnCardMoved`/`OnTargetChanged` отсутствуют |
| Карточные оверлеи | `CardListView`, `CardDetailOverlay`, `OverlayNavigationService` есть |
| Боевой HUD | есть верхний uGUI `CombatStats` (`POWER vs SHIELD`), отдельного боевого оверлея нет |
| Стрелка | один `AttackArrowView`: LineRenderer, оранжевый, без dash/grow, без Boost, якоря в центре карт |
| Триггеры | карты статично рендерятся в Trigger Zone; камеры/последовательности проверки нет |
| Стоимость | ad-hoc выбор в `BattleView`: один `DamageIndex`/`DiscardPrintingId`; нет `EffectConfig`/overlay |
| Анимационный стек | пакетов DOTween/UniTask в `Packages/manifest.json` нет |
| Камера | фиксированная, `GameBoard3D.EnsureCamera()` выставляет pose при создании/показе |
Важное следствие: текущий `GameBoard3D.Render` ставит `immediate = true` для
новой/сменившей зону карты. Это телепорт при появлении/переносе и должно быть
устранено в рамках `CardMoveAnimator`.
## 1. CardMoveAnimator
### Задачи
1. Ввести `CardMoveAnimator` и очередь запросов `CardMoveRequest`:
`CardInstance/InstanceId`, `FromZone`, `ToZone`, from/to pose, `Duration`,
`Delay`, `ArcHeight`, флаги flip/rest, `Important`.
2. Перевести на него все переходы:
- поле ↔ поле (V/RC), hand → field (Call), field → hand;
- deck → hand/field/Damage/Trigger/Drop;
- field → Drop/Damage;
- deck → Trigger;
- Trigger → hand/Drop/field;
- guardian/intercept/retire/damage/trigger.
3. Движение по дуге: позиция через `Lerp` + подъём `sin(pi*t)*ArcHeight`
(или bezier), ease-in-out `SmoothStep`; rotation/scale интерполировать
одновременно.
4. Длительности из `AnimationConfig`: `MoveDuration`, `ImportantMoveDuration`,
`MoveDelayBetweenCards`, `ArcHeight`; дефолты 0.4–0.6 / 0.8–1.0 / 0.15–0.25.
5. Все переходы последовательности выполняются через один async-контекст:
stagger-паузы входят в блокировку; lock снимается в `try/finally`.
6. Не использовать `transform.position = ...` в игровой логике. Исключения:
drag под курсором, `Snap` первичной раскладки и сервисные телепорты при
reset/clear.
7. Переработать diff в `GameBoard3D.Render`: по `InstanceId` и переходу
зоны определить источник и анимировать; пул не должен менять ключ/Ref
раньше старта анимации.
8. Для скрытой информации (deck/opponent hand/drop) не анимировать лицо
карты до раскрытия; рубашка/флип — часть запроса.
9. Добавить события/хуки `CardMoveStarted`/`CardMoveFinished` (или
эквивалент), чтобы trigger/camera/arrow не полагались на таймеры.
### Критерий
Любое перемещение плавное, без телепорта; последовательные перемещения идут
с задержкой; ввод заблокирован до конца пачки; lock не остаётся после
исключения/отмены.
## 2. TriggerCheckSequence + CameraDirector
### Задачи
1. `CameraDirector`: снимок `position/rotation/FOV` при входе в цепочку;
`ZoomToTrigger()`, `ReturnToOrigin()`, повторный зум; идемпотентный
`Restore()`; не допускать накопления смещения/дрейфа.
2. Учесть текущий `GameBoard3D.EnsureCamera()`: он не должен перезаписывать
камеру во время работы режиссёра; ввести явное владение камерой.
3. `TriggerCheckSequence` как оркестратор:
- **A (одиночная)**: zoom → card from deck to Trigger → reveal hold →
return camera → resolve effect;
- **B (цепочка)**: zoom once → card 1 → hold → if choice: return camera →
choose target → re-zoom → next card → after last return to snapshot;
- после последней проверки камера точно в исходном pose.
4. Тайминги из `AnimationConfig`: `CameraZoomDuration`,
`TriggerRevealHold`, `CameraReturnDuration`, `CameraRezoomDuration`.
5. InputLock на всю последовательность; **перед выбором цели камеру
вернуть и lock снять**; после выбора — снова lock и re-zoom.
6. Не сбрасывать цепочку при повторном snapshot/clock; sequence state
должен быть идемпотентным и завершаться по snapshot/событию.
### Критерий
Одиночная проверка: зум → карта → резолв → камера вернулась. Цепочка:
зум один раз, проверки по очереди, выбор цели при возвращённой камере,
после последней — точный исходный pose без дрейфа.
## 3. Ориентация карт
### Задачи
1. Trigger Zone:
- своя карта: локальный yaw **+90°** (по часовой вокруг Y);
- противника: зеркально **−90°**;
- yaw задаётся матрицей/позой с учётом `localSeat`, а не только
`BaseYaw`.
2. Скрытые карты:
- рука противника, колода (обе), Drop при флаге — рубашкой вверх;
- переворот 180° вокруг X интерполируется вместе с move;
- `DropZoneFaceDown` добавить в `PlaymatLayoutConfig`/`BoardConfig`;
по правилам Classic default = **false** (Drop лицом вверх), но флаг
обязателен.
3. `Card3D.SetState`/`SetPose` расширить произвольной ориентацией и
начальным pose; не сбрасывать rotation через `Snap`, если идёт анимация.
4. Trigger-зона противника остаётся зеркальной по позиции и повороту.
### Критерий
Своя Trigger Zone — 90° по часовой; противника — зеркально. Opponent hand /
deck / Drop (если faceDown) показывают рубашку; flip происходит в процессе
анимации, не мгновенно.
## 4. CostPaymentController + EffectConfig
### Текущее ограничение
Сейчас цена выбирается ad-hoc: `_pendingAbilityCost` и один
`AbilityDamageIntent.DamageIndex`/`DiscardPrintingId`. `CounterBlast` и
`SoulBlast` на сервере автоматически забирают следующие карты; выбрать
несколько конкретных целей нельзя. Для требований multi-target cost нужен
**протокол + движок**, а не только UI.
### Задачи
1. `EffectConfig` (ScriptableObject): `CostType`
(`CounterBlast`/`SoulBlast`/…), `Amount`, `AllowFewer`, `MinRequired`,
`TargetZone`.
2. `CostPaymentController`: открыть overlay → собрать выбор → проверить
`X == N` или `X <= N && X >= MinRequired` → подтвердить → применить
стоимость → закрыть.
3. `CostPaymentOverlay`: описание, счётчик `Выбрано: X / N` (или `X` при
`AllowFewer`), подсветка hover/selected, повторный клик — toggle,
кнопка подтверждения disabled при невалидном выборе, красный счётчик.
4. Ввод: lock, кроме кликов по валидным целям и подтверждения.
5. Серверная часть: новый intent/массив выбранных damage/soul indexes
(или расширение `ActivateAbilityCommand`) с идемпотентностью и
проверкой количества; обновить `CounterBlast`/`SoulBlast` так, чтобы они
принимали выбор, а не брали первые карты.
6. Совместимость с текущими `AbilityDamageTargeting`/damage choice;
client protocol versioning не ломать.
### Критерий
Оверлей со счётчиком, toggle и зелёной подсветкой; кнопка активна только
при корректном выборе; после подтверждения стоимость применена и ввод
разблокирован; неверный выбор не отправляется.
## 5. TargetArrow / ArrowDirector
### Задачи
1. `ArrowConfig` (SO): `AttackColor`, `BoostColor`, `GrowDuration`,
`DashScrollSpeed`, `Thickness`, `BoostGrowDuration`,
`BoostThickness`, `HeadLength`, `HeadWidth`, `FadeDuration`.
2. Обобщить `AttackArrowView` до `TargetArrow`: тип `Attack`/`Boost`,
цвет, наконечник, общий материал/шейдер/анимация; отдельные префабы
только при доказанной необходимости.
3. `ArrowDirector`: одна attack-стрелка, N boost-стрелок; очистка при смене
фазы/боя/уходе карты; подписка на изменение цели/перемещение.
4. Геометрия:
- над плейматом (`Y` offset), без z-fighting;
- лёгкая дуга, чтобы не перекрывать карты;
- наконечник у **края** карты-цели, не в центре;
- from/to anchors обновлять после анимации карт.
5. Анимация attack: grow 0→1, бегущий dash, pulse, fade+scale down.
Boost: те же скрипты, синий цвет, тоньше/быстрее; несколько стрелок —
stagger.
6. `AttackResolutionQueue`: очередь целей, пошаговый резолв, подсветка
текущей цели + пауза; пока одна цель, но API на будущее.
7. Боевой оверлей **не создавать**. Power/Shield показывать у карт через
World-Space UI. Сейчас `CombatStats` — верхний uGUI-стрип; решить:
оставить как debug/скрыть или перенести показатели к картам.
8. **Пунктир**: рекомендуемый подход — **world-space dash shader**:
плотность штриха зависит от мирового расстояния, а не от UV stretch;
один материал для attack/boost; работает с изогнутым LineRenderer.
Fallback — сегментированный mesh, если шейдер/платформа не даст
стабильного результата. Выбор зафиксировать в `ArrowConfig`/документе.
### Критерий
Атака — красная пунктирная стрелка с наконечником; Boost — синяя от заднего
ряда к переднему; dash анимирован; исчезновение плавное; якоря у края;
несколько boost-стрелок со stagger; камера/движение карт не ломают точки.
## 6. Конфиги и техническая база
1. Расширить `AnimationConfig`:
`MoveDuration`, `ImportantMoveDuration`, `MoveDelayBetweenCards`,
`ArcHeight`, `CameraZoomDuration`, `TriggerRevealHold`,
`CameraReturnDuration`, `CameraRezoomDuration`, `GrowDuration`,
`DashScrollSpeed`, `BoostGrowDuration`, `BoostThickness`, `ArrowFade`.
2. Новые ScriptableObject: `EffectConfig`, `ArrowConfig`.
3. Подход к асинхронности: в проекте нет UniTask/DOTween. Нужно решение:
- предпочтительно добавить UniTask и вести последовательности async/await;
- либо использовать Coroutine + общий `AnimationSequence` без новых
зависимостей.
Зафиксировать до реализации; не смешивать три подхода.
4. Не ломать `CardListView`, `CardDetailOverlay`, `InputLockService`,
существующий `Card3D`/`GameBoard3D` pooling.
5. Проверки: EditMode-тесты на интерполяцию/ориентации/валидацию цены/якоря
стрелок; smoke; серверные тесты при изменении протокола цены.
## 7. Порядок работ
1. **P0**: `AnimationConfig` + `CardMoveAnimator` + интеграция diff/pool +
ориентации (п.1, 3).
2. **P0**: `CameraDirector` + `TriggerCheckSequence` (п.2).
3. **P1**: `ArrowConfig` + `TargetArrow`/`ArrowDirector` + attack/boost
анимации + `AttackResolutionQueue` (п.5).
4. **P1**: `EffectConfig` + `CostPaymentController/Overlay` +
протокол/движок multi-target cost (п.4).
5. **P2**: World-Space Power/Shield; `CombatStats` принят как HUD, удалять
его без отдельного решения нельзя; polish и профилирование.
## 8. Принятые решения
- **Асинхронность:** без новых пакетов; `CardMoveAnimator` и
`TriggerCheckSequence` работают через `Tick`/корутинный эквивалент.
- **Drop:** `DropZoneFaceDown = false` по Classic; флаг доступен в
`PlaymatLayoutConfig`.
- **Dash:** UV-tile fallback (`LineTextureMode.Tile` + прокрутка offset);
world-space shader оставлен как будущая замена.
- **`CombatStats`:** верхний strip **принят как HUD** (решение 2026-10-24);
world-space Power/Shield — отложенный P2.
- **Multi-target cost:** расширен `ActivateAbilityIntent.DamageIndexes`
(`[Key(3)]`, обратная совместимость); `CounterBlast` принимает массив.
Отдельный `CostPaymentOverlay` **требуется**; текущий выбор в панели
«Действия» — промежуточная реализация. `Soul Blast` и остальные типы —
следующий шаг.
- **Камера:** владелец — `CameraDirector`; `EnsureCamera()` не перезаписывает
pose, пока `CameraDirector.IsActive`. `TriggerCheckSequence` снимает
`InputLock` перед выбором цели и возвращает камеру после hold.