# 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.