From 083357f3705f2e021077720b6a1ddf37a828a786 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Sat, 24 Oct 2026 01:42:26 +0500 Subject: [PATCH] Document 3D UX backlog and implementation plan --- docs/3d-ux-backlog.md | 236 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 docs/3d-ux-backlog.md diff --git a/docs/3d-ux-backlog.md b/docs/3d-ux-backlog.md new file mode 100644 index 0000000..7653684 --- /dev/null +++ b/docs/3d-ux-backlog.md @@ -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()`.