VanguardReMaster/docs/3d-ux-backlog.md

18 KiB
Raw Permalink Blame History

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) и клиентского выбора; текущий счётчик в панели «Действия» — промежуточная реализация. Отдельный CostPaymentOverlay требуется (решение 2026-10-24), но ещё не сделан.
  • Не сделано: 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 принят как HUD, удалять его без отдельного решения нельзя; polish и профилирование.

8. Принятые решения

  • Асинхронность: без новых пакетов; CardMoveAnimator и TriggerCheckSequence работают через Tick/корутинный эквивалент.
  • Drop: DropZoneFaceDown = false по Classic; флаг доступен в PlaymatLayoutConfig.
  • Dash: UV-tile fallback (LineTextureMode.Tile + прокрутка offset); world-space shader оставлен как будущая замена.
  • CombatStats: верхний strip принят как HUD (решение 2026-10-24); world-space Power/Shield — отложенный P2.
  • Multi-target cost: расширен ActivateAbilityIntent.DamageIndexes ([Key(3)], обратная совместимость); CounterBlast принимает массив. Отдельный CostPaymentOverlay требуется; текущий выбор в панели «Действия» — промежуточная реализация. Soul Blast и остальные типы — следующий шаг.
  • Камера: владелец — CameraDirector; EnsureCamera() не перезаписывает pose, пока CameraDirector.IsActive. TriggerCheckSequence снимает InputLock перед выбором цели и возвращает камеру после hold.