16 KiB
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
Задачи
- Ввести
CardMoveAnimatorи очередь запросовCardMoveRequest:CardInstance/InstanceId,FromZone,ToZone, from/to pose,Duration,Delay,ArcHeight, флаги flip/rest,Important. - Перевести на него все переходы:
- поле ↔ поле (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.
- Движение по дуге: позиция через
Lerp+ подъёмsin(pi*t)*ArcHeight(или bezier), ease-in-outSmoothStep; rotation/scale интерполировать одновременно. - Длительности из
AnimationConfig:MoveDuration,ImportantMoveDuration,MoveDelayBetweenCards,ArcHeight; дефолты 0.4–0.6 / 0.8–1.0 / 0.15–0.25. - Все переходы последовательности выполняются через один async-контекст:
stagger-паузы входят в блокировку; lock снимается в
try/finally. - Не использовать
transform.position = ...в игровой логике. Исключения: drag под курсором,Snapпервичной раскладки и сервисные телепорты при reset/clear. - Переработать diff в
GameBoard3D.Render: поInstanceIdи переходу зоны определить источник и анимировать; пул не должен менять ключ/Ref раньше старта анимации. - Для скрытой информации (deck/opponent hand/drop) не анимировать лицо карты до раскрытия; рубашка/флип — часть запроса.
- Добавить события/хуки
CardMoveStarted/CardMoveFinished(или эквивалент), чтобы trigger/camera/arrow не полагались на таймеры.
Критерий
Любое перемещение плавное, без телепорта; последовательные перемещения идут с задержкой; ввод заблокирован до конца пачки; lock не остаётся после исключения/отмены.
2. TriggerCheckSequence + CameraDirector
Задачи
CameraDirector: снимокposition/rotation/FOVпри входе в цепочку;ZoomToTrigger(),ReturnToOrigin(), повторный зум; идемпотентныйRestore(); не допускать накопления смещения/дрейфа.- Учесть текущий
GameBoard3D.EnsureCamera(): он не должен перезаписывать камеру во время работы режиссёра; ввести явное владение камерой. 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.
- Тайминги из
AnimationConfig:CameraZoomDuration,TriggerRevealHold,CameraReturnDuration,CameraRezoomDuration. - InputLock на всю последовательность; перед выбором цели камеру вернуть и lock снять; после выбора — снова lock и re-zoom.
- Не сбрасывать цепочку при повторном snapshot/clock; sequence state должен быть идемпотентным и завершаться по snapshot/событию.
Критерий
Одиночная проверка: зум → карта → резолв → камера вернулась. Цепочка: зум один раз, проверки по очереди, выбор цели при возвращённой камере, после последней — точный исходный pose без дрейфа.
3. Ориентация карт
Задачи
- Trigger Zone:
- своя карта: локальный yaw +90° (по часовой вокруг Y);
- противника: зеркально −90°;
- yaw задаётся матрицей/позой с учётом
localSeat, а не толькоBaseYaw.
- Скрытые карты:
- рука противника, колода (обе), Drop при флаге — рубашкой вверх;
- переворот 180° вокруг X интерполируется вместе с move;
DropZoneFaceDownдобавить вPlaymatLayoutConfig/BoardConfig; по правилам Classic default = false (Drop лицом вверх), но флаг обязателен.
Card3D.SetState/SetPoseрасширить произвольной ориентацией и начальным pose; не сбрасывать rotation черезSnap, если идёт анимация.- 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.
Задачи
EffectConfig(ScriptableObject):CostType(CounterBlast/SoulBlast/…),Amount,AllowFewer,MinRequired,TargetZone.CostPaymentController: открыть overlay → собрать выбор → проверитьX == NилиX <= N && X >= MinRequired→ подтвердить → применить стоимость → закрыть.CostPaymentOverlay: описание, счётчикВыбрано: X / N(илиXприAllowFewer), подсветка hover/selected, повторный клик — toggle, кнопка подтверждения disabled при невалидном выборе, красный счётчик.- Ввод: lock, кроме кликов по валидным целям и подтверждения.
- Серверная часть: новый intent/массив выбранных damage/soul indexes
(или расширение
ActivateAbilityCommand) с идемпотентностью и проверкой количества; обновитьCounterBlast/SoulBlastтак, чтобы они принимали выбор, а не брали первые карты. - Совместимость с текущими
AbilityDamageTargeting/damage choice; client protocol versioning не ломать.
Критерий
Оверлей со счётчиком, toggle и зелёной подсветкой; кнопка активна только при корректном выборе; после подтверждения стоимость применена и ввод разблокирован; неверный выбор не отправляется.
5. TargetArrow / ArrowDirector
Задачи
ArrowConfig(SO):AttackColor,BoostColor,GrowDuration,DashScrollSpeed,Thickness,BoostGrowDuration,BoostThickness,HeadLength,HeadWidth,FadeDuration.- Обобщить
AttackArrowViewдоTargetArrow: типAttack/Boost, цвет, наконечник, общий материал/шейдер/анимация; отдельные префабы только при доказанной необходимости. ArrowDirector: одна attack-стрелка, N boost-стрелок; очистка при смене фазы/боя/уходе карты; подписка на изменение цели/перемещение.- Геометрия:
- над плейматом (
Yoffset), без z-fighting; - лёгкая дуга, чтобы не перекрывать карты;
- наконечник у края карты-цели, не в центре;
- from/to anchors обновлять после анимации карт.
- над плейматом (
- Анимация attack: grow 0→1, бегущий dash, pulse, fade+scale down. Boost: те же скрипты, синий цвет, тоньше/быстрее; несколько стрелок — stagger.
AttackResolutionQueue: очередь целей, пошаговый резолв, подсветка текущей цели + пауза; пока одна цель, но API на будущее.- Боевой оверлей не создавать. Power/Shield показывать у карт через
World-Space UI. Сейчас
CombatStats— верхний uGUI-стрип; решить: оставить как debug/скрыть или перенести показатели к картам. - Пунктир: рекомендуемый подход — world-space dash shader:
плотность штриха зависит от мирового расстояния, а не от UV stretch;
один материал для attack/boost; работает с изогнутым LineRenderer.
Fallback — сегментированный mesh, если шейдер/платформа не даст
стабильного результата. Выбор зафиксировать в
ArrowConfig/документе.
Критерий
Атака — красная пунктирная стрелка с наконечником; Boost — синяя от заднего ряда к переднему; dash анимирован; исчезновение плавное; якоря у края; несколько boost-стрелок со stagger; камера/движение карт не ломают точки.
6. Конфиги и техническая база
- Расширить
AnimationConfig:MoveDuration,ImportantMoveDuration,MoveDelayBetweenCards,ArcHeight,CameraZoomDuration,TriggerRevealHold,CameraReturnDuration,CameraRezoomDuration,GrowDuration,DashScrollSpeed,BoostGrowDuration,BoostThickness,ArrowFade. - Новые ScriptableObject:
EffectConfig,ArrowConfig. - Подход к асинхронности: в проекте нет UniTask/DOTween. Нужно решение:
- предпочтительно добавить UniTask и вести последовательности async/await;
- либо использовать Coroutine + общий
AnimationSequenceбез новых зависимостей. Зафиксировать до реализации; не смешивать три подхода.
- Не ломать
CardListView,CardDetailOverlay,InputLockService, существующийCard3D/GameBoard3Dpooling. - Проверки: EditMode-тесты на интерполяцию/ориентации/валидацию цены/якоря стрелок; smoke; серверные тесты при изменении протокола цены.
7. Порядок работ
- P0:
AnimationConfig+CardMoveAnimator+ интеграция diff/pool + ориентации (п.1, 3). - P0:
CameraDirector+TriggerCheckSequence(п.2). - P1:
ArrowConfig+TargetArrow/ArrowDirector+ attack/boost анимации +AttackResolutionQueue(п.5). - P1:
EffectConfig+CostPaymentController/Overlay+ протокол/движок multi-target cost (п.4). - P2: World-Space Power/Shield, удаление/скрытие
CombatStatsпри необходимости, polish и профилирование.
8. Открытые решения перед кодом
- UniTask или Coroutine (в проекте нет ни DOTween, ни UniTask).
DropZoneFaceDowndefault: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().