client: DeckBuilder — один экран, мультифильтры и список колоды
CI / test (push) Successful in 6m8s Details

- Один экран: слева коллекция, справа панель «Мои колоды» ⇄ «Колода»;
  отдельные DeckList/DeckEditor удалены.
- Мультифильтры клана, грейда и триггера в оверлее: чипы с подсветкой,
  черновик, «Сбросить»/«Отмена»/«Применить», сводка в топбаре.
- Список колод: 50/50, T 16/16, Classic, G0–G3, бейдж валидности,
  подсветка открытой.
- Список карт колоды: группировка по nameId, сортировка грейд → имя,
  превью-иллюстрация (верхние 59% карты) без искажений, имя, ×N, G.
- Live-валидация 50/16/4; «Сохранить», «В бой», «С ботом» активны только
  для валидной сохранённой колоды.
- CardImageService.ApplyIllustration; DeckListFilter/DeckListModel;
  5 тестов DeckBuilderTests.
- Проверки: EditMode 67/67, SCENE_SMOKE success=True, PLAYMAT_SMOKE
  success=True, standalone собран.
This commit is contained in:
MpakobecEx 2026-10-31 21:41:08 +05:00
parent 1999ccb3d8
commit ecb7314bb6
11 changed files with 1551 additions and 1007 deletions

View File

@ -15,6 +15,11 @@ namespace Vanguard.UI
private const string ResourceFolder = "CardArt/"; private const string ResourceFolder = "CardArt/";
private static readonly Dictionary<string, Sprite> Cache = private static readonly Dictionary<string, Sprite> Cache =
new Dictionary<string, Sprite>(); new Dictionary<string, Sprite>();
private static readonly Dictionary<string, Sprite> IllustrationCache =
new Dictionary<string, Sprite>();
/// <summary>Доля высоты карты, занятая иллюстрацией (до полосы флейвора).</summary>
public const float IllustrationHeight = 0.59f;
public static void Apply(Image target, string imageKey, Sprite fallback) public static void Apply(Image target, string imageKey, Sprite fallback)
{ {
@ -27,6 +32,48 @@ namespace Vanguard.UI
target.preserveAspect = false; target.preserveAspect = false;
} }
/// <summary>
/// Превью только иллюстрации: верхняя часть карты без рамки, имени и
/// текста способности. Пропорции сохраняются.
/// </summary>
public static void ApplyIllustration(Image target, string imageKey, Sprite fallback)
{
if (target == null)
return;
var sprite = ResolveIllustration(imageKey, fallback);
target.sprite = sprite;
target.color = Color.white;
target.preserveAspect = true;
}
/// <summary>Спрайт иллюстрации (обрезанная верхняя часть карты).</summary>
public static Sprite ResolveIllustration(string imageKey, Sprite fallback)
{
if (string.IsNullOrWhiteSpace(imageKey))
return fallback;
if (IllustrationCache.TryGetValue(imageKey, out var cached))
return cached != null ? cached : fallback;
var texture = Resources.Load<Texture2D>(ResourceFolder + imageKey);
if (texture == null)
{
IllustrationCache[imageKey] = null;
return fallback;
}
var height = Mathf.Max(1, Mathf.RoundToInt(texture.height * IllustrationHeight));
var sprite = Sprite.Create(
texture,
new Rect(0f, texture.height - height, texture.width, height),
new Vector2(0.5f, 0.5f),
100f);
sprite.name = imageKey + "_illustration";
IllustrationCache[imageKey] = sprite;
return sprite;
}
/// <summary>Спрайт реального арта или процедурный fallback.</summary> /// <summary>Спрайт реального арта или процедурный fallback.</summary>
public static Sprite Resolve(string imageKey, Sprite fallback) public static Sprite Resolve(string imageKey, Sprite fallback)
{ {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Vanguard.Api.Contracts;
namespace Vanguard.UI
{
/// <summary>
/// Состояние мультифильтров коллекции. Пустой набор означает «все
/// значения»; активные наборы объединяются по И.
/// </summary>
public sealed class DeckFilter
{
public HashSet<string> Clans { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<int> Grades { get; } = new HashSet<int>();
public HashSet<string> Triggers { get; } = new HashSet<string>(StringComparer.Ordinal);
public bool IsEmpty => Clans.Count == 0 && Grades.Count == 0 && Triggers.Count == 0;
public bool Matches(CardCatalogItemDto meta)
{
if (meta == null)
return IsEmpty;
if (Clans.Count > 0 && !Clans.Contains(meta.Clan))
return false;
if (Grades.Count > 0 && !Grades.Contains(meta.Grade))
return false;
if (Triggers.Count > 0 && !Triggers.Contains(meta.Trigger ?? "None"))
return false;
return true;
}
public void Clear()
{
Clans.Clear();
Grades.Clear();
Triggers.Clear();
}
public void CopyFrom(DeckFilter other)
{
Clear();
if (other == null)
return;
Clans.UnionWith(other.Clans);
Grades.UnionWith(other.Grades);
Triggers.UnionWith(other.Triggers);
}
public DeckFilter Clone()
{
var copy = new DeckFilter();
copy.CopyFrom(this);
return copy;
}
}
/// <summary>Одна строка списка колоды: карта по названию и её количество.</summary>
public sealed class DeckCardGroup
{
public string NameId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Grade { get; set; }
public int Count { get; set; }
public CardCatalogItemDto Meta { get; set; }
public List<string> PrintingIds { get; } = new List<string>();
}
/// <summary>Локальная проверка колоды до сохранения.</summary>
public struct DeckValidation
{
public int Count;
public int Triggers;
public bool MaxCopiesOk;
public bool IsValid => Count == 50 && Triggers == 16 && MaxCopiesOk;
}
/// <summary>Чистые правила фильтрации, группировки и проверки колоды.</summary>
public static class DeckListModel
{
public static List<DeckCardGroup> GroupDeck(
IEnumerable<string> cards,
IReadOnlyDictionary<string, CardCatalogItemDto> catalog)
{
var groups = new Dictionary<string, DeckCardGroup>(StringComparer.Ordinal);
foreach (var printingId in cards)
{
var meta = catalog != null && catalog.TryGetValue(printingId, out var found)
? found
: null;
var key = meta != null && !string.IsNullOrEmpty(meta.NameId)
? meta.NameId
: printingId;
if (!groups.TryGetValue(key, out var group))
{
group = new DeckCardGroup
{
NameId = key,
Name = meta != null ? Loc.CardName(meta) : key,
Grade = meta != null ? meta.Grade : int.MaxValue,
Meta = meta,
};
groups[key] = group;
}
group.Count++;
group.PrintingIds.Add(printingId);
}
return groups.Values
.OrderBy(group => group.Grade)
.ThenBy(group => group.Name, StringComparer.Ordinal)
.ToList();
}
public static DeckValidation Validate(
IEnumerable<string> cards,
IReadOnlyDictionary<string, CardCatalogItemDto> catalog)
{
var result = new DeckValidation { MaxCopiesOk = true };
var byName = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var printingId in cards)
{
result.Count++;
var meta = catalog != null && catalog.TryGetValue(printingId, out var found)
? found
: null;
var nameId = meta != null && !string.IsNullOrEmpty(meta.NameId)
? meta.NameId
: printingId;
byName.TryGetValue(nameId, out var used);
used++;
byName[nameId] = used;
if (used > 4)
result.MaxCopiesOk = false;
if (meta != null && IsTrigger(meta.Trigger))
result.Triggers++;
}
return result;
}
public static bool IsTrigger(string trigger) =>
!string.IsNullOrEmpty(trigger)
&& !string.Equals(trigger, "None", StringComparison.Ordinal);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0bdfff22a21c12a4ea9036711e5164c7

View File

@ -415,6 +415,24 @@ namespace Vanguard.UI
"Bug and improvement reports will arrive later: the server needs a ticket section.", "Bug and improvement reports will arrive later: the server needs a ticket section.",
["Топ игроков по рейтингу появится позже: нужен серверный лидерборд."] = ["Топ игроков по рейтингу появится позже: нужен серверный лидерборд."] =
"The rating leaderboard will appear later: the server needs a leaderboard endpoint.", "The rating leaderboard will appear later: the server needs a leaderboard endpoint.",
["Создать"] = "Create",
["← К списку колод"] = "← Back to decks",
["Готова"] = "Ready",
["Не готова"] = "Not ready",
["Без триггера"] = "No trigger",
["Критический"] = "Critical",
["Добор"] = "Draw",
["Стойка"] = "Stand",
["Лечение"] = "Heal",
["Сбросить"] = "Reset",
["Применить"] = "Apply",
["Удалить колоду"] = "Delete deck",
["не сохранено"] = "not saved",
["Сохранено"] = "Saved",
["Кланы появятся после загрузки коллекции."] =
"Clans will appear after the collection loads.",
["Нужно ровно 50 карт, 16 триггеров и не больше 4 копий."] =
"Exactly 50 cards, 16 triggers and no more than 4 copies are required.",
}; };
private static readonly KeyValuePair<string, string>[] Phrases = Exact private static readonly KeyValuePair<string, string>[] Phrases = Exact

View File

@ -57,6 +57,9 @@ namespace Vanguard.Tests
Assert.IsTrue(Exists("LobbyScreen")); Assert.IsTrue(Exists("LobbyScreen"));
Assert.IsTrue(Exists("OnboardingScreen")); Assert.IsTrue(Exists("OnboardingScreen"));
Assert.IsTrue(Exists("DeckBuilderScreen")); Assert.IsTrue(Exists("DeckBuilderScreen"));
Assert.IsTrue(Exists("DeckListPanel"), "Нет панели списка колод");
Assert.IsTrue(Exists("DeckPanel"), "Нет панели колоды");
Assert.IsTrue(Exists("FilterOverlay"), "Нет оверлея фильтров");
Assert.IsTrue(Exists("StoreScreen")); Assert.IsTrue(Exists("StoreScreen"));
Assert.IsTrue(Exists("HistoryScreen")); Assert.IsTrue(Exists("HistoryScreen"));
Assert.IsTrue(Exists("ReplayScreen")); Assert.IsTrue(Exists("ReplayScreen"));

View File

@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Vanguard.Api.Contracts;
using Vanguard.UI;
namespace Vanguard.Tests
{
public sealed class DeckBuilderTests
{
private static CardCatalogItemDto Card(
string printingId,
string nameId,
int grade,
string clan,
string trigger = "None")
{
return new CardCatalogItemDto
{
PrintingId = printingId,
NameId = nameId,
NameEn = nameId,
NameRu = nameId,
Grade = grade,
Clan = clan,
Trigger = trigger,
};
}
private static Dictionary<string, CardCatalogItemDto> Catalog(
params CardCatalogItemDto[] cards)
{
return cards.ToDictionary(card => card.PrintingId);
}
[Test]
public void Empty_Filter_Matches_Every_Card()
{
var filter = new DeckFilter();
Assert.IsTrue(filter.IsEmpty);
Assert.IsTrue(filter.Matches(Card("p1", "A", 0, "RoyalPaladin")));
Assert.IsTrue(filter.Matches(Card("p2", "B", 3, "Kagero", "Heal")));
}
[Test]
public void Multi_Filter_Combines_Clan_Grade_And_Trigger()
{
var filter = new DeckFilter();
filter.Clans.Add("RoyalPaladin");
filter.Clans.Add("Kagero");
filter.Grades.Add(1);
filter.Grades.Add(2);
filter.Triggers.Add("Heal");
Assert.IsTrue(filter.Matches(Card("p1", "A", 1, "Kagero", "Heal")));
Assert.IsFalse(filter.Matches(Card("p2", "B", 1, "Kagero", "Draw")));
Assert.IsFalse(filter.Matches(Card("p3", "C", 0, "Kagero", "Heal")));
Assert.IsFalse(filter.Matches(Card("p4", "D", 1, "NovaGrappler", "Heal")));
Assert.IsFalse(filter.IsEmpty);
}
[Test]
public void GroupDeck_Sums_Printings_And_Sorts_By_Grade_Then_Name()
{
Loc.SetLanguage(UiLanguage.Ru);
var catalog = Catalog(
Card("p1", "Zeta", 2, "Kagero"),
Card("p2", "Alpha", 2, "Kagero"),
Card("p3", "Alpha", 2, "Kagero"),
Card("p4", "Beta", 0, "Kagero"));
var groups = DeckListModel.GroupDeck(
new[] { "p1", "p2", "p3", "p4" },
catalog);
Assert.AreEqual(3, groups.Count);
Assert.AreEqual("Beta", groups[0].NameId);
Assert.AreEqual("Alpha", groups[1].NameId);
Assert.AreEqual(2, groups[1].Count);
Assert.AreEqual("Zeta", groups[2].NameId);
}
[Test]
public void GroupDeck_Keeps_Unknown_Printing_Last()
{
var catalog = Catalog(Card("p1", "Alpha", 0, "Kagero"));
var groups = DeckListModel.GroupDeck(new[] { "missing", "p1" }, catalog);
Assert.AreEqual(2, groups.Count);
Assert.AreEqual("Alpha", groups[0].NameId);
Assert.AreEqual("missing", groups[1].NameId);
}
[Test]
public void Validate_Requires_Count_Triggers_And_Copy_Limit()
{
var catalog = new Dictionary<string, CardCatalogItemDto>();
var cards = new List<string>();
for (var i = 0; i < 50; i++)
{
var printingId = "p" + i;
catalog[printingId] = Card(
printingId,
"card" + i,
i % 4,
"RoyalPaladin",
i < 16 ? "Critical" : "None");
cards.Add(printingId);
}
var valid = DeckListModel.Validate(cards, catalog);
Assert.AreEqual(50, valid.Count);
Assert.AreEqual(16, valid.Triggers);
Assert.IsTrue(valid.IsValid);
var shortDeck = DeckListModel.Validate(cards.Take(49), catalog);
Assert.IsFalse(shortDeck.IsValid);
var copyDeck = new List<string>
{
"p0", "p0", "p0", "p0", "p0",
};
var copyValidation = DeckListModel.Validate(copyDeck, catalog);
Assert.IsFalse(copyValidation.MaxCopiesOk);
Assert.IsFalse(copyValidation.IsValid);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97caa8ca43fbef44fb292c3a777eb5f1

View File

@ -43,7 +43,7 @@
| TASK-2026-020 | `[x]` | Парсер карт и артов с официального сайта | DeepSeek: анализ и реализация | идемпотентность 0 diff; `validate` не запускался | | TASK-2026-020 | `[x]` | Парсер карт и артов с официального сайта | DeepSeek: анализ и реализация | идемпотентность 0 diff; `validate` не запускался |
| TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь | | TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь |
| TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь | | TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь |
| TASK-2026-023 | `[ ]` | DeckBuilder: один экран, мультифильтры и список колоды | DeepSeek: анализ и план готовы; реализация ожидает | не выполнена | | TASK-2026-023 | `[!]` | DeckBuilder: один экран, мультифильтры и список колоды | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 67/67; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная — пользователь |
Новые задачи добавляются в конец файла отдельными разделами. Не используй один Новые задачи добавляются в конец файла отдельными разделами. Не используй один
раздел для нескольких независимых задач. раздел для нескольких независимых задач.
@ -2348,11 +2348,11 @@ HUD с номером хода и фазовой шкалой, компактн
### TASK-2026-023 — DeckBuilder: один экран, мультифильтры и список колоды ### TASK-2026-023 — DeckBuilder: один экран, мультифильтры и список колоды
- **Статус:** `[ ]` - **Статус:** `[!]`
- **Создано:** 2026-10-31 - **Создано:** 2026-10-31
- **Анализ:** DeepSeek выполнен - **Анализ:** DeepSeek выполнен
- **Реализация:** не начата - **Реализация:** DeepSeek: C1–C11 выполнены
- **Проверка:** не выполнена - **Проверка:** EditMode 67/67; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная приёмка — за пользователем
- **Затронутые системы:** `Runtime/UI/DeckBuilderView.cs`, - **Затронутые системы:** `Runtime/UI/DeckBuilderView.cs`,
`Runtime/UI/CardImageService.cs`, `Localization.cs`, EditMode-тесты. `Runtime/UI/CardImageService.cs`, `Localization.cs`, EditMode-тесты.
Сервер, правила, протокол и YAML не меняются. Сервер, правила, протокол и YAML не меняются.
@ -2417,106 +2417,90 @@ HUD с номером хода и фазовой шкалой, компактн
#### C. Чек-лист изменений #### C. Чек-лист изменений
- [ ] **C1. `MODIFY` `Runtime/UI/CardImageService.cs`** — превью - [x] **C1. `MODIFY` `Runtime/UI/CardImageService.cs`** — превью
иллюстрации. иллюстрации.
- `ApplyIllustration(Image, imageKey, fallback)`: спрайт из верхней части - `ApplyIllustration(Image, imageKey, fallback)`: спрайт из верхней части
карты — `Rect(0, h * 0.41, w, h * 0.59)` для 350×511 (иллюстрация до карты — `Rect(0, h * 0.41, w, h * 0.59)` (350×511), с отдельным кешем.
полосы флейвора), с кешем по `imageKey`. - `preserveAspect = true`; при отсутствии арта — процедурный fallback.
- `preserveAspect = true`; выравнивание — центральное; при отсутствии - Полный арт (`Apply`) не изменён.
арта — процедурный fallback без обрезки. - Реализация: выполнена. Проверка: EditMode 67/67, ручная — позже.
- Полный арт (`Apply`) не меняется: он нужен карточке и 3D-картам.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет. - Зависит: нет.
- [ ] **C2. `MODIFY` `Runtime/UI/DeckBuilderView.cs`** — один экран. - [x] **C2. `MODIFY` `Runtime/UI/DeckBuilderView.cs`** — один экран.
- Убрать `_listView`/`_editorView` и `BackToList`; оставить один корень - Убраны `_listView`/`_editorView`; один корень `DeckBuilderScreen`:
`DeckBuilderScreen`: топбар (назад в Lobby, заголовок, фильтры, топбар (назад в Lobby, заголовок, сводка фильтров, фильтры, сброс) и
сброс, статус) и тело (коллекция + правая панель). тело (коллекция + правая панель 380 px).
- Правая панель: `DeckListPanel` и `DeckPanel` (карты колоды), видимость - Правая панель: `DeckListPanel` и `DeckPanel`; переключение без смены
переключается; при открытии колоды — `DeckPanel`, кнопка «К списку сцены; статус — под коллекцией.
колод» возвращает `DeckListPanel`. - Реализация: выполнена. Проверка: EditMode 67/67, панели в
- Ширина правой панели 380 px; содержимое — без пересоздания сцены. `Client_Views_Build`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C3, C4. - Зависит: C1, C3, C4.
- [ ] **C3. `MODIFY` список колод.** - [x] **C3. `MODIFY` список колод.**
- Строка: имя, `N/50`, `T N/16`, формат (`Classic`, локализовано), - Строка: имя, `N/50`, `T N/16`, `Classic`, `G0…G3`, бейдж
распределение `G0…G3`, бейдж «Готова / Не готова». «Готова / Не готова»; валидность считается локально.
- Валидность считать локально (50 карт, 16 триггеров, ≤4 по `nameId`). - Открытая колода подсвечена; «Создать» — в шапке панели.
- Подсветка открытой колоды; кнопка «Создать колоду» — над списком. - Реализация: выполнена. Проверка: EditMode 67/67, ручная — позже.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2. - Зависит: C2.
- [ ] **C4. `MODIFY` список карт колоды.** - [x] **C4. `MODIFY` список карт колоды.**
- Группировать по `nameId`, счётчик — сумма принтов; превью — первая - Группировка по `nameId`; счётчик — сумма принтов; превью — иллюстрация
карта группы. последнего принта группы.
- Строка 84 px: иллюстрация 64×54 (пропорция 1.18), имя (2 строки), - Строка 84 px: превью 64×54, имя, `G`, `×N`, триггер и клан; сортировка
бейдж `G`, `×N`, триггер. грейд → имя.
- Сортировка: `Grade` → `Loc.CardName` (`StringComparer.Ordinal`). - Клик — убрать копию; ПКМ — карточка.
- Клик по строке — убрать одну копию; ПКМ — карточка. Счётчики и - Реализация: выполнена. Проверка: `GroupDeck_*` тесты, EditMode 67/67.
валидация обновляются сразу.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C2. - Зависит: C1, C2.
- [ ] **C5. `MODIFY` фильтры — мультивыбор.** - [x] **C5. `MODIFY` фильтры — мультивыбор.**
- Хранить `HashSet<string> Clans`, `HashSet<int> Grades`, - `DeckFilter` с наборами клана/грейда/триггера; пустой набор = «все».
`HashSet<string> Triggers`; пустой набор = «все». - Оверлей: чипы с подсветкой, «Сбросить» (черновик), «Отмена»,
- Оверлей: группы чипов с подсветкой выбранного, «Все»/«Сбросить», «Применить»; сводка `Клан: … · Ранг: … · Триггер: …`.
«Применить» (применяет черновик) и «Отмена» (откатывает). - Применение пересобирает коллекцию; счётчики обновляются точечно.
- Сводка в топбаре: `Клан: RP, Kagero · Ранг: 0, 1 · Триггер: Heal`. - Реализация: выполнена. Проверка: тесты фильтра, EditMode 67/67.
- `MatchesFilter` проверяет вхождение во все активные наборы.
- После применения — пересборка коллекции; счётчики в ячейках
обновляются без полного пересоздания (если возможно).
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2, C6. - Зависит: C2, C6.
- [ ] **C6. `MODIFY` фильтрация и сортировка — чистый класс.** - [x] **C6. `CREATE` `Runtime/UI/DeckListFilter.cs`.**
- Вынести в `Runtime/UI/DeckListFilter.cs` (или статический helper): - `DeckFilter`, `DeckCardGroup`, `DeckValidation`, `DeckListModel`:
`Matches(meta, filter)`, `SortDeck(cards, catalog)`, `GroupDeck(...)`. `Matches`, `GroupDeck`, `Validate`, `IsTrigger`.
- Фильтр: наборы клана/грейда/триггера; сортировка колоды: грейд → имя; - Реализация: выполнена. Проверка: 4 теста `DeckBuilderTests`.
группировка по `nameId`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C4, C5. - Зависит: C4, C5.
- [ ] **C7. `MODIFY` ячейки коллекции.** - [x] **C7. `MODIFY` ячейки коллекции.**
- Иллюстрация через `ApplyIllustration`, область арта с правильной - Иллюстрация `ApplyIllustration`, область арта 174×144 без растяжения.
пропорцией (например, 174×147), без растяжения. - Счётчик по `nameId`; при лимите 4 или 16 триггеров цифры красные;
- Показывать `×в колоде` по `nameId`; при достижении лимита 4 или 16 добавление по-прежнему блокируется с сообщением.
триггеров — приглушать и не добавлять. - Реализация: выполнена. Проверка: EditMode 67/67, ручная — позже.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1, C5. - Зависит: C1, C5.
- [ ] **C8. `MODIFY` live-валидация.** - [x] **C8. `MODIFY` live-валидация.**
- `UpdateStats` дополнить статусом валидности и цветом счётчиков; - `UpdateStats`: счётчики, распределение, «не сохранено», цвет;
«Сохранить», «В бой», «С ботом» — `interactable` только при валидной «Сохранить» — при валидной колоде, «В бой»/«С ботом» — только для
сохранённой колоде. сохранённой валидной колоды; «Удалить» — для сохранённой.
- Ошибки лимитов — в статусе редактора, без модальных окон. - Ошибки — в статусе, без модальных окон.
- Реализация: не выполнена. Проверка: не выполнена. - Реализация: выполнена. Проверка: EditMode 67/67, ручная — позже.
- Зависит: C3, C4. - Зависит: C3, C4.
- [ ] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи. - [x] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- «Мои колоды», «К списку колод», «Готова», «Не готова», «Клан: », - «Создать», «К списку колод», «Готова/Не готова», варианты триггеров,
«Ранг: », «Триггер: », «Применить», «Отмена», «Все кланы», «Сбросить», «Применить», «Удалить колоду», «не сохранено», ошибки.
«Все ранги», «Все триггеры», «Иллюстрация» и подписи счётчиков. - Реализация: выполнена. Проверка: дубликатов ключей нет.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C2–C5. - Зависит: C2–C5.
- [ ] **C10. `TEST` EditMode.** - [x] **C10. `TEST` EditMode.**
- `DeckListFilter`: мультивыбор клана/грейда/триггера, пустой набор = - `DeckBuilderTests`: пустой фильтр, мультивыбор, группировка/сортировка,
все; сортировка грейд → имя; группировка по `nameId` суммирует принты. неизвестный принт, валидация 50/16/4.
- `CardImageService`: обрезка иллюстрации возвращает неполный rect - `Client_Views_Build`: проверены `DeckListPanel`, `DeckPanel`,
(проверка по спрайту/текстуре без реального файла — на fallback). `FilterOverlay`.
- UI: один экран, переключение панелей, сводка фильтров, бейдж - Реализация: выполнена. Проверка: EditMode **67/67** в копии без
валидности. `Library`.
- Существующий `Client_Views_Build` — без изменений ожиданий.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1–C9. - Зависит: C1–C9.
- [ ] **C11. `VERIFY` EditMode, smoke, standalone.** - [x] **C11. `VERIFY` EditMode, smoke, standalone.**
- Копия без `Library`; EditMode; `SCENE_SMOKE`; standalone. - Копия без `Library`; EditMode; `SCENE_SMOKE`; `PLAYMAT_SMOKE`.
- Ручная проверка: фильтры, список колод, редактор, добавление/удаление, - Реализация: выполнена. Проверка: EditMode 67/67,
сохранение, «В бой»/«С ботом», ПКМ-карточка. `SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True`; ручная
- Реализация: не выполнена. Проверка: не выполнена. проверка — за пользователем.
- Зависит: C10. - Зависит: C10.
#### D. Последовательность выполнения #### D. Последовательность выполнения
@ -2565,11 +2549,22 @@ HUD с номером хода и фазовой шкалой, компактн
#### G. Результат #### G. Результат
- **Реализовано:** — - **Реализовано:** C1–C11. DeckBuilder — один экран: коллекция с
- **Проверки:** — мультифильтрами (клан/грейд/триггер), справа панель «Мои колоды» ⇄
- **Не завершено/заблокировано:** — «Колода». Список колод показывает `50/50`, триггеры, грейды и
- **Skill обновлён:** — валидность; список колоды группируется по `nameId`, сортируется
- **Git:** — грейд → имя, превью — иллюстрация карты без искажений.
Live-валидация управляет «Сохранить»/«В бой»/«С ботом».
- **Проверки:** EditMode **67/67** в копии без `Library` (+5 тестов
`DeckBuilderTests`); `SCENE_SMOKE success=True`;
`PLAYMAT_SMOKE success=True`.
- **Не завершено:** ручная проверка фильтров, переключения панелей,
добавления/удаления, сохранения, «В бой»/«С ботом», ПКМ-карточки.
- **Замечания:** граница иллюстрации 0.59 подобрана по BT01/TD01
(350×511); при другом макете карты константу нужно проверить. Правка
имени колоды помечает колоду как несохранённую.
- **Skill обновлён:** не требуется.
- **Git:** не коммитилось.

View File

@ -6,6 +6,29 @@
--- ---
## 2026-10-31 — TASK-2026-023: переделка DeckBuilder
- Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄
«Колода»; переключение без смены сцены.
- Мультифильтры клана, грейда и триггера в модальном оверлее: чипы,
черновик, «Сбросить», «Отмена», «Применить»; сводка в топбаре.
- Список колод: имя, `N/50`, `T N/16`, `Classic`, `G0…G3`, бейдж
«Готова / Не готова», подсветка открытой.
- Список карт колоды: группировка по `nameId`, сортировка грейд → имя,
превью-иллюстрация (верхние 59% карты) без искажений, имя, `×N`, G,
триггер и клан.
- Live-валидация 50/16/4 управляет «Сохранить», «В бой», «С ботом»;
изменение имени помечает колоду как несохранённую.
- Новые: `CardImageService.ApplyIllustration`, `DeckListFilter.cs`
(`DeckFilter`, `DeckListModel`, `DeckValidation`), 5 тестов
`DeckBuilderTests`.
- Проверки: EditMode **67/67**, `SCENE_SMOKE success=True`,
`PLAYMAT_SMOKE success=True`, standalone собран.
- Ручная проверка (фильтры, панели, добавление/удаление, сохранение,
«В бой»/«С ботом», ПКМ) — за пользователем.
---
## 2026-10-31 — TASK-2026-022: Lobby по референсу ## 2026-10-31 — TASK-2026-022: Lobby по референсу
- `Lobby` переведён на 2D-дашборд: процедурный фон, топбар (инициалы, ник, - `Lobby` переведён на 2D-дашборд: процедурный фон, топбар (инициалы, ник,

View File

@ -61,17 +61,17 @@
- Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`, - Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`,
`PlayerState`, `GameConnection`. `PlayerState`, `GameConnection`.
- Работает: логин/регистрация/refresh, онбординг, коллекция с разбором, - Работает: логин/регистрация/refresh, онбординг, коллекция с разбором,
декбилдер в духе HS (список колод справа, карточная сетка коллекции, декбилдер (слева карточная сетка коллекции с мультифильтрами, справа
фильтры в оверлее, сортировка клан→ранг→имя, редактор с вертикальным панель «Мои колоды» ⇄ «Колода»; список карт колоды — грейд→имя,
списком карт колоды и статистикой по рангам/общему числу), превью-иллюстрации), магазин (бустер/триал-дека), история матчей и
магазин (бустер/триал-дека), история матчей и реплей, очередь, реплей, очередь, WebSocket-матч (snapshot, интенты, маллиган, сдача,
WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`), `GameFinished`), таймер, результат.
таймер, результат. - Декбилдер: `DeckBuilderView` (один экран), `DeckListFilter`
- Декбилдер: `DeckBuilderView`; каталог метаданных — `GET /api/cards` (`DeckFilter`/`DeckListModel`); каталог метаданных — `GET /api/cards`
(grade, trigger, clan, rarity, printing, power/shield/type, текст (grade, trigger, clan, rarity, printing, power/shield/type, текст
способности), клиент объединяет с коллекцией. способности), клиент объединяет с коллекцией.
Изображения карт — реальные арты из `Resources/CardArt` (124/124), с Превью карт — иллюстрация (верхние 59% арта) без искажений; полный арт
процедурным fallback по цвету клана. остаётся в карточке и 3D-поле.
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и - ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
крафтом; SP/TD не разбираются и не крафтятся. крафтом; SP/TD не разбираются и не крафтятся.
- Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и - Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и
@ -285,7 +285,7 @@ Standalone Windows:
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
``` ```
Клиентские проверки: **62/62** EditMode (включая новые тесты TASK-2026-001: Клиентские проверки: **67/67** EditMode (включая новые тесты TASK-2026-001:
повторный render скрытых карт, открытие урона, source-позиция нового юнита, повторный render скрытых карт, открытие урона, source-позиция нового юнита,
coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод
строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила
@ -376,9 +376,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
**TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной **TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной
приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone
собраны и запущены. **TASK-2026-023 — DeckBuilder (один экран, собраны и запущены. **TASK-2026-023 — DeckBuilder (один экран,
мультифильтры, список колоды): анализ и план готовы**, реализация — мультифильтры, список колоды): реализован C1–C11, ждёт ручной
следующим проходом. После приёмки — **TASK-2026-019** (загрузочная приёмки.** EditMode 67/67, `SCENE_SMOKE`, `PLAYMAT_SMOKE`; standalone.
сцена, fade, кадр без покадровых аллокаций). После приёмки — **TASK-2026-019** (загрузочная сцена, fade, кадр без
покадровых аллокаций).
2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride, 2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride,
move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого
урона, рука противника, повторные клики UI во время/после анимации. урона, рука противника, повторные клики UI во время/после анимации.
@ -455,7 +456,8 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
multi-target Soul Blast/прочие стоимости, отдельный `CostPaymentOverlay`, multi-target Soul Blast/прочие стоимости, отдельный `CostPaymentOverlay`,
world-space Power/Shield (верхний strip принят как HUD), world-space world-space Power/Shield (верхний strip принят как HUD), world-space
dash shader; полировка позиций на конкретном мониторе. dash shader; полировка позиций на конкретном мониторе.
4. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты. 4. Декбилдер: drag&drop карт, пресеты, предупреждение о
несохранённых изменениях, «Все/Ничего» по секциям фильтра.
5. 3D-наполнение: модели карт/стола, prefab-экраны сцен, анимации retire/ghost. 5. 3D-наполнение: модели карт/стола, prefab-экраны сцен, анимации retire/ghost.
6. Звук и настройки, DOTween. 6. Звук и настройки, DOTween.
7. Лаунчер WPF. 7. Лаунчер WPF.