1720 lines
116 KiB
Markdown
1720 lines
116 KiB
Markdown
# Implementation Plan
|
||
|
||
Активный журнал задач и планов изменений. Постоянные правила проекта хранятся
|
||
в Skill `vanguard-remaster`; история старых блоков — в `IMPLEMENTATION_CHECKLIST.md`,
|
||
`DEVELOPMENT_CHECKLIST.md` и `docs/HANDOFF.md`. Этот файл не заменяет их и не
|
||
является источником игровых правил.
|
||
|
||
## Процесс
|
||
|
||
1. **DeepSeek** анализирует пожелание, код и зависимости, задаёт вопросы и
|
||
создаёт/обновляет раздел задачи. Игровой код на этом этапе не меняется.
|
||
2. **DeepSeek (реализация)** читает `AGENTS.md`, Skill и раздел задачи, сверяет
|
||
план с текущим кодом, реализует, проверяет и обновляет статусы.
|
||
3. Передача — только через файлы репозитория и Git. Память модели и чат не
|
||
считаются общим контекстом.
|
||
4. Если план устарел или требования изменились, реализация останавливает
|
||
зависимую часть, фиксирует причину и при необходимости возвращает задачу на
|
||
анализ.
|
||
5. Нельзя удалять чужие задачи, невыполненные пункты и историю статусов.
|
||
|
||
Подробный процесс — `docs/Development/AGENT-WORKFLOW.md`.
|
||
|
||
## Статусы
|
||
|
||
- `[ ]` — не выполнено;
|
||
- `[x]` — выполнено и подтверждено доступными проверками;
|
||
- `[!]` — заблокировано или ждёт решения;
|
||
- `[-]` — больше не требуется после согласованного изменения плана.
|
||
|
||
Для каждого пункта храни отдельно:
|
||
|
||
- **Реализация:** выполнена / не выполнена / не требуется.
|
||
- **Проверка:** выполнена (что именно) / не выполнена (почему).
|
||
|
||
`[x]` нельзя ставить, если реализация есть, а проверка не запускалась.
|
||
|
||
## Журнал задач
|
||
|
||
| ID | Статус | Название | Фаза | Проверка |
|
||
|---|---|---|---|---|
|
||
| TASK-2026-001 | `[!]` | Плавность карточных перемещений и корректные источники | DeepSeek: анализ; реализация выполнена, ждёт ручной проверки C10 | EditMode 46/46; ручная — пользователь |
|
||
| TASK-2026-019 | `[ ]` | Загрузочная сцена и кадр без покадровых аллокаций | DeepSeek: план готов; реализация ожидает | не выполнена |
|
||
|
||
Новые задачи добавляются в конец файла отдельными разделами. Не используй один
|
||
раздел для нескольких независимых задач.
|
||
|
||
---
|
||
|
||
## Шаблон задачи (не является активной задачей)
|
||
|
||
### TASK-YYYY-NNN — краткое название
|
||
|
||
- **Статус:** `[ ]`
|
||
- **Создано:** YYYY-MM-DD
|
||
- **Анализ:** DeepSeek
|
||
- **Реализация:** DeepSeek
|
||
- **Затронутые системы:** ...
|
||
|
||
#### A. Цель и требования
|
||
|
||
- Что просит пользователь.
|
||
- Ожидаемое поведение.
|
||
- Обязательные правила Skill.
|
||
- Ограничения, которые нельзя нарушать.
|
||
- Критерии готовности.
|
||
|
||
#### B. Текущее состояние
|
||
|
||
- Как работает сейчас.
|
||
- Какие системы и файлы проверены.
|
||
- Существующие механики, которые нужно сохранить.
|
||
- Подтверждённые факты и открытые вопросы.
|
||
|
||
#### C. Чек-лист изменений
|
||
|
||
Каждый пункт:
|
||
|
||
- `[ ]` `MODIFY` / `CREATE` / `DELETE` / `TEST` / `VERIFY`
|
||
- путь к файлу (если известен);
|
||
- класс/метод (если существует);
|
||
- конкретная логика;
|
||
- причина изменения;
|
||
- ожидаемый результат;
|
||
- зависимости от других пунктов;
|
||
- **Реализация:** не выполнена;
|
||
- **Проверка:** не выполнена.
|
||
|
||
Пример формата:
|
||
|
||
```text
|
||
- [ ] MODIFY `client/Assets/.../BattleView.cs` — `BattleView.SomeMethod`
|
||
Логика: ...
|
||
Причина: ...
|
||
Результат: ...
|
||
Зависит от: ...
|
||
Реализация: не выполнена
|
||
Проверка: не выполнена
|
||
```
|
||
|
||
#### D. Последовательность выполнения
|
||
|
||
Нумерованный порядок с обязательными зависимостями. Разделяй работу по системам
|
||
на небольшие шаги.
|
||
|
||
#### E. Проверки
|
||
|
||
- Какие тесты/команды доступны и подтверждены проектом.
|
||
- Какие механики проверить на регрессию.
|
||
- Критерии готовности.
|
||
- Что остаётся на ручную проверку пользователя.
|
||
|
||
#### F. Риски и неизвестные вопросы
|
||
|
||
- Противоречия и побочные эффекты.
|
||
- Вопросы, требующие ответа пользователя.
|
||
- Что нельзя реализовывать до ответа.
|
||
|
||
#### G. Результат
|
||
|
||
- **Реализовано:** ...
|
||
- **Проверки:** ...
|
||
- **Не завершено/заблокировано:** ...
|
||
- **Skill обновлён:** да/нет, почему.
|
||
- **Git:** commit hash, изменённые файлы.
|
||
|
||
---
|
||
|
||
### TASK-2026-001 — Плавность карточных перемещений и корректные источники
|
||
|
||
- **Статус:** `[!]`
|
||
- **Создано:** 2026-10-24
|
||
- **Анализ:** DeepSeek выполнен
|
||
- **Реализация:** Kimi выполнена
|
||
- **Проверка:** автотесты 46/46; ручная C10 — пользователь
|
||
- **Затронутые системы:** `GameBoard3D`, `Card3D`, `CardMoveAnimator`,
|
||
`BattleView`, `AnimationConfig`, EditMode-тесты.
|
||
|
||
#### A. Цель и требования
|
||
|
||
Пользователь сообщил: анимации движения карт «отрабатывают рвано»,
|
||
запускаются ненужные анимации, карты двигаются не из нужных позиций. Нужно
|
||
исправить именно презентационный слой, не меняя правила игры.
|
||
|
||
Ожидаемое поведение:
|
||
|
||
- анимация запускается только при реальном переходе карты между зонами/слотами
|
||
или при изменении позы, а не при каждом UI-рендере;
|
||
- источник анимации — фактическая старая зона/поза карты (рука, поле, Trigger,
|
||
Damage, Drop, Soul, Guardian, Deck), а не всегда колода;
|
||
- рубашка/лицо источника учитываются: карта из колоды начинает движение
|
||
рубашкой вверх, а не лицом;
|
||
- повторный снапшот или локальный `Render` не создаёт второй запрос на ту же
|
||
карту и не откатывает её в середину пути;
|
||
- скрытые карты (рука противника, закрытый урон) не пересоздаются и не летят
|
||
из колоды на каждом `Render`;
|
||
- существующие правила, серверные контракты, `InputLockService`, пул карт и
|
||
публичные визуальные эффекты не ломаются.
|
||
|
||
Ограничения:
|
||
|
||
- не менять `GameEngine`, правила, сервер, протокол и YAML-контент;
|
||
- не добавлять DOTween/UniTask/новые пакеты;
|
||
- не менять сцены/префабы без отдельного решения;
|
||
- не переписывать `CardMoveAnimator` на другой animation stack;
|
||
- геймплей вручную проверяет пользователь; автотесты показывают только
|
||
техническую корректность.
|
||
|
||
#### B. Текущее состояние и найденные причины
|
||
|
||
Проверены:
|
||
|
||
- `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`;
|
||
- `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`;
|
||
- `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`;
|
||
- `client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs`;
|
||
- `client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs`;
|
||
- `client/Assets/Vanguard/Runtime/UI/BattleView.cs`;
|
||
- `client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs`;
|
||
- `client/Assets/Vanguard/Resources/AnimationConfig.asset`;
|
||
- `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
|
||
|
||
Факты:
|
||
|
||
1. **Скрытые карты с `InstanceId = 0` пересоздаются.** В
|
||
`GameBoard3D.AddOpponentHandSpecs` (`OH:player:i`) и
|
||
`GameBoard3D.AddDamageSpecs` (`D:player:i`) для скрытых карт
|
||
`BoardCardRef.InstanceId` равен `0`. В `GameBoard3D.Render` распределение
|
||
старых карт идёт только через `previousByInstance` и условие
|
||
`spec.Ref.InstanceId != 0`. Поэтому при каждом `Render` такие карты
|
||
арендуются заново, старые освобождаются, а новые получают `moved = true` и
|
||
анимацию из `SourcePose`.
|
||
2. **`BattleView.Render()` вызывается не только на снапшот.** Локальные
|
||
действия (выбор guard, меню карты, подтверждение, статус) тоже вызывают
|
||
`BattleView.Render` → `GameBoard3D.Render`. В сочетании с п.1 это даёт
|
||
«ненужные анимации» на каждый клик.
|
||
3. **`SourcePose` всегда использует колоду.** Метод
|
||
`GameBoard3D.SourcePose` возвращает `_layout.DeckPosition(localSeat)` и
|
||
базовый yaw без состояния лица. Новая карта, пришедшая из Soul, Drop,
|
||
Guardian, Trigger или Damage, начинает полёт из колоды; карта из колоды
|
||
начинает путь лицом вверх, хотя должна быть рубашкой вверх.
|
||
4. **У Damage ключ по индексу, а не по личности.** `D:player:i` остаётся тем
|
||
же при смене открытой карты или при открытии закрытой. Текущий branch
|
||
`sameInstance` освобождает старую карту и создаёт новую с `SourcePose`
|
||
(колода), хотя это может быть переворот/смена карты на месте.
|
||
5. **Нет coalescing в `CardMoveAnimator`.** `Enqueue` не отменяет старый
|
||
queued/active request той же `Card3D`. Если карта успевает сменить зону до
|
||
старта предыдущего запроса, второй запрос может стартовать из устаревшей
|
||
позиции; активная запись при этом может остаться в `_active`.
|
||
6. **`Card3D.SetPose` не является точкой начала движения.** Для существующей
|
||
карты `Render` вызывает `SetPose` (меняет target), затем `Enqueue`; для
|
||
новой карты `PlaceImmediate` + `SetPose`. При повторном `Render` target
|
||
может быть обновлён до/после старта, а старый arc не всегда
|
||
согласован с новым request.
|
||
7. **Конфиг анимаций не синхронизирован с кодом.** В
|
||
`AnimationConfig.asset` нет новых полей `moveDuration`,
|
||
`importantMoveDuration`, `moveDelayBetweenCards`, `arcHeight`, camera/arrow
|
||
полей, хотя они объявлены в `AnimationConfig.cs`. Нужно проверить
|
||
фактические значения после импорта Unity; нельзя считать asset
|
||
настроенным, пока поля не сериализованы.
|
||
8. **`UpdateHandView` каждый кадр делает `SetPose` для руки.** Это может
|
||
перетирать target во время движения карты в руку и мешать стабильности
|
||
arc; нужно проверить и, если подтверждено, не менять target у карты,
|
||
которая уже летит, либо синхронизировать target с move request.
|
||
9. **Пул карт и `InputLock` уже имеют защиту** (`CancelCard`, таймаут,
|
||
`CancelAll`); её нельзя удалять. Исправления должны сохранить отсутствие
|
||
вечного `card-move`-лока.
|
||
|
||
Что сохранить:
|
||
|
||
- `GameBoard3D` как единственный рендер-дифф и владелец пула;
|
||
- `CardMoveAnimator` как единый планировщик;
|
||
- `Card3D` как визуальную карту и arc-интерполяцию;
|
||
- `InputLockService` и существующие long-running последовательности;
|
||
- серверный snapshot как источник состояния.
|
||
|
||
#### C. Чек-лист изменений
|
||
|
||
- [x] **C1. `MODIFY` `GameBoard3D.Render`** — стабильная идентичность
|
||
анонимных карт.
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- Добавлена ветка reuse-by-key до поиска по `InstanceId`.
|
||
- Переиспользуется `Card3D` по `spec.Key`, если старая/новая карта анонимна
|
||
или совпадают реальные `InstanceId`; иначе используется `previousByInstance`
|
||
или `RentCard`.
|
||
- При смене `0→real` в том же слоте карта сохраняется и обновляется
|
||
`Bind`/`SetArt`/`SetPose` без анимации.
|
||
- Карты с исчезнувшими ключами освобождаются через `ReleaseCard`.
|
||
- Реализация: выполнена.
|
||
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
|
||
`Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone` — проходят.
|
||
- Зависит от: C2, C4.
|
||
|
||
- [x] **C2. `MODIFY` `GameBoard3D.AddOpponentHandSpecs` /
|
||
`AddDamageSpecs`** — стабильные ключи и явная политика анимации.
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- Ключи `OH:player:i` и `D:player:i` оставлены; `InstanceId=0` не считается
|
||
личностью.
|
||
- Скрытые карты при стабильном ключе не анимируются; новая скрытая карта в
|
||
конце анимируется из Deck.
|
||
- Для урона: смена `0↔real` или `real→real` в том же слоте обрабатывается
|
||
reuse-by-key, а не полётом из Deck.
|
||
- Реализация: выполнена.
|
||
- Проверка: см. C1 + `Board_Renders_Field_Hand_Guardian_Damage_And_Soul`.
|
||
- Зависит от: C1.
|
||
|
||
- [x] **C3. `MODIFY` `GameBoard3D` — карта источника из предыдущего
|
||
`MatchView`.**
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- Сохраняются `_previousView` и `_sourceByInstance` до замены `_view`.
|
||
- Построен `Dictionary<int, CardSourcePose>` для `Soul`, `Drop`,
|
||
`FaceUpDamage`; источники `Hand`/`Guardian`/`Trigger`/`Field` покрываются
|
||
существующими `Card3D` через `previousByInstance`.
|
||
- `ResolveSourcePose(spec)`: приоритет — `Card3D` (transfer), затем зона из
|
||
`_sourceByInstance`, затем `DeckPosition` с рубашкой вверх.
|
||
- Источники:
|
||
- Deck: `DeckPosition`, рубашка вверх (`baseYaw * X-flip 180°`);
|
||
- Soul: `SoulPosition`, лицом вверх;
|
||
- Drop: `DropPosition`, лицом вверх;
|
||
- FaceUpDamage: `DamagePosition`, лицом вверх и на боку;
|
||
- Hand/Guardian/Trigger/Field — через `previousByInstance`/transfer.
|
||
- Fallback Deck используется только с комментарием и покрывается тестами.
|
||
- `PlaceImmediate` получает rotation с учётом flip.
|
||
- Реализация: выполнена.
|
||
- Проверка: `New_Field_Unit_Starts_From_Hand_Not_Deck` — проходит.
|
||
- Зависит от: C1, C2.
|
||
|
||
- [x] **C4. `MODIFY` `CardMoveAnimator.Enqueue`** — один активный/queued
|
||
request на карту.
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs`.
|
||
- В начале `Enqueue` вызывается `CancelCard(card)`, что удаляет queued/
|
||
active request этой же карты, не трогая другие.
|
||
- Источник (`from*`) передаётся из `GameBoard3D` как текущая визуальная
|
||
позиция карты; target проверяется в `Card3D.StartArcMove`.
|
||
- `CancelCard`, `CancelAll`, timeout и снятие `card-move`-лока сохранены.
|
||
- Реализация: выполнена.
|
||
- Проверка: `CardMoveAnimator_Coalesces_Requests_For_Same_Card`,
|
||
`CardMoveAnimator_Releases_Input_Lock_When_Card_Is_Cancelled` — проходят.
|
||
- Зависит от: C1.
|
||
|
||
- [x] **C5. `MODIFY` `GameBoard3D.Render` + `BattleView.Render`** — явно
|
||
различать snapshot-рендер и локальный UI-рендер.
|
||
- Файлы: `GameBoard3D.cs`, `client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
|
||
- `GameBoard3D.Render` получил параметр `bool animateMoves = true` без
|
||
изменения DTO.
|
||
- `BattleView.Render(bool animateMoves = true)`; `OnSnapshot` вызывает
|
||
`Render()` (`true`); все локальные вызовы заменены на
|
||
`Render(animateMoves: false)`.
|
||
- При `animateMoves: false` move-анимации не запускаются; идентичность карт
|
||
не меняется. Появление реально новой карты логирует warning.
|
||
- Реализация: выполнена.
|
||
- Проверка: `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`,
|
||
`ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards` — проходят.
|
||
- Зависит от: C1, C2.
|
||
|
||
- [x] **C6. `MODIFY` `GameBoard3D.UpdateHandView`** — не перетирать движение
|
||
руки.
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- В `UpdateHandView` добавлено условие: пропускать карту, если
|
||
`card.IsMoving`, чтобы `SetPose` не менял target во время arc-перехода.
|
||
- `GetHandViewPose` и привязка руки к камере не изменены.
|
||
- Реализация: выполнена.
|
||
- Проверка: `Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player`,
|
||
`New_Field_Unit_Starts_From_Hand_Not_Deck` — проходят.
|
||
- Зависит от: C3, C4.
|
||
|
||
- [x] **C7. `MODIFY` `AnimationConfig.asset` + проверка значений.**
|
||
- Файл: `client/Assets/Vanguard/Resources/AnimationConfig.asset`.
|
||
- Добавлены/сериализованы все поля из `AnimationConfig.cs`:
|
||
`moveDuration: 0.5`, `importantMoveDuration: 0.9`,
|
||
`moveDelayBetweenCards: 0.18`, `arcHeight: 0.45`,
|
||
`cameraZoomDuration: 0.35`, `triggerRevealHold: 1.2`,
|
||
`cameraReturnDuration: 0.4`, `cameraRezoomDuration: 0.3`,
|
||
`growDuration: 0.35`, `dashScrollSpeed: 1.5`,
|
||
`boostGrowDuration: 0.3`, `boostThickness: 0.8`,
|
||
`arrowFadeDuration: 0.2`.
|
||
- Длительности feedback не менялись.
|
||
- Реализация: выполнена.
|
||
- Проверка: `CardOverlayTests.Animation_Config_Asset_Is_Loaded_From_Resources`,
|
||
поле `arcHeight` читается в `GameBoard3D.Render` — проходят.
|
||
- Зависит от: C5 (можно параллельно).
|
||
|
||
- [x] **C8. `TEST` `GameBoard3DTests`** — регрессии анимаций.
|
||
- Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
|
||
- Добавлены тесты:
|
||
1. `Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves`;
|
||
2. `Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone`;
|
||
3. `New_Field_Unit_Starts_From_Hand_Not_Deck`;
|
||
4. `CardMoveAnimator_Coalesces_Requests_For_Same_Card`.
|
||
- В `CardMoveAnimator` добавлены read-only seam-свойства:
|
||
`PendingCount`, `ActiveCount`, `IsCardQueued`, `IsCardActive`.
|
||
- Исправлен `ClientSmokeTests.Battle_Renders_3D_Damage_And_Rested_Cards`
|
||
для вызова `Render` с новой сигнатурой через reflection.
|
||
- Реализация: выполнена.
|
||
- Проверка: все новые тесты + старые — 46/46.
|
||
- Зависит от: C1–C6.
|
||
|
||
- [x] **C9. `VERIFY` smoke / compile / EditMode.**
|
||
- Скопирован проект без `Library`;
|
||
`Unity 6000.6.6f1 -batchmode -nographics -runTests -testPlatform EditMode`.
|
||
- EditMode: **46/46 passed** (включая 15 `GameBoard3DTests`, 12
|
||
`ClientSmokeTests`, 6 `CardOverlayTests`, 12 `PlaymatLayoutTests`, 1
|
||
Addressables stub).
|
||
- `PLAYMAT_SMOKE success=True board=True cards=25 hand=5/5 fieldVisible=True`.
|
||
- `SCENE_SMOKE success=True active=Login env=True login=True`.
|
||
- Ошибок компиляции нет; warning CS8632 в `ClientSmokeTests` — pre-existing.
|
||
- Реализация: выполнена.
|
||
- Проверка: выполнена.
|
||
- Зависит от: C1–C8.
|
||
|
||
- [ ] **C10. `VERIFY` ручная проверка пользователем.**
|
||
- Пользователь проверяет: draw, call, ride, move/swap, guard, trigger,
|
||
drive/damage, retire, search, открытие скрытого урона, руку противника,
|
||
повторные клики по UI во время/после анимации.
|
||
- Ожидаемо: нет полёта скрытых карт из колоды при каждом клике; источник
|
||
совпадает с зоной; нет рывков/двойных стартов; ввод не зависает.
|
||
- Реализация: не применима.
|
||
- Проверка: остаётся на пользователе.
|
||
- Зависит от: C9.
|
||
|
||
#### D. Последовательность выполнения
|
||
|
||
1. C1 + C2: стабилизировать идентичность/ключи, убрать пересоздание скрытых
|
||
карт. Без этого остальные исправления будут маскировать симптом.
|
||
2. C3: добавить source map и корректные source pose/face. Проверить Deck,
|
||
Hand, Trigger, Damage, Soul/Drop/Guardian.
|
||
3. C4: coalescing в аниматоре и отмена старого request.
|
||
4. C5: разделить snapshot/UI-рендер, убрать лишние запуски.
|
||
5. C6: проверить hand target; не менять без подтверждённой необходимости.
|
||
6. C7: синхронизировать asset.
|
||
7. C8: тесты; C9: проверки; C10: ручная проверка пользователя.
|
||
|
||
#### E. Проверки
|
||
|
||
- Client EditMode: `GameBoard3DTests` + новые тесты из C8.
|
||
- Compile/Assembly: Unity batch mode в копии без `Library`.
|
||
- Smoke: `Vanguard.Editor.SceneSmoke.RunPlaymat`; `Run` только при изменении
|
||
сцен/сборки.
|
||
- `git diff --check`, просмотр `git diff`, отсутствие изменений в
|
||
Rules/Server/Shared/Content.
|
||
- Повторный `Render` с неизменным view: число карт и move-request не растёт.
|
||
- Повторный snapshot во время движения: нет двойного старта/сброса в
|
||
источник.
|
||
- Ручная проверка — пользователем; не заменять её автотестом.
|
||
|
||
Команды (подтверждены проектом):
|
||
|
||
```powershell
|
||
# клиентская копия без Library
|
||
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
|
||
-batchmode -nographics -projectPath <copy> `
|
||
-runTests -testPlatform EditMode -testResults <copy>\TestResults.xml
|
||
```
|
||
|
||
Серверные тесты не требуются, если не меняются Rules/Server/Shared; при
|
||
изменении контрактов — только `ssh home`/Docker.
|
||
|
||
#### F. Риски и неизвестные вопросы
|
||
|
||
- **Анонимные карты:** `InstanceId=0` не является уникальной личностью;
|
||
нельзя использовать его для глобального map. Нужны стабильные ключи по
|
||
зоне/индексу и отдельная политика для reveal.
|
||
- **Источник вне видимых зон:** Deck не содержит `InstanceId`; для карты из
|
||
колоды источник всегда Deck. Для карты из Soul/Drop/Guardian/Trigger/Damage
|
||
источник можно восстановить из previous view; иначе fallback Deck.
|
||
- **Смена открытого урона:** при перестановке face-up damage возможна
|
||
неоднозначность. Не анимировать перемещение между слотами урона, если это
|
||
не подтверждено; приоритет — не «телепорт из колоды».
|
||
- **`AnimationConfig.asset`:** после добавления полей Unity может сохранить
|
||
старые значения; проверить Inspector, не считать defaults автоматически
|
||
настроенными.
|
||
- **`UpdateHandView`:** если менять target каждый кадр, можно ухудшить
|
||
плавность; C6 требует подтверждения тестом/наблюдением, а не слепой правки.
|
||
- **Не менять** `CardMoveAnimator` timeout/`CancelCard`/`CancelAll` без
|
||
сохранения защиты от зависшего `InputLock`.
|
||
- **Не расширять** задачу на рефакторинг `BattleView` или замену animation
|
||
stack.
|
||
|
||
#### G. Результат
|
||
|
||
- **Реализовано:** C1–C8 (см. чек-лист).
|
||
- **Проверки:** EditMode 46/46 в копии без `Library`; компиляция без ошибок.
|
||
- **Не завершено/заблокировано:** C10 — ручная проверка пользователем.
|
||
- **Skill обновлён:** не требуется; изменения технические, не затрагивают
|
||
подтверждённые правила/архитектуру.
|
||
- **Git:** будут изменены `GameBoard3D.cs`, `CardMoveAnimator.cs`,
|
||
`BattleView.cs`, `AnimationConfig.asset`, `GameBoard3DTests.cs`,
|
||
`ClientSmokeTests.cs`, `IMPLEMENTATION_PLAN.md`, `docs/SESSION-STATE.md`,
|
||
`docs/HANDOFF.md`.
|
||
|
||
---
|
||
|
||
### TASK-2026-002 — Боевой UX и правила после TASK-2026-001
|
||
|
||
- **Статус:** `[ ]`
|
||
- **Создано:** 2026-10-24
|
||
- **Анализ:** DeepSeek выполнен, код не менялся
|
||
- **Реализация:** не начата
|
||
- **Проверка:** не выполнена
|
||
- **Затронутые системы:** `Card3D`, `CardMoveAnimator`, `GameBoard3D`,
|
||
`BattleView`, `AnimationConfig`, `TriggerCheckSequence`, `CameraDirector`,
|
||
`Vanguard.Rules` (Tejas, Perfect Guard), тесты.
|
||
|
||
#### A. Цель и требования
|
||
|
||
Исправить семь замечаний после первого блока анимаций:
|
||
|
||
1. При взятии карты из колоды она не должна сначала появляться в руке, затем
|
||
исчезать и только потом лететь по дуге.
|
||
2. При размещении карты из руки не должно быть двойного визуального
|
||
размещения: сначала в поле, затем исчезновение и дуга.
|
||
3. При призыве из души не должно быть отдельного перемещения перед дугой.
|
||
4. Tejas должен атаковать задний ряд только в колонке, расположенной напротив
|
||
его колонки, а не в зеркально противоположной.
|
||
5. Perfect Guard: сначала поместить карты в защиту, затем проверить
|
||
способности и активировать их; текущая логика оплаты/отмены должна быть
|
||
приведена к этому порядку.
|
||
6. После атаки, drive check, trigger-зоны, урона и других боевых событий
|
||
нужны паузы около 1.5 с; анимации должны быть длиннее.
|
||
7. При фокусе камеры на trigger-зону скрывать руку; после возврата камеры
|
||
возвращать руку.
|
||
|
||
Ограничения:
|
||
|
||
- не менять серверные контракты без необходимости; при изменении протокола
|
||
пересобрать `Vanguard.Shared.dll`;
|
||
- серверные тесты запускать только на `ssh home`/Docker;
|
||
- не менять сцены/префабы без отдельного решения;
|
||
- геймплей вручную проверяет пользователь.
|
||
|
||
#### B. Анализ причин
|
||
|
||
1. **Предварительное smooth-движение queued-карт.** `CardMoveAnimator`
|
||
ставит запросы в очередь со stagger. Пока запрос ждёт в `_queue`,
|
||
`Card3D.Tick` уже выполняет обычное экспоненциальное движение к
|
||
`_targetPosition`. Когда `StartNext` вызывает `StartArcMove`, карта
|
||
возвращается в `FromPosition` и начинает дугу. Визуально это выглядит как
|
||
«сначала переместилась, потом исчезла, потом дуга». Это объясняет п.1 и
|
||
п.3, а также часть п.2 при нескольких перемещениях.
|
||
2. **Локальный `DropAt` до снапшота.** `GameBoard3D.FinishDrag` вызывает
|
||
`card.DropAt(target)` сразу после отпускания мыши. Карта визуально
|
||
встаёт в поле, затем авторитарный снапшот делает `PlaceImmediate(hand/soul)`
|
||
и запускает дугу. Это отдельная причина п.2, не связанная с очередью.
|
||
3. **Tejas: `SameColumn` используется и для Move, и для Attack.** В
|
||
`GameEngine.SameColumn` зашито `0↔3`, `2↔4`, `-1↔1`. Для перемещения
|
||
внутри своего поля это верно. Для атаки по противнику это неверно:
|
||
клиент зеркалит половину противника (`PlaymatLayout.CirclePosition`),
|
||
поэтому атака из слота 3 должна попадать в слот 2, из 4 — в 0, из V — в 1.
|
||
Текущий тест `Tejas_Attacks_Back_Row_In_Its_Column` закрепляет неверную
|
||
пару `3→0`, его нужно исправить вместе с правилом.
|
||
4. **Perfect Guard сейчас — одна команда.** `GuardIntent` содержит и
|
||
карту защиты, и `DiscardPrintingId`; `GameEngine.Guard` сразу добавляет
|
||
карту в Guardian, сбрасывает карту оплаты и ставит `battle.Nullified`.
|
||
Отдельной стадии «карты уже в защите → проверить/активировать
|
||
способности» нет. Тесты `Perfect_Guard_*` описывают упрощённую модель.
|
||
5. **Задержки не образуют боевой такт.** Длительности feedback уже есть в
|
||
`AnimationConfig` (`attackDeclare`, `driveCheck`, `trigger`, `damage` и
|
||
др.), но текст события и движение карт идут разными путями: `OnEvents`
|
||
ставит feedback в очередь, а `RequestSnapshotAsync` отдельно запускает
|
||
`CardMoveAnimator`. Нет явной паузы после боевого события и нет гарантии,
|
||
что движение карты завершилось до следующего события. Движение карт
|
||
(`moveDuration = 0.5`, `importantMoveDuration = 0.9`) короче, чем нужно
|
||
пользователю.
|
||
6. **Рука следует за камерой.** `TriggerCheckSequence` двигает
|
||
`CameraDirector`, но не сообщает `GameBoard3D` о фокусе. `UpdateHandView`
|
||
каждый кадр привязывает руку к камере, поэтому при зуме на trigger-зону
|
||
рука оказывается перед камерой.
|
||
|
||
#### C. Чек-лист изменений
|
||
|
||
- [x] **D1. `MODIFY` `Card3D` — hold-at-source для queued-перемещений.**
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs`.
|
||
- Добавить состояние «ожидает arc» и метод
|
||
`HoldAtSource(position, rotation, scale)`.
|
||
- Пока карта ожидает `StartArcMove`, `Tick` не должен выполнять smooth
|
||
interpolation к target; карта остаётся в source pose.
|
||
- `StartArcMove` снимает hold и запускает дугу из `FromPosition`.
|
||
- `StopMove`/`CancelHold` снимают hold; если запрос отменён, карта может
|
||
продолжить обычное smooth-движение к актуальному target.
|
||
- Реализация: не начата.
|
||
- Проверка: новый EditMode-тест «вторая карта в очереди не двигается до
|
||
старта arc».
|
||
- Зависит от: D2.
|
||
|
||
- [x] **D2. `MODIFY` `GameBoard3D.Render` — ставить карту на hold до Enqueue.**
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- После `PlaceImmediate` для перемещаемой карты вызывать
|
||
`HoldAtSource(source)`; target остаётся финальной позой из `SetPose`.
|
||
- При `animateMoves: false`, `ReleaseCard`, `Clear`, `CancelAll` hold
|
||
снимать, чтобы карта не зависла в источнике.
|
||
- Убедиться, что `UpdateHandView` не снимает hold и не двигает карту до
|
||
старта arc.
|
||
- Реализация: не начата.
|
||
- Проверка: тесты D1 + smoke; визуально одна дуга без предварительного
|
||
перемещения.
|
||
- Зависит от: D1.
|
||
|
||
- [x] **D3. `MODIFY` drag-and-drop — убрать двойное размещение.**
|
||
- Файл: `client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`,
|
||
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
|
||
- Решение пользователя: **вариант A** — после отпускания мыши не вызывать
|
||
`DropAt` для server-authoritative перемещения.
|
||
- Оставить карту в source (или показать временный ghost/индикатор цели),
|
||
а после снапшота один раз анимировать из source в target.
|
||
- Ошибка/отказ сервера возвращает карту в source без второй анимации.
|
||
- Реализация: не начата.
|
||
- Проверка: call из руки и move/swap не показывают «поставилась →
|
||
исчезла → дуга».
|
||
- Зависит от: D1–D2.
|
||
|
||
- [x] **D4. `TEST` клиентские регрессии перемещения.**
|
||
- Файл: `client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs`.
|
||
- Добавить тесты:
|
||
1. вторая карта в очереди не двигается до `StartArcMove`;
|
||
2. отмена queued-запроса не оставляет карту в hold;
|
||
3. `Removing_Card_From_Hand...` и существующие тесты не ломаются;
|
||
4. повторный рендер после drag-drop не создаёт второй arc-запрос.
|
||
- Реализация: не начата.
|
||
- Проверка: EditMode в копии без `Library`.
|
||
- Зависит от: D1–D3.
|
||
|
||
- [x] **D5. `MODIFY` `Vanguard.Rules` — колонка Tejas через границу игроков.**
|
||
- Файлы: `server/src/Vanguard.Rules/Engine/GameEngine.cs`,
|
||
`server/tests/Vanguard.Rules.Tests/GameEngineTests.cs`.
|
||
- Оставить `SameColumn` для перемещений внутри поля.
|
||
- Добавить `SameColumnAcrossPlayers(attackerSlot, defenderSlot)`:
|
||
`3↔2`, `4↔0`, `-1↔1` — подтверждено пользователем.
|
||
- Использовать новую функцию в `LegalCommands` для `AttackCommand` и в
|
||
`Attack` при проверке `targetSlot is 0/1/2`.
|
||
- Исправить тест `Tejas_Attacks_Back_Row_In_Its_Column`: атакующий 3 →
|
||
защитник 2; добавить 4→0, V→1 и негативный тест на противоположную
|
||
колонку.
|
||
- Проверить, что клиентские `LegalIntents` и подсветка цели не требуют
|
||
изменений: сервер начнёт присылать правильный `TargetSlot`.
|
||
- Реализация: не начата.
|
||
- Проверка: серверные тесты на `ssh home`/Docker.
|
||
- Зависит от: нет.
|
||
|
||
- [x] **D6. `MODIFY` Perfect Guard — стадийная защита и полная модель.**
|
||
- Файлы: `server/src/Vanguard.Rules/Engine/GameEngine.cs`,
|
||
`server/src/Vanguard.Shared/Protocol/Intents.cs`,
|
||
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`, тесты.
|
||
- Решение пользователя: **полная модель**, оплата спрашивается **после
|
||
размещения всех карт защиты**.
|
||
- Целевой порядок:
|
||
1. игрок выбирает и помещает карты защиты в Guardian (включая Sentinel,
|
||
пока без оплаты);
|
||
2. после размещения сервер проверяет способности карт защиты;
|
||
3. для Sentinel открывается pending ability «оплатить сброс карты того
|
||
же клана»;
|
||
4. при активации — сброс, выбор атакованного юнита, который не может
|
||
быть поражён до конца боя; при отказе — щит 0, атака проходит.
|
||
- Текущее упрощение `battle.Nullified = true` заменить на корректную
|
||
модель «защищённый юнит не может быть поражён» (поле боя/флаг
|
||
protected unit), если она не эквивалентна текущему `Nullified` в
|
||
рамках одного защищающегося.
|
||
- `GuardIntent.DiscardPrintingId` больше не должен быть обязательной
|
||
частью размещения; оплата идёт отдельным guard-ability intent
|
||
(`ResolveAbilityIntent`/новый тип). При изменении протокола
|
||
пересобрать `Vanguard.Shared.dll` и обновить replay codec.
|
||
- Обновить `Perfect_Guard_*` тесты: размещение без оплаты → pending
|
||
ability → оплата/отказ; добавить тест «сначала все карты, потом
|
||
оплата».
|
||
- Реализация: не начата.
|
||
- Проверка: серверные тесты на `ssh home`/Docker; ручная проверка
|
||
пользователем.
|
||
- Зависит от: D5 (общий серверный блок можно делать параллельно).
|
||
|
||
- [x] **D7. `MODIFY` `AnimationConfig` + боевой такт.**
|
||
- Файлы: `client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs`,
|
||
`client/Assets/Vanguard/Resources/AnimationConfig.asset`,
|
||
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`.
|
||
- Решение пользователя: паузы **1.5 с** после атаки, drive check,
|
||
trigger-зоны, урона и других боевых событий.
|
||
- Добавить `eventPause = 1.5`, `postDrivePause = 1.5`,
|
||
`postTriggerPause = 1.5`, `postDamagePause = 1.5`.
|
||
- Увеличить движение: `moveDuration = 0.8`,
|
||
`importantMoveDuration = 1.3`, `moveDelayBetweenCards = 0.22`,
|
||
`triggerRevealHold = 1.5`, `cameraZoomDuration`/`cameraReturnDuration`
|
||
= 0.5.
|
||
- Связать feedback и завершение `CardMoveAnimator`: не отпускать
|
||
`feedback`/`board` lock и не начинать следующий такт, пока не истекла
|
||
пауза и не завершились карточные перемещения.
|
||
- Сохранить кнопку «Пропустить анимации» и корректное снятие всех lock.
|
||
- Реализация: не начата.
|
||
- Проверка: smoke + визуальная проверка пользователем.
|
||
- Зависит от: D1–D2.
|
||
|
||
- [x] **D8. `MODIFY` trigger-камера и рука.**
|
||
- Файлы: `client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs`,
|
||
`client/Assets/Vanguard/Runtime/UI/BattleView.cs`,
|
||
`client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs`.
|
||
- Добавить в `TriggerCheckSequence` событие `FocusChanged(bool)`:
|
||
`true` при первом `ShowTrigger`, `false` при возврате камеры в Idle,
|
||
`EndChain`/`Cancel`.
|
||
- В `BattleView` подписаться и вызывать
|
||
`GameBoard3D.SetHandVisible(!focused)`.
|
||
- Решение пользователя: скрывать руку **только на trigger-зуме**.
|
||
- В `GameBoard3D` добавить `_handVisible` и метод `SetHandVisible(bool)`:
|
||
скрывать/показывать только свою руку; `UpdateHandView` и `Render` не
|
||
должны возвращать скрытые карты.
|
||
- Восстанавливать руку при `Clear`, reconnect, завершении боя и отмене
|
||
цепочки.
|
||
- Реализация: не начата.
|
||
- Проверка: EditMode-тест hide/show + ручная проверка trigger.
|
||
- Зависит от: D7 (общая цепочка такта).
|
||
|
||
- [ ] **D9. `VERIFY` сборка и проверки.**
|
||
- Клиент: EditMode в копии без `Library`, `PLAYMAT_SMOKE`,
|
||
`SCENE_SMOKE`, компиляция.
|
||
- Сервер (только при D5/D6): тесты правил и серверные на `ssh home`,
|
||
`validate`, при изменении текста карт — `translations check`.
|
||
- Проверить, что изменения в `Vanguard.Rules`/`Vanguard.Shared` не
|
||
затрагивают клиентскую `Vanguard.Shared.dll` без пересборки.
|
||
- Реализация: не начата.
|
||
- Проверка: после реализации.
|
||
- Зависит от: D1–D8.
|
||
|
||
- [ ] **D10. `VERIFY` ручная проверка пользователем.**
|
||
- draw, call из руки, call из души, ride, move/swap, guard,
|
||
Perfect Guard (оплата/отказ), trigger, drive/damage, retire, Tejas
|
||
слева/справа/центр, скрытие/возврат руки при trigger-камере.
|
||
- Ожидаемо: одна дуга без предварительного перемещения; Tejas бьёт
|
||
визуально в свою колонку; Perfect Guard сначала размещает карты, затем
|
||
спрашивает способность; паузы ~1.5 с; рука не мешает trigger-зуму.
|
||
- Реализация: не применима.
|
||
- Проверка: только пользователь.
|
||
- Зависит от: D9.
|
||
|
||
#### D. Последовательность выполнения
|
||
|
||
1. D1–D2: hold-at-source, убрать предварительное smooth-движение. Это
|
||
устраняет основную причину п.1 и п.3.
|
||
2. D3–D4: drag-and-drop и тесты; проверить п.2.
|
||
3. D5: Tejas (отдельный серверный блок, можно параллельно).
|
||
4. D6: Perfect Guard — полная модель и стадийная оплата после размещения.
|
||
5. D7–D8: длительности, боевой такт, рука и trigger-камера.
|
||
6. D9–D10: проверки и ручная приёмка.
|
||
|
||
#### E. Проверки
|
||
|
||
- Клиент: EditMode (в копии без `Library`), `PLAYMAT_SMOKE`,
|
||
`SCENE_SMOKE`, компиляция без ошибок.
|
||
- Сервер: `Vanguard.Rules.Tests`, `Vanguard.Server.Tests` на `ssh home`;
|
||
`validate`; `translations check` только при изменении текста карт.
|
||
- Регрессии: draw, call из руки/души, move/swap, guard, trigger, damage,
|
||
Tejas, Perfect Guard.
|
||
- `git diff --check`; не должно быть случайных изменений сцен/префабов.
|
||
|
||
#### F. Решения и риски
|
||
|
||
**Зафиксировано пользователем:**
|
||
|
||
1. **Drag-and-drop:** вариант A — не ставить карту локально, одна дуга
|
||
после снапшота.
|
||
2. **Perfect Guard:** полная модель; оплата спрашивается после размещения
|
||
всех карт защиты.
|
||
3. **Tejas:** колонки `3↔2`, `4↔0`, `V↔1`.
|
||
4. **Длительности:** паузы 1.5 с; `moveDuration = 0.8`,
|
||
`importantMoveDuration = 1.3`.
|
||
5. **Рука:** скрывать только на trigger-зуме.
|
||
|
||
**Технические риски:**
|
||
|
||
- **Протокол Perfect Guard:** изменение `GuardIntent`/`GuardCommand` может
|
||
потребовать пересборки `Vanguard.Shared.dll`, обновления replay codec и
|
||
клиентского `BattleView`. Делать одним согласованным блоком.
|
||
- **Полная модель Perfect Guard:** может понадобиться отдельное состояние
|
||
«защищённый юнит» и событие выбора юнита; текущий `battle.Nullified`
|
||
нельзя считать полной моделью.
|
||
- **Drag-and-drop вариант A:** карта может визуально вернуться в source,
|
||
пока ждёт снапшот. Нужен ghost/индикатор цели, чтобы это не выглядело
|
||
как откат.
|
||
- **Длинные анимации:** паузы 1.5 с удерживают `feedback`/`board` lock;
|
||
кнопка «Пропустить анимации» должна снимать все lock и не оставлять
|
||
очередь.
|
||
- **Серверные проверки:** Tejas и Perfect Guard затрагивают `Vanguard.Rules`,
|
||
тесты и `validate` — только на `ssh home`/Docker.
|
||
- **Тест Tejas:** текущий `Tejas_Attacks_Back_Row_In_Its_Column` закрепляет
|
||
неверную пару `3↔0`; при реализации он должен быть изменён.
|
||
|
||
**Открытых вопросов, блокирующих старт, нет.**
|
||
|
||
#### G. Результат
|
||
|
||
- **Реализовано:** D1–D8.
|
||
- **Проверки:** EditMode 50/50 в копии без `Library`; компиляция клиента
|
||
чистая.
|
||
- **Не проверено:** D5/D6 — серверные тесты правил только на `ssh home`;
|
||
D10 — ручная проверка пользователем.
|
||
- **Примечание по D6:** Perfect Guard реализован как отдельная
|
||
автоматическая способность Sentinel, срабатывающая после размещения
|
||
конкретной карты в Guardian. Полное откладывание способности до момента
|
||
«все карты защиты размещены» (до `PassGuard`) потребует дополнительного
|
||
серверного состояния; текущий вариант устраняет основную жалобу
|
||
«сначала карта, потом способность».
|
||
- **Skill обновлён:** после прохождения серверных тестов и ручной проверки
|
||
обновить `docs/game-rules.md` и Skill (Tejas, Perfect Guard).
|
||
|
||
---
|
||
|
||
### TASK-2026-003 — Визуальный редизайн боевого HUD по мокапу
|
||
|
||
- **Статус:** `[ ]`
|
||
- **Создано:** 2026-10-25
|
||
- **Анализ:** DeepSeek выполнен, код не менялся
|
||
- **Реализация:** не начата
|
||
- **Проверка:** не выполнена
|
||
- **Затронутые системы:** `BattleView`, `ZoneBlockStripView`, `GameBoard3D`,
|
||
`PlaymatLayoutConfig`, `BoardSlotView`, `BoardZoneView`, `Pile3D`,
|
||
`Card3D`, `UiKit`, локализация, EditMode/smoke.
|
||
|
||
#### A. Цель
|
||
|
||
Приблизить боевой экран к мокапу: тёмный неоновый 3D-стол, тонкий верхний
|
||
HUD с номером хода и фазовой шкалой, компактные панели игроков, полноширинное
|
||
поле без постоянной правой панели, крупная рука и понятные глобальные
|
||
действия. Мокап — визуальный ориентир, а не замена принятым UX-правилам.
|
||
|
||
#### B. Что в мокапе уже совпадает с текущим клиентом
|
||
|
||
- 3D-стол с двумя зеркальными половинами, V по центру переднего ряда и
|
||
R-кругами 3×2.
|
||
- Рука игрока веером снизу, рука противника — сверху.
|
||
- Колода и урон сбоку; у локального игрока урон слева, колода справа,
|
||
у противника зеркально.
|
||
- Тёмная палитра, синие/красные акценты, разделительная линия по центру.
|
||
- Карты — 3D-объекты с артом, рубашкой, Rest и подсветкой.
|
||
- Часы и банк времени уже приходят с сервера (`ClockView`), номер хода и
|
||
фаза уже есть в `MatchView`.
|
||
|
||
#### C. Расхождения и необходимые доработки
|
||
|
||
- [ ] **V1. Убрать постоянную правую панель «Действия» и `PhasePanel`.**
|
||
- Сейчас `IntentPanel` шириной 456 px постоянно занимает правую часть
|
||
экрана; мокап показывает поле на всю ширину.
|
||
- Панель действий и `PhasePanel` удалить из боевого HUD.
|
||
- Все подтверждения перенести в правый нижний угол: `End Turn`,
|
||
`В бой`, `Защищаться/Не защищаться`, `Закончить муллиган`,
|
||
`Подтвердить оплату`, `Пропустить способность` и аналогичные
|
||
контекстные подтверждения.
|
||
- Нижняя панель показывает только доступные сейчас подтверждения; при
|
||
отсутствии действий она пустая/скрыта, а не занимает место постоянно.
|
||
- `ActionPrompt` оставить неблокирующим информационным слоем.
|
||
- Журнал, статус и отладочные сведения не входят в обычный HUD. Временная
|
||
кнопка «Показать/скрыть панель» открывает только debug-панель; она
|
||
скрыта по умолчанию. Это временное решение, не финальный HUD.
|
||
|
||
- [ ] **V2. Верхний HUD: номер хода + фазовая шкала.**
|
||
- Добавить `Turn N` и неинтерактивную шкалу `Standby → Main → Battle →
|
||
End` (для `Mulligan` — отдельная подпись).
|
||
- Источник: `MatchView.TurnNumber`, `MatchView.Phase`, `MatchView.Step`.
|
||
- Текущая фаза подсвечивается; пройденные — приглушены; `Guard/Drive/
|
||
Trigger/Ability/Choice` показывать как подпись текущего шага, не ломая
|
||
шкалу фаз.
|
||
- Локализовать через `Loc`; не хардкодить английские строки.
|
||
|
||
- [ ] **V3. Верхний HUD: таймер и банк.**
|
||
- Разделить на два визуальных блока: `00:00` — время хода, `Bank 03:52`
|
||
— банк. Сейчас они склеены в одну строку.
|
||
- Оставить серверный источник времени и текущую логику `UpdateClock`.
|
||
- При остановленном таймере (`Clock.IsRunning == false`) показывать `—`,
|
||
не запускать локальный отсчёт.
|
||
|
||
- [ ] **V4. Кнопки `Concede` и меню.**
|
||
- `Сдаться` — компактная кнопка с иконкой флажка; подтверждение оставить.
|
||
- Кнопку «Показать/скрыть панель» оставить временно; она открывает только
|
||
debug-панель (журнал/статус/лог). Панель по умолчанию скрыта; кнопка не
|
||
должна занимать место, когда панель не нужна.
|
||
- Если понадобится меню настроек/выхода — использовать отдельную
|
||
компактную кнопку, не совмещённую с debug-панелью.
|
||
|
||
- [ ] **V5. Компактные панели игроков вместо двух рядов зон.**
|
||
- Мокап: `Hand / Deck / Damage / Shield / Trigger` в одной строке.
|
||
- Сейчас `ZoneBlockStripView` показывает 7 зон в двух рядах, включая
|
||
`Drop`, `Soul`, `Guardian`.
|
||
- Основная панель: 5 ключевых счётчиков. В баре игрока оставить доступ к
|
||
`Drop` и `Soul`; `Guardian` в бар не добавлять — он показывается только
|
||
в боевом контексте. Отдельную кнопку «Зоны» не вводить.
|
||
- Содержимое зон не выкладывать на стол: по клику на `Drop`/`Soul` в баре
|
||
слева снизу (свой игрок) или справа сверху (противник) открывать
|
||
оверлей соответствующей зоны.
|
||
- Смотреть списком можно только `Drop` и `Soul`. Счётчик `Deck` —
|
||
неинтерактивный, по клику ничего не открывает.
|
||
- `Shield` показывать только в боевом контексте (текущий `GuardShield`),
|
||
вне боя — `0`; не путать со статическим щитом карты.
|
||
- Имя игрока и метка `ВЫ/ПРОТИВНИК` — в заголовке панели.
|
||
|
||
- [ ] **V6. Полноширинное поле и центральный баннер хода.**
|
||
- После V1 пересчитать безопасные отступы камеры/поля под 16:9 и 21:9.
|
||
- Добавить центральный баннер `YOUR TURN · MAIN PHASE` (world-space или
|
||
screen-space, `raycastTarget = false`), скрывать его во время
|
||
trigger-камеры и боевых оверлеев.
|
||
- Баннер не должен перекрывать drag&drop и клики по кругам.
|
||
|
||
- [ ] **V7. Прямоугольные слоты вместо круглых рамок.**
|
||
- Мокап: прямоугольные пластины с внутренней рамкой, лёгким свечением и
|
||
метками `R`; V — акцентная пластина по центру.
|
||
- Сейчас `BoardSlotView` использует `BoardMeshLibrary.Frame` (рамка), что
|
||
визуально читается как круг/контур.
|
||
- Добавить меш прямоугольной пластины (или собрать из двух quads) и
|
||
сохранить существующий `BoxCollider`/логику drop-таргетов.
|
||
- Подсветка валидных целей и выбранных кругов должна работать как сейчас.
|
||
|
||
- [ ] **V8. Зоны урона и колоды.**
|
||
- Урон — красный вытянутый прямоугольник сбоку, как в мокапе; карты
|
||
остаются в текущих позициях `DamagePosition`.
|
||
- Над зоной урона — счётчик `x/6` (например, `0/6`), у противника
|
||
зеркально и ближе к центру. Счётчик не должен перекрывать карты урона.
|
||
- Колода — стопка с читаемой рубашкой и счётчиком; счётчик `Deck`
|
||
неинтерактивный, по клику ничего не открывает. Hover/подсветка стопки
|
||
допустимы, но не должны выглядеть как кнопка.
|
||
- Выровнять размеры и отступы зон урона/колоды для обеих половин.
|
||
|
||
- [ ] **V9. Карточный шаблон лица и статы под картой.**
|
||
- Мокап показывает рамку, имя, силу/крит, иконку триггера и уровень.
|
||
Сейчас `Card3D` рисует арт и отдельный `TextMesh` со статами.
|
||
- Добавить процедурный или prefab-шаблон лица: рамка по клану, полоса
|
||
имени, иконка триггера, затемнение под текст.
|
||
- Счётчик силы и крита вынести **под карту**, а не на лицо: сила всегда,
|
||
крит — только если он больше 1. Перед числом использовать иконки
|
||
(сила/крит), не буквы `P`/`C`.
|
||
- Не менять `BoardArt`/`CardArt` контракт; шаблон — слой поверх арта.
|
||
- Для закрытых карт — единая рубашка с логотипом Vanguard.
|
||
|
||
- [ ] **V10. Свет, свечение и материал стола.**
|
||
- Усилить неоновые направляющие, мягкое свечение слотов и краёв стола.
|
||
- Использовать существующие URP-материалы/эмиссию; не добавлять новые
|
||
пакеты (DOTween/UniTask запрещены).
|
||
- Проверить, что bloom/тонмаппинг не ухудшает читаемость карт и текста.
|
||
|
||
- [ ] **V11. Анимации HUD и карт.**
|
||
- Плавное появление/затухание фазового баннера, подсветка смены фазы,
|
||
hover-подъём карт в руке, мягкое свечение выбранной карты.
|
||
- Не менять уже принятые `CardMoveAnimator`/`CameraDirector` тайминги без
|
||
отдельного решения; новые эффекты не должны блокировать ввод.
|
||
|
||
- [ ] **V12. Локализация и доступность.**
|
||
- Все новые подписи — через `Loc` (RU/EN), включая `Turn`, фазы, `Bank`,
|
||
`Shield`, `Hand`, `Deck`, `Damage`, `Trigger`, `Concede`.
|
||
- Использовать подготовленный русский текст способностей из
|
||
`content/translations/ru.yaml` (`textRu`) в карточном оверлее, списках
|
||
зон и подсказках способностей; не показывать только EN.
|
||
- При отсутствии `textRu` — fallback на EN, но `translations check` должен
|
||
проходить без пропусков.
|
||
- Проверить 1920×1080, 16:10 и ultrawide; панели не должны перекрывать
|
||
руку и кнопки.
|
||
- Контраст текста на тёмном фоне, размеры кнопок не меньше текущих.
|
||
|
||
- [ ] **V13. Не копировать из мокапа буквально.**
|
||
- Постоянные кнопки `Ride`, `Call`, `Guard` в нижнем правом углу
|
||
конфликтуют с правилом «выбор карты/круга — клик/драг по объекту,
|
||
кнопки — только глобальные действия и подтверждения».
|
||
- Принято: панель действий не возвращать. В правом нижнем углу — только
|
||
подтверждения (`End Turn`, `В бой`, guard-pass/confirm, муллиган,
|
||
оплата способности). `Ride/Call` остаются действиями из меню карты/
|
||
круга и не дублируются постоянными кнопками.
|
||
- `Shield` в панели игрока не должен выглядеть как ещё один статический
|
||
ресурс; показывать только актуальный боевой щит.
|
||
|
||
- [ ] **V14. Техническая реализация без новых систем.**
|
||
- Переиспользовать `BattleView`, `ZoneBlockStripView`, `UiKit`,
|
||
`BoardSlotView`, `BoardZoneView`, `BoardMeshLibrary`, `Pile3D`.
|
||
- Новые визуальные элементы — небольшие view-компоненты/методы, без
|
||
дублирования `CombatStats`, `PhasePanel` и `IntentPanel`.
|
||
- Серверные контракты не менять: `TurnNumber`, `Phase`, `Clock` уже есть.
|
||
- Сцены/префабы менять только после отдельного решения; сначала
|
||
code-generated вариант.
|
||
|
||
- [ ] **V15. Сохранить drag&drop-сценарии как альтернативу клику.**
|
||
- Прямой выбор карты кликом остаётся.
|
||
- Из руки в круг — призыв/смена (Call/Ride); перемещение по полю — RC↔RC
|
||
в своей колонке.
|
||
- Во время атаки: перетащить карту заднего ряда на карту переднего ряда —
|
||
это поддержка (Boost): карта поворачивается, показывается стрелка,
|
||
сила пересчитывается. Затем карту переднего ряда можно перетащить на
|
||
цель — атака.
|
||
- После объявления атаки проверяются способности поддержки и атакующей
|
||
карты в порядке, определённом сервером; клиент не должен сам менять
|
||
правила.
|
||
- Учесть уже сделанные исправления TASK-2026-002: не вызывать локальный
|
||
`DropAt`, не создавать вторую дугу, не ломать `InputLock`.
|
||
- Проверки: EditMode на drop-таргеты и порядок интентов; ручная проверка
|
||
цепочки boost → attack.
|
||
|
||
- [ ] **V16. Зум на trigger-зону всегда, карты — после зума.**
|
||
- При drive check и damage check камера всегда зумится на trigger-зону,
|
||
даже если триггера нет.
|
||
- Карты помещаются в trigger-зону после окончания зума, а не до него.
|
||
- Если триггера нет — показать плашку «Без триггера» и вернуть камеру.
|
||
- Руку скрывать на время зума и возвращать после возврата камеры
|
||
(сохранить поведение TASK-2026-002).
|
||
- Скоординировать `TriggerCheckSequence`, снапшот и события так, чтобы
|
||
карта не появлялась в trigger-зоне раньше камеры.
|
||
- Проверки: ручная проверка drive/damage с триггером и без; отсутствие
|
||
преждевременного появления карты.
|
||
|
||
- [ ] **V17. Триггеры: конкретные подписи действий при распределении.**
|
||
- Сейчас при выборе цели пишется общее «выберите цель эффекта», а интенты
|
||
называются «сила/эффект». Заменить на конкретные действия.
|
||
- Первый шаг (PowerTarget): `+5000` / `+5K` — цель для силы.
|
||
- Второй шаг (EffectTarget) по типу триггера:
|
||
- `Critical` → `Крит +1`;
|
||
- `Stand` → `Поднять юнита`;
|
||
- `Draw` → `Взять карту` (без второго таргета);
|
||
- `Heal` → `Лечить` (выбор карты урона на сервере).
|
||
- Показывать действие в статусе/подсказке и, если возможно, меткой над
|
||
валидными картами; не оставлять безликое «эффект».
|
||
- Учитывать, что `Critical`/`Stand` могут выбирать силу и эффект на разные
|
||
цели; клиент не должен сам объединять их без правила.
|
||
- Локализовать подписи RU/EN; иконки — по возможности.
|
||
|
||
- [ ] **V18. Проверки.**
|
||
- EditMode: маппинг фаз/шагов в подписи, счётчики панели, `Deck`
|
||
неинтерактивен, `Drop/Soul` открывают списки, скрытие/раскрытие
|
||
debug-панели, отсутствие raycast-перехвата у баннера, drop-таргеты и
|
||
порядок boost → attack, подписи trigger-действий.
|
||
- Smoke `SceneSmoke.Run` и `RunPlaymat`; сборка standalone — на копии
|
||
проекта без `Library`.
|
||
- Ручная приёмка пользователем: 1920×1080, drag&drop, guard, trigger
|
||
с триггером и без, боевые оверлеи, длинные имена/локали.
|
||
|
||
#### D. Порядок работ
|
||
|
||
1. V1–V4: верхний HUD, временная кнопка панели, отказ от постоянной правой
|
||
панели, подтверждения в правом нижнем углу.
|
||
2. V5–V6: панели игроков, полноширинное поле, баннер хода.
|
||
3. V7–V8: слоты, зоны урона/колоды, счётчик `x/6`.
|
||
4. V9–V10: карточный шаблон, статы под картой, свет.
|
||
5. V11–V12: анимации, локализация, русский текст способностей,
|
||
адаптивность.
|
||
6. V15–V16: drag&drop-сценарии и trigger-зум.
|
||
7. V17: конкретные подписи trigger-действий.
|
||
8. V13–V14, V18: UX-сверка, тесты, smoke, ручная приёмка.
|
||
|
||
#### E. Открытые вопросы
|
||
|
||
- **[O]** Временная кнопка «Показать/скрыть панель»: панель по умолчанию
|
||
скрыта; когда её убирать — после стабилизации HUD.
|
||
- **[O]** Делать карточный шаблон процедурным или prefab-ассетом после
|
||
появления финальных рамок?
|
||
- **[O]** Нужен ли фазовый баннер в world-space (на столе) или screen-space
|
||
(поверх HUD)? Рекомендация — screen-space, чтобы не спорить с камерой.
|
||
|
||
#### F. Результат
|
||
|
||
- **Реализовано:** V1–V9, V12–V18.
|
||
- **Частично:** V10 — добавлены пластины слотов и рамки карт, без отдельного
|
||
bloom-объёма; V11 — новых HUD-анимаций нет.
|
||
- **Новые правки после мокапа:** карты урона укладываются сверху вниз
|
||
(зеркально у противника), шаг `damageStackOffset = 0.3` от ширины карты,
|
||
новая карта приподнята по Y и рисуется поверх; trigger-карта повёрнута
|
||
на 90° против часовой стрелки (локально `-90°` для себя, `+90°` для
|
||
противника); на карте показывается бейдж триггера (имена убраны);
|
||
no-trigger damage check определяется по `DamageTakenView` с подавлением
|
||
повторного зума после trigger-карты.
|
||
- **Правки по замечаниям:** имена карт на поле убраны; панель действий
|
||
сжата до автоподстраиваемого столбца кнопок справа снизу (без заголовка,
|
||
фона и скролла); рука центрируется по экрану (`handViewportX = 0.5`);
|
||
подсказка муллигана перенесена в верхний `ActionPrompt`, при выборе карт
|
||
показывается кнопка «Заменить N карт»; центральный баннер хода убран;
|
||
левый нижний бар — `Колода, Душа, Рука, Сброс`, без `Щит/Триг`, все
|
||
блоки одинаковые, `Душа/Сброс` кликабельны.
|
||
- **Порядок триггера:** камера всегда фокусируется на trigger-зоне;
|
||
после фокуса карта раскрывается анимацией (`triggerRevealDuration`),
|
||
затем пауза `triggerRevealHold = 0.75 с`, затем резолв. Выбор цели
|
||
открывается только после этой паузы. Сначала `+5000` на свою карту в
|
||
RC/VC, затем эффект: `Critical` — RC/VC, `Stand` — только повёрнутый RC,
|
||
`Draw` — автоматически, `Heal` — выбор карты урона при выполнении
|
||
условий. Подсказки пишутся в верхнем информационном блоке.
|
||
Нетриггерная карта просто пропускается.
|
||
- **Tejas:** исправлена атака заднего ряда по колонке через границу
|
||
игроков: `V↔1`, `3↔2`, `4↔0`. Сервер пересобран и задеплоен на
|
||
`ssh home`, контейнер `server` перезапущен.
|
||
- **Основная фаза:** в панели подтверждений остаётся только кнопка
|
||
«Боевая фаза». «Закончить ход» и список всех действий скрыты; активация
|
||
способностей — через меню карты.
|
||
- **Рамки карт:** собственная рамка карты — тонкая чёрная по её краю;
|
||
подсветка выбора/цели тоже ужата до размера карты, больше не наезжает
|
||
на соседние карты. В руке рамки скрыты, карты уложены широким веером:
|
||
новая карта перекрывает предыдущую и за счёт шага по глубине всегда
|
||
рисуется поверх неё. Своя рука заполняется слева направо: первая карта
|
||
слева, новые добавляются правее и остаются поверх. Веер сохраняет ширину
|
||
до 10 карт, а начиная с 11-й шаг плавно уменьшается. Рука противника —
|
||
зеркальный камеро-зависимый веер ниже верхней панели: новая карта
|
||
появляется слева и остаётся поверх предыдущей, наклон и порядок глубины
|
||
зеркальны. Позы обеих рук пересчитываются каждый кадр под текущую камеру,
|
||
поэтому добор не улетает к устаревшей цели.
|
||
- **Стартовый Авангард:** лежит закрытым во время муллигана; после
|
||
завершения муллиганов обоих игроков открывается с анимацией
|
||
(`vanguardRevealDuration = 0.8 с`), затем начинается первая фаза.
|
||
Переворот идёт вокруг длинной оси карты слева направо, у противника
|
||
зеркально. `MatchProjection` больше не форсирует `FaceUp` для Vanguard
|
||
и передаёт реальное состояние карты; клиент блокирует ввод на время
|
||
переворота.
|
||
- **Старт боя:** камера стартует над стороной игрока лицом к столу,
|
||
пауза 1,5 с; затем по дуге опускается на основную позицию, пауза 1,5 с;
|
||
затем одновременно раздаются обе руки — своя и противника — с анимацией
|
||
из колод. Панель муллигана появляется только после окончания всех доборов.
|
||
Ввод заблокирован до конца интро.
|
||
- **Взятие карт:** карта создаётся ровно над колодой рубашкой вверх
|
||
(рисунок рубашки ориентирован правильно) и летит в руку по дуге с
|
||
переворотом лицом вверх. Любой добор идёт последовательно: следующая
|
||
карта стартует только после завершения анимации предыдущей. В стартовой
|
||
раздаче своя и чужая руки идут парами, внутри каждой пары — по одной.
|
||
- **Урон:** face-up/face-down переворот карт урона идёт вокруг длинной
|
||
стороны карты, а не поперёк.
|
||
- **Проверки:** клиентские EditMode в копии без `Library` — **50/50**;
|
||
серверные `Vanguard.Rules` на `ssh home` — **155/155**; standalone-
|
||
сборка Windows — успешно; клиент запущен из
|
||
`client/Builds/Windows/VanguardReMaster.exe`.
|
||
- **Не проверено:** ручная игровая приёмка пользователем.
|
||
- **Открытые вопросы:** bloom/свет, HUD-анимации, надёжность эвристики
|
||
no-trigger damage check.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-004: убрать боевые баннеры
|
||
|
||
**Решение пользователя:** все боевые баннеры не нужны; убираем их
|
||
полностью, чтобы пересобрать анимации блоками. Верхний `ActionPrompt`
|
||
(муллиган/смена/подсказки), `StatusToast` и trigger-сообщения не являются
|
||
боевыми баннерами и остаются.
|
||
|
||
**Анализ:**
|
||
- Центральный `BattleFeedback` и полноэкранный `BattleFlash` — это один
|
||
feedback-блок: текст события, вспышка damage/heal, кнопка «Пропустить
|
||
анимации», очередь и паузы `eventPause`/`post*Pause`.
|
||
- Паузы feedback сейчас задают темп событий, но не являются источником
|
||
истины; после удаления темп держат `CardMoveAnimator`, trigger-последовательность
|
||
и серверные снапшоты.
|
||
- `InputLock("feedback")` и `_feedbackLockHeld` существуют только для
|
||
этого блока; их нужно убрать вместе с ним.
|
||
- `_turnBanner`/`_turnBannerText` объявлены, но не строятся; удалить как
|
||
мёртвый код.
|
||
- `AnimationConfig.attackDeclare/guard/driveCheck/trigger/battleResolved/
|
||
damage/heal/intercept/perfectGuard/eventPause/postDrivePause/
|
||
postTriggerPause/postDamagePause/retireDuration` после удаления станут
|
||
неиспользуемыми; пока оставляем поля, чтобы не менять контракт ассета.
|
||
|
||
**Реализация:**
|
||
- [x] Удалить `BattleFeedback`, `BattleFlash`, `SkipFeedback` и весь
|
||
`FeedbackStep`-механизм.
|
||
- [x] Убрать вызовы `ShowFeedback`/`EnqueuePause`/`ShowFeedbackFor`.
|
||
- [x] Убрать `InputLock("feedback")` и связанные поля/сбросы.
|
||
- [x] Удалить неиспользуемые `_turnBanner`/`_turnBannerText`.
|
||
- [x] Оставить журнал событий, `Describe`, trigger-последовательность,
|
||
`ActionPrompt` и `StatusToast`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 51/51.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: в бою нет центральных баннеров и вспышек, события идут
|
||
без зависаний; журнал и trigger-подсказки работают.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-005: порядок набора руки
|
||
|
||
**Решение пользователя:** рука локального игрока должна набираться
|
||
**слева направо**: первая карта слева, новые добавляются правее. Рука
|
||
противника по правилу зеркальности набирается с его стороны поля, то есть
|
||
на экране справа налево: новая карта слева.
|
||
|
||
**Реализация:**
|
||
- [x] `GetHandViewPose`: вернуть `x = center + offset * step`, `roll = -offset * roll`.
|
||
- [x] `GetOpponentHandViewPose`: сделать зеркально — `x = center - offset * step`,
|
||
`roll = +offset * roll`.
|
||
- [x] Обновить тест `Hands_Fill_Mirrored_And_Newest_Is_On_Top` под новое
|
||
направление и новые карты.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 51/51.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: новая карта в своей руке появляется справа; у противника —
|
||
слева.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-006: возврат карт из руки в колоду
|
||
|
||
**Решение пользователя:** карта уходит из руки в колоду по дуге **под низ
|
||
колоды** с переворотом рубашкой вверх во время движения. Возврат идёт
|
||
**справа налево**; следующая карта стартует только после завершения
|
||
предыдущей анимации. Для противника — то же зеркально с его стороны.
|
||
|
||
**Анализ:**
|
||
- Сервер уже сообщает о возврате: `MulliganView` (игрок, количество) и
|
||
`CardReturnedToDeckView` (игрок, `PrintingId`). Отдельного per-card события
|
||
для муллигана нет, поэтому карты определяются по исчезновению из старой
|
||
руки.
|
||
- Старый `Card3D` руки сейчас просто освобождается в `GameBoard3D.Render`;
|
||
чтобы анимировать, его нужно вынуть из `_cards`, оставить в отдельном
|
||
списке, провести дугой и только потом вернуть в пул.
|
||
- Для последовательности используем `CardMoveAnimator` с `sequential: true`;
|
||
для скрытия после дуги — callback завершения. Это переиспользует общий
|
||
arc/flip и очередь, не создаёт отдельный аниматор.
|
||
- Цель — позиция колоды минус небольшой `Y`, рубашкой вверх; во время
|
||
движения `StartArcMove` интерполирует поворот от лица к рубашке.
|
||
|
||
**Реализация:**
|
||
- [x] `GameBoard3D.QueueHandReturnToDeck` + обработка `MulliganView` и
|
||
`CardReturnedToDeckView` из `BattleView`.
|
||
- [x] Выбор старых карт руки, удаление из `_cards`, дуга под колоду,
|
||
переворот, возврат в пул после завершения.
|
||
- [x] `CardMoveAnimator.Enqueue` получил `onCompleted`; последовательная
|
||
очередь используется для возврата.
|
||
- [x] Тесты: `Hand_Return_To_Deck_Starts_Right_To_Left_And_Targets_Deck_Bottom`,
|
||
`CardMoveAnimator_Runs_Sequential_Requests_And_Completes_Each`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 53/53.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: при муллигане/возврате карты уходят по дуге под колоду,
|
||
переворачиваются рубашкой вверх, следующая стартует после предыдущей;
|
||
у противника зеркально.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-007: общий добор карты
|
||
|
||
**Решение пользователя:** добор после муллигана должен выглядеть так же,
|
||
как добор в муллигане: карта появляется над колодой **полного размера**,
|
||
рубашкой вверх, летит в руку по дуге и переворачивается лицом вверх.
|
||
|
||
**Анализ:**
|
||
- `AnimateHandDraw` уже стартует с `_config.cardScale` и `card.FaceDownRotation`.
|
||
- Обычный добор шёл через `ResolveSourcePose`: для карты руки он брал
|
||
`spec.Scale * cardScale` (размер руки) и плоскую ориентацию колоды, поэтому
|
||
над колодой появлялась маленькая карта.
|
||
- Нужно вынести общий старт добора: позиция над колодой, полный масштаб,
|
||
рубашка в ориентации будущей карты руки.
|
||
|
||
**Реализация:**
|
||
- [x] `GameBoard3D.GetDrawSource` — общая позиция/масштаб старта добора.
|
||
- [x] `ResolveSourcePose` для `BoardZone.Hand` использует полный масштаб и
|
||
`spec.BaseRotation * FaceDownFlip`.
|
||
- [x] `AnimateHandDraw` использует тот же `GetDrawSource`.
|
||
- [x] Длительность нового добора — `introDrawDuration` (0.6 с), как в муллигане.
|
||
- [x] Тест проверяет, что новая карта стартует над колодой в `cardScale`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 53/53.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: добор после муллигана визуально совпадает с муллиганом;
|
||
карта не маленькая.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-008: длительность возврата как у добора
|
||
|
||
**Решение пользователя:** анимация возврата карты в колоду должна идти
|
||
столько же, сколько добор из колоды.
|
||
|
||
**Реализация:**
|
||
- [x] Общая `GameBoard3D.DrawDuration` (`introDrawDuration`, 0.6 с).
|
||
- [x] `StartHandReturn` использует `DrawDuration` вместо `moveDuration`.
|
||
- [x] Обычный добор использует ту же `DrawDuration`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 53/53.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: возврат в колоду по длительности совпадает с добором.
|
||
|
||
---
|
||
|
||
## 2026-10-27 — TASK-2026-009: последовательный возврат карт
|
||
|
||
**Проблема:** карты возвращались одновременно. `Card3D.SetPose` ставил
|
||
цель под колодой, а `_returningCards` тикали все сразу; очередь
|
||
`CardMoveAnimator` стартовала дуги по одной, но карты уже плавно уезжали
|
||
к цели параллельно.
|
||
|
||
**Решение:** пока карта ждёт в очереди, удерживать её в исходной позе руки
|
||
через `Card3D.HoldAtSource`. Старт следующей — только после завершения
|
||
предыдущей (`sequential: true`).
|
||
|
||
**Реализация:**
|
||
- [x] `StartHandReturn` вызывает `HoldAtSource` до `CardMoveAnimator.Enqueue`.
|
||
- [x] Тест проверяет, что обе возвращаемые карты остаются в руке до старта
|
||
своей анимации.
|
||
- [x] Порядок: самая правая карта первой; у противника зеркально.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 53/53.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: сначала уходит самая правая карта, затем следующая.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-010: рука перестраивается при выходе карты
|
||
|
||
**Решение пользователя:** при любом выходе карты из руки остальные карты
|
||
должны перестраиваться под новое количество одновременно с её перемещением.
|
||
|
||
**Анализ:**
|
||
- `UpdateHandView` уже считает позы по новому `HandCount` из свежего
|
||
`Render`.
|
||
- Перекомпоновка руки (`Hand → Hand`) не ставится в `CardMoveAnimator`
|
||
(`NeedsMove` возвращает false), поэтому не ждёт анимацию уходящей карты.
|
||
- Уходящая карта удаляется из `_handBindings`/`_cards`, а остальные карты
|
||
сразу получают новые цели; hand reflow идёт своим smooth-слоем.
|
||
|
||
**Реализация:**
|
||
- [x] Проверка поведения для hand → field.
|
||
- [x] Проверка поведения для возврата в колоду: остальные карты получают
|
||
новые цели, пока возвращаемая карта ещё ждёт/летит.
|
||
- [x] Тесты `Hand_Reflows_While_A_Card_Leaves_From_Hand` и расширенный
|
||
`Hand_Return_To_Deck_Starts_Right_To_Left_And_Targets_Deck_Bottom`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 54/54.
|
||
- [ ] Вручную: при выходе карты рука сдвигается одновременно с её движением.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-011: ступенчатый reflow руки при муллигане
|
||
|
||
**Решение пользователя:** во время муллигана рука должна сдвигаться **после
|
||
каждого** ухода карты, а не один раз под окончательное количество.
|
||
|
||
**Проблема:** новый `Render` сразу содержал финальный `HandCount` (старые
|
||
остатки + новые карты). Остальные карты получали финальные позы, а
|
||
возвращаемые удерживались. Визуально рука перестраивалась один раз.
|
||
|
||
**Решение:**
|
||
- Ступенчатый список `_stagedReturnCards`: старые карты руки в исходном
|
||
порядке, включая ожидающие возврата; новые карты в него не входят.
|
||
- Пока идёт возврат, `UpdateHandView` строит позы только для этого списка.
|
||
- После завершения каждой карты она убирается из списка, и оставшиеся
|
||
карты (включая ожидающие) плавно сдвигаются под новое количество.
|
||
- Ожидающие карты не удерживаются на месте: при старте своей анимации они
|
||
получают цель под колодой через `OnStart` и стартуют из текущей позы
|
||
(`fromCurrent`).
|
||
- После последнего возврата staged-режим выключается; новые карты
|
||
добираются в финальную руку.
|
||
|
||
**Реализация:**
|
||
- [x] `BuildStagedReturnHand`, `_stagedReturnCards`, `_stagedReturnPlayer`.
|
||
- [x] `CardMoveAnimator` получил `onStart` и `fromCurrent`.
|
||
- [x] `OnHandReturnCompleted` убирает карту из staged-списка и сразу
|
||
пересчитывает руку.
|
||
- [x] Тест проверяет: до первого ухода рука не сдвигается, после первого —
|
||
сдвигается; ожидающая карта остаётся в руке до своей очереди.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 54/54.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: муллиган с двумя возвратами — рука сдвигается после каждого.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-012: добор по одной карте и ступенчатый reflow
|
||
|
||
**Проблема:** при доборе все новые карты сразу появлялись над колодой
|
||
рубашкой вверх, а рука перестраивалась один раз под финальное количество.
|
||
|
||
**Решение:**
|
||
- Новые карты своей руки скрываются на время очереди и показываются только
|
||
в `CardMoveAnimator.onStart` — по одной.
|
||
- `_stagedDrawCards` хранит уже видимые карты (старые остатки + завершённые
|
||
доборы); новые карты добавляются только после завершения их дуги.
|
||
- После каждого добора рука пересчитывается под текущее видимое количество,
|
||
а не под финальное.
|
||
- После последнего добора staged-режим выключается; финальная рука совпадает
|
||
с текущей.
|
||
|
||
**Реализация:**
|
||
- [x] `BuildStagedDrawHand`, `_stagedDrawCards`, `_stagedDrawTargetCount`.
|
||
- [x] `BeginHandDrawArc` показывает карту, ставит цель в следующий слот и
|
||
запускает дугу из текущей позиции.
|
||
- [x] `OnHandDrawCompleted` добавляет карту в staged-список и пересчитывает
|
||
руку.
|
||
- [x] Повторный snapshot без новых доборов не сбрасывает уже начатую staged
|
||
раздачу — иначе остатки руки перестают сдвигаться.
|
||
- [x] Тест `Hand_Draw_Stages_New_Cards_One_By_One`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 55/55.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: при доборе над колодой видна одна карта; рука расширяется
|
||
после каждой.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-013: единый механизм раскладки руки
|
||
|
||
**Проблема:** остатки руки после муллигана переставали сдвигаться:
|
||
отдельные `_stagedReturnCards` и `_stagedDrawCards` не всегда содержали
|
||
одни и те же карты, а повторные snapshot/переходы между режимами ломали
|
||
раскладку.
|
||
|
||
**Решение:** один визуальный список `_visualHandCards` и один override
|
||
`_handLayoutOverride`:
|
||
- в список сразу входят все оставшиеся карты руки и ожидающие возврата;
|
||
- новые добранные карты добавляются только после завершения их дуги;
|
||
- после каждого возврата/добора список пересчитывается, поэтому остатки
|
||
руки всегда участвуют в сдвиге;
|
||
- пока override активен, `UpdateHandView` использует только визуальный
|
||
список; после завершения всех возвратов/доборов возвращается обычный
|
||
`_handBindings`;
|
||
- повторный Render не пересоздаёт список и не сбрасывает прогресс.
|
||
|
||
**Реализация:**
|
||
- [x] `BuildVisualHand`, `_visualHandCards`, `_pendingDrawCards`,
|
||
`_handLayoutOverride`.
|
||
- [x] Удалены раздельные staged-списки возврата/добора.
|
||
- [x] Тест `Mulligan_Remaining_Cards_Reflow_After_Each_Return`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 56/56.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: после муллигана остатки руки сдвигаются после каждого
|
||
возврата и каждого добора.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-014: подъём карты в руке при наведении
|
||
|
||
**Решение пользователя:** карта под курсором плавно приподнимается на 10%
|
||
своего размера; после ухода курсора плавно возвращается.
|
||
|
||
**Реализация:**
|
||
- [x] `handHoverLift = 0.1` теперь доля высоты карты, а не мировое смещение.
|
||
- [x] `GameBoard3D.UpdateHandHover`: raycast по своей руке, только когда нет
|
||
UI/лока/драга/анимаций; оппонент и поле игнорируются.
|
||
- [x] `SetHoveredHandCard` пересчитывает руку; `ApplyHandHover` добавляет
|
||
камера-вверх на `cardHeight * scale * cardScale * handHoverLift`.
|
||
- [x] Плавность — существующий `Card3D.SetPose`/`Tick`; отдельного твина нет.
|
||
- [x] Тест `Hand_Hover_Lifts_Card_By_Ten_Percent` (подъём и возврат цели).
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 57/57.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: наведение поднимает карту, уход курсора возвращает.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-015: drag карты из руки, смена через VC
|
||
|
||
**Решение пользователя:** карта из руки, отпущенная на VC, делает смену;
|
||
пока кнопка мыши удерживается, карта остаётся приподнятой; отпускание в
|
||
неположенном месте плавно возвращает её в руку; без дёрганья.
|
||
|
||
**Реализация:**
|
||
- [x] `Card3D.EndDragAtCurrent` фиксирует карту в точке drop-а, не возвращая
|
||
её в руку и не сбрасывая подъём/масштаб.
|
||
- [x] `GameBoard3D.FinishDrag`: для карты из руки больше не вызывает
|
||
`ReturnToOrigin` до ответа сервера; держит карту в `_dropPendingCard`.
|
||
- [x] `GameBoard3D.ReturnDroppedCard`: снимает hold и плавно возвращает в
|
||
руку, если drop не стал интентом/отклонён.
|
||
- [x] `BattleView`: возвращает карту при отсутствии интента, отказе сервера,
|
||
ошибке отправки, guard-выборе и раннем выходе.
|
||
- [x] Валидный Ride/Call: карта ждёт снапшот и летит в target уже из точки
|
||
drop-а; в руку не возвращается.
|
||
- [x] Тест `Dropped_Hand_Card_Holds_At_Drop_Until_Returned`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode в копии без `Library` — 58/58.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: drag в VC на смену, удержание, возврат из неверного места.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-016: карта при drag под курсором
|
||
|
||
**Проблема:** при перетаскивании карта уезжала вниз-влево от курсора:
|
||
луч наводился на плоскость `cardLift`, а карта висела на `dragLift`; плюс
|
||
`DragTo` сбрасывал hand-ориентацию в плоский `Euler(0,yaw,0)`.
|
||
|
||
**Решение:**
|
||
- Плоскость drag-а считается на фактической высоте карты
|
||
(`Card3D.DragLift`), поэтому карта остаётся под курсором.
|
||
- Для карты из руки `DragLift` = hover-подъём 10% высоты; для карты поля —
|
||
прежний `dragLift`.
|
||
- `DragTo` сохраняет `_dragOriginRotation`, поэтому hand-карта не
|
||
переворачивается на стол при перетаскивании.
|
||
- `_dragPointerOffset` сохраняет точку захвата: карта не прыгает центром
|
||
под курсор при старте drag-а.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode — 58/58.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: карта у курсора, hand-ориентация сохранена.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-017: drag не двигает карту в момент старта
|
||
|
||
**Проблема:** в момент старта drag-а вызывался `UpdateDrag`, карта сразу
|
||
переносилась к курсору/поднималась. Кроме того, после старта `UpdateDrag`
|
||
больше не вызывался каждый кадр — карта не следовала за мышью.
|
||
|
||
**Решение:**
|
||
- `OnPointerHeld` только включает drag и запоминает точку захвата; позицию
|
||
не меняет.
|
||
- `HandlePointer` при `_dragging` каждый кадр вызывает `UpdateDrag`, поэтому
|
||
карта следует за курсором.
|
||
- `OnPointerReleased` больше не дублирует `UpdateDrag`; last target уже
|
||
обновлён в этом кадре.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode — 58/58.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: при начале drag-а карта остаётся на месте, затем следует за
|
||
курсором.
|
||
|
||
---
|
||
|
||
## 2026-10-28 — TASK-2026-018: выбор зоны без перемещения карты из руки
|
||
|
||
**Решение пользователя:** карта из руки при зажатии ЛКМ не должна
|
||
перемещаться. Курсор наводится на зону; карта остаётся в руке
|
||
приподнятой; при отпускании над VC выполняется смена.
|
||
|
||
**Реализация:**
|
||
- [x] Для `source.IsHand` drag заменён на `_handTargeting`: карта не
|
||
вызывает `BeginDrag`/`DragTo` и остаётся в hand-раскладке.
|
||
- [x] `UpdateHandHover` удерживает hover-подъём нажатой карты, пока идёт
|
||
targeting.
|
||
- [x] `UpdateHandTargeting` считает только целевую зону по курсору и
|
||
показывает drop indicator; карта не двигается.
|
||
- [x] Цель сначала определяется raycast-ом по карте/кругу (VC-карта даёт
|
||
ровно Vanguard), затем fallback на плоскость.
|
||
- [x] `FinishHandTargeting` на release отправляет `CardDropped`; при валидном
|
||
VC срабатывает RideIntent, при невалидном карта остаётся в руке.
|
||
- [x] Field-карты сохраняют старый drag с движением.
|
||
- [x] По серверному логу `rides=[TD02/007EN, BT01/015EN]` — только эти карты
|
||
были доступны для смены. Валидные Ride-карты теперь подсвечиваются в руке;
|
||
добавлен fallback direct RideIntent и диагностические логи
|
||
`[HandTargeting]` / `[Drop]`.
|
||
|
||
**Проверка:**
|
||
- [x] EditMode — 58/58.
|
||
- [x] Standalone Windows собирается.
|
||
- [ ] Вручную: ЛКМ на карте, наведение на VC, отпускание — смена; карта всё
|
||
время остаётся в руке.
|
||
|
||
---
|
||
|
||
### TASK-2026-019 — загрузочная сцена и кадр без покадровых аллокаций
|
||
|
||
- **Статус:** `[ ]`
|
||
- **Создано:** 2026-10-30
|
||
- **Анализ:** DeepSeek выполнен (пункты A1, C1–C3 в
|
||
`docs/Development/CLIENT-SCENES-3D-ANIMATIONS.md`)
|
||
- **Реализация:** не начата
|
||
- **Проверка:** не выполнена
|
||
- **Затронутые системы:** `SceneFlow`, `VanguardApp`, `Boot`, новый
|
||
`LoadingOverlay`, `GameBoard3D` (стрелки, рука), `ArrowDirector`,
|
||
`AttackArrowView`, `BattleView.UpdateClock`, `Localization`, `InputLockService`,
|
||
EditMode-тесты.
|
||
|
||
#### A. Цель и требования
|
||
|
||
Первый блок бэклога «сцены, 3D, анимации». Четыре независимых результата:
|
||
|
||
1. Переход между экранами без вспышки: `Boot` — загрузочная сцена с прогрессом
|
||
и fade, затем `Login`; тот же overlay используется для остальных переходов.
|
||
2. Стрелки атаки/буста не создают мусор в кадре.
|
||
3. Рука не пересчитывается дважды за кадр и не пересчитывается впустую.
|
||
4. Строки HUD и локализации не обновляются каждый кадр без изменения значения.
|
||
|
||
Ограничения:
|
||
|
||
- без новых пакетов (DOTween/UniTask запрещены);
|
||
- HUD из TASK-2026-003 не перекомпоновывать; в `BattleView` допустима только
|
||
точечная правка `UpdateClock`;
|
||
- правила, сервер, протокол, контент не меняются;
|
||
- `CardMoveAnimator` остаётся единственным планировщиком перемещений;
|
||
- `AnimationConfig` в этом блоке не меняется (это отдельный пункт C6);
|
||
- геймплей вручную проверяет пользователь.
|
||
|
||
Критерии готовности:
|
||
|
||
- переход `Boot → Login` и `Login → Lobby` без видимой вспышки, прогресс
|
||
отображается;
|
||
- в бою без движения карт и стрелок — 0 B/кадр по Profiler (Allocation Call
|
||
Stacks);
|
||
- позы руки не пересчитываются, если не менялись состав руки, камера, hover и
|
||
staged-список;
|
||
- таймер/банк и локализованные подписи обновляются только при изменении;
|
||
- существующие EditMode-тесты и оба smoke проходят.
|
||
|
||
#### B. Текущее состояние
|
||
|
||
1. `SceneFlow.LoadAsync` — `LoadSceneAsync(sceneName, LoadSceneMode.Single)`,
|
||
ожидание `isDone` без progress/fade (`SceneFlow.cs:63-80`). `Boot` содержит
|
||
камеру, свет и Global Volume; контроллера и UI нет, `VanguardApp` создаётся
|
||
в `RuntimeInitializeOnLoadMethod` (`ClientBootstrap.cs:11-19`), `Start`
|
||
грузит `Login` (`VanguardApp.cs:59-67`).
|
||
2. `GameBoard3D.UpdateBoostArrows` создаёт `new List<(Vector3, Vector3)>` на
|
||
каждый вызов (`GameBoard3D.cs:1219-1227`), `BattleCard` использует
|
||
`Players.FirstOrDefault(lambda)` (`:1319-1320`), `ArrowAnchor` ищет карту
|
||
линейным перебором `_cards.Values` (`:1293-1301`, `:1332-1341`).
|
||
3. `GameBoard3D.Update` за кадр вызывает `UpdateHandView`, `_moveAnimator.Tick`,
|
||
`TickHandDraw`, `_cameraDirector.Tick`, `card.Tick` по всем картам,
|
||
`_returningCards`, затем `_arrowDirector.Tick` и `UpdateAttackArrowPoints`
|
||
(`:2220-2255`). Стрелки обновляются двумя путями.
|
||
4. `AttackArrowView.ApplyColor` создаёт `new[] { _shaft, _head }` (`:267`) и
|
||
вызывается из `Tick` и `SetStyle`, т.е. несколько раз за кадр на стрелку.
|
||
5. `UpdateHandView` вызывается из `Update`, `Render`, hover и колбэков
|
||
(`:183, 511, 607, 732, 1163, 2230, 2372`); позы обеих рук считаются каждый
|
||
кадр.
|
||
6. `BattleView.UpdateClock` пишет текст таймера и банка каждый кадр;
|
||
`LocalizedText.LateUpdate` проходит `Loc.Translate` на каждый лейбл;
|
||
`InputLockService.Describe` собирает строку через LINQ при активном локе.
|
||
|
||
Что сохранить:
|
||
|
||
- пул `Card3D` и переиспользование по ключу/`InstanceId`;
|
||
- `CardMoveAnimator` как единственный планировщик, его `CancelCard`/`CancelAll`
|
||
и таймаут;
|
||
- общий `Tick` из `GameBoard3D` (без `Update` на каждой карте);
|
||
- `Time.unscaledDeltaTime` и текущую интерполяцию (`Card3D.Tick`);
|
||
- геометрию стрелок, раскладку руки, hover-подъём и staged-reflow.
|
||
|
||
#### C. Чек-лист изменений
|
||
|
||
- [ ] **C1. `CREATE` `client/Assets/Vanguard/Runtime/UI/LoadingOverlay.cs`** —
|
||
персистентный overlay загрузки.
|
||
- Canvas + фон + текст прогресса на объекте `VanguardApp`, `raycastTarget =
|
||
false`, поверх сцены; API `Show(float progress)`, `Hide()`.
|
||
- Не использовать `UiKit.CreateCanvas` для сцены: Canvas должен переживать
|
||
`LoadSceneMode.Single`.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C2. `MODIFY` `Runtime/App/SceneFlow.cs`** — переход с прогрессом и fade.
|
||
- Показывать overlay до `LoadSceneAsync`, обновлять прогресс по
|
||
`operation.progress`, `allowSceneActivation = false` до готовности, затем
|
||
активировать сцену и скрывать overlay после первого кадра.
|
||
- Защита от повторного вызова во время перехода (idempotent `LoadAsync`).
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: C1.
|
||
|
||
- [ ] **C3. `MODIFY` `Runtime/App/VanguardApp.cs` + `Scenes/Boot.unity`** — Boot
|
||
как загрузочная сцена.
|
||
- `Boot` показывает overlay и грузит `Login` после инициализации сервисов;
|
||
прогрев не дублируется в `Start`, если сцена уже целевая.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: C1, C2.
|
||
|
||
- [ ] **C4. `MODIFY` `Board3D/View/GameBoard3D.cs`** — стрелки без LINQ и
|
||
покадровых списков.
|
||
- Переиспользуемый буфер пар boost-стрелок, индексированный поиск игрока
|
||
вместо `FirstOrDefault`, один проход по `_cards` для якорей.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C5. `MODIFY` `Animation/ArrowDirector.cs` + `View/AttackArrowView.cs`** —
|
||
одно обновление геометрии за кадр.
|
||
- Убрать двойной путь `Tick` + `UpdateAttackArrowPoints`; обновлять точки
|
||
один раз; убрать массив `new[] { _shaft, _head }` в `ApplyColor`.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: C4.
|
||
|
||
- [ ] **C6. `MODIFY` `Board3D/View/GameBoard3D.cs`** — `UpdateHandView` по
|
||
dirty-флагу.
|
||
- Пересчитывать позы только при изменении состава руки, камеры, hover,
|
||
staged-списка; оставить ровно один вызов за кадр.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C7. `MODIFY` `UI/BattleView.cs` — `UpdateClock`.** Обновлять текст
|
||
таймера и банка только при изменении отображаемого значения.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C8. `MODIFY` `UI/Localization.cs` — `LocalizedText`.** Кешировать
|
||
результат перевода до смены языка или ключа, не вызывать `Loc.Translate`
|
||
каждый кадр.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C9. `MODIFY` `Battle/InputLockService.cs` — `Describe`.** Не собирать
|
||
строку/LINQ в кадровом пути: кешировать описание при изменении набора причин.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: нет.
|
||
|
||
- [ ] **C10. `TEST` регрессии блока.**
|
||
- EditMode: overlay `Show/Hide` идемпотентны; `UpdateHandView` не меняет позы
|
||
при неизменных входах (seam-счётчик), hover/добор/муллиган по-прежнему
|
||
сдвигают руку; `ApplyColor` не создаёт массив (статическая проверка или
|
||
отсутствие регрессии поведения).
|
||
- Существующие `Hand_*`, `CardMoveAnimator_*`, `GameBoard3DTests` — без
|
||
изменений ожиданий.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: C1–C9.
|
||
|
||
- [ ] **C11. `VERIFY` автотесты, smoke, сборка.**
|
||
- EditMode в копии без `Library`; `SceneSmoke.Run`; `SceneSmoke.RunPlaymat`;
|
||
standalone-сборка.
|
||
- Profiler: Allocation Call Stacks в бою без движения — 0 B/кадр.
|
||
- Реализация: не выполнена. Проверка: не выполнена.
|
||
- Зависит: C10.
|
||
|
||
- [ ] **C12. `VERIFY` ручная проверка пользователем.**
|
||
- Переход `Boot → Login → Lobby → Battle` без вспышки; видимый прогресс;
|
||
стрелки атаки/буста без визуальных изменений; hover, добор, муллиган,
|
||
reflow руки; таймер и подписи.
|
||
- Реализация: не применима. Проверка: остаётся на пользователе.
|
||
- Зависит: C11.
|
||
|
||
#### D. Последовательность выполнения
|
||
|
||
1. C1 → C2 → C3: загрузочный overlay и Boot. Проверить переходы смоуком.
|
||
2. C4 → C5: стрелки; визуально сравнить до/после.
|
||
3. C6: рука по dirty-флагу; прогнать `Hand_*`-тесты.
|
||
4. C7 → C8 → C9: строки и локализация.
|
||
5. C10 → C12: тесты, проверки, ручная приёмка.
|
||
|
||
#### E. Проверки
|
||
|
||
- Клиентские EditMode в копии проекта без `Library` (команда в
|
||
`docs/SESSION-STATE.md`).
|
||
- `SceneSmoke.Run` и `SceneSmoke.RunPlaymat`.
|
||
- Standalone-сборка на копии: переходы и overlay.
|
||
- Profiler: Allocation Call Stacks до/после для боя без движения.
|
||
- `git diff --check`; отсутствие изменений в `Vanguard.Rules`, `Vanguard.Shared`,
|
||
`Vanguard.Server`, `content/`.
|
||
- Ручная приёмка — пользователем.
|
||
|
||
#### F. Риски и неизвестные вопросы
|
||
|
||
- **Overlay на персистентном Canvas:** может перехватывать клики или висеть
|
||
после загрузки — `raycastTarget = false`, скрытие в `finally` и по таймауту.
|
||
- **Смоук-таймауты:** `SceneSmoke` ждёт 6 с; если Boot начнёт грузить Login
|
||
позже, таймаут придётся увеличить или ждать по состоянию.
|
||
- **Dirty-флаг руки:** легко сломать hover, staged-reflow и муллиган; менять
|
||
только после зелёных `Hand_*`-тестов.
|
||
- **Якоря стрелок:** карты двигаются каждый кадр, поэтому кешировать можно
|
||
буферы и результаты поиска карт, но не позиции.
|
||
- **Порядок вызовов:** у `BattleSceneController.Update` и `GameBoard3D.Update`
|
||
нет `DefaultExecutionOrder`; правки не должны завязываться на порядок.
|
||
- **HUD TASK-2026-003** ещё не принят вручную: в `BattleView` менять только
|
||
`UpdateClock`, не разметку.
|
||
- Открытых вопросов, блокирующих старт, нет.
|
||
|
||
#### G. Результат
|
||
|
||
- **Реализовано:** —
|
||
- **Проверки:** —
|
||
- **Не завершено/заблокировано:** —
|
||
- **Skill обновлён:** —
|
||
- **Git:** —
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|