Document 3D UX backlog and implementation plan
CI / test (push) Successful in 1m57s Details

This commit is contained in:
MpakobecEx 2026-10-24 01:42:26 +05:00
parent 0d5f7b933d
commit 083357f370
1 changed files with 236 additions and 0 deletions

236
docs/3d-ux-backlog.md Normal file
View File

@ -0,0 +1,236 @@
# 3D UX backlog
Анализ требований по UX-долгам 3D-поля. **Это план доработок, не реализация.**
Дата анализа: **2026-10-24**.
## 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` при
необходимости, polish и профилирование.
## 8. Открытые решения перед кодом
- UniTask или Coroutine (в проекте нет ни DOTween, ни UniTask).
- `DropZoneFaceDown` default: `false` по Classic, но нужен ли рубашкой вверх
для Drop в текущем UX?
- Dash: world-space shader или сегментированный mesh (рекомендация —
shader, fallback — mesh).
- Судьба верхнего `CombatStats`: скрыть/оставить debug/перенести к картам.
- Протокол multi-target cost: новый `CostPaymentIntent` или расширение
`ActivateAbilityIntent` массивами (влияет на `Vanguard.Shared` и
обратную совместимость).
- Камера: кто владеет `Camera` во время trigger sequence и как это
сочетается с `EnsureCamera()`.