Document 3D UX backlog and implementation plan
CI / test (push) Successful in 1m57s
Details
CI / test (push) Successful in 1m57s
Details
This commit is contained in:
parent
0d5f7b933d
commit
083357f370
|
|
@ -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()`.
|
||||
Loading…
Reference in New Issue