258 lines
17 KiB
Markdown
258 lines
17 KiB
Markdown
# 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`) и клиентского выбора; overlay
|
||
заменён счётчиком в панели «Действия» (по правилам проекта).
|
||
- **Не сделано:** 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` при
|
||
необходимости, polish и профилирование.
|
||
|
||
## 8. Принятые решения
|
||
|
||
- **Асинхронность:** без новых пакетов; `CardMoveAnimator` и
|
||
`TriggerCheckSequence` работают через `Tick`/корутинный эквивалент.
|
||
- **Drop:** `DropZoneFaceDown = false` по Classic; флаг доступен в
|
||
`PlaymatLayoutConfig`.
|
||
- **Dash:** UV-tile fallback (`LineTextureMode.Tile` + прокрутка offset);
|
||
world-space shader оставлен как будущая замена.
|
||
- **`CombatStats`:** верхний strip пока оставлен как HUD; world-space
|
||
Power/Shield — P2.
|
||
- **Multi-target cost:** расширен `ActivateAbilityIntent.DamageIndexes`
|
||
(`[Key(3)]`, обратная совместимость); `CounterBlast` принимает массив.
|
||
`Soul Blast` и остальные типы — следующий шаг.
|
||
- **Камера:** владелец — `CameraDirector`; `EnsureCamera()` не перезаписывает
|
||
pose, пока `CameraDirector.IsActive`. `TriggerCheckSequence` снимает
|
||
`InputLock` перед выбором цели и возвращает камеру после hold.
|