client: DeckBuilder — фильтры, карты вне коллекции и «Распылить всё»
CI / test (push) Successful in 5m53s
Details
CI / test (push) Successful in 5m53s
Details
- Кнопка «Сброс» убрана из строки фильтров; сброс остался в оверлее. - В фильтрах добавлен режим «Показывать вне коллекции»: карты без копий показываются серыми, без добавления в колоду (ПКМ — карточка). - Рядом с «Фильтры» — «Распылить всё», если есть копии сверх 4 на название (SP пропускаются). Подтверждение показывает количество и пыль. - DeckListModel.PlanExtras + тест; EditMode 70/70, standalone собран.
This commit is contained in:
parent
c7f2c51fec
commit
42e57f5ac1
|
|
@ -50,6 +50,8 @@ namespace Vanguard.UI
|
|||
private Button _botButton;
|
||||
private Button _duelButton;
|
||||
private Button _deleteButton;
|
||||
private Button _dismantleAllButton;
|
||||
private RectTransform _unownedChips;
|
||||
private RectTransform _deckMenuOverlay;
|
||||
private Text _deckMenuTitle;
|
||||
private DeckDto _deckMenuDeck;
|
||||
|
|
@ -61,8 +63,9 @@ namespace Vanguard.UI
|
|||
private RectTransform _clanChips;
|
||||
private RectTransform _confirmOverlay;
|
||||
private InputField _searchInput;
|
||||
private Text _confirmTitle;
|
||||
private Text _confirmText;
|
||||
private DeckDto _pendingDelete;
|
||||
private Action _confirmAction;
|
||||
|
||||
private RectTransform _cardOverlay;
|
||||
private Image _cardArt;
|
||||
|
|
@ -200,8 +203,15 @@ namespace Vanguard.UI
|
|||
var filters = UiKit.CreateButton(filterBar, "Filters", "Фильтры", OpenFilters, 16);
|
||||
SetFixedSize(filters, 110f, 38f);
|
||||
|
||||
var reset = UiKit.CreateButton(filterBar, "ResetFilters", "Сброс", ResetFilters, 16);
|
||||
SetFixedSize(reset, 90f, 38f);
|
||||
_dismantleAllButton = UiKit.CreateButton(
|
||||
filterBar,
|
||||
"DismantleAll",
|
||||
"Распылить всё",
|
||||
ConfirmDismantleExtras,
|
||||
15);
|
||||
SetFixedSize(_dismantleAllButton, 200f, 38f);
|
||||
SetButtonColor(_dismantleAllButton, new Color(0.30f, 0.16f, 0.08f, 0.95f));
|
||||
_dismantleAllButton.gameObject.SetActive(false);
|
||||
|
||||
var scroll = UiKit.CreateGridScrollView(
|
||||
collectionArea,
|
||||
|
|
@ -334,7 +344,7 @@ namespace Vanguard.UI
|
|||
var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f));
|
||||
|
||||
var card = UiKit.CreateCenteredCard(
|
||||
overlay, "FilterCard", new Vector2(900f, 940f), PanelFill);
|
||||
overlay, "FilterCard", new Vector2(900f, 1000f), PanelFill);
|
||||
UiKit.AddVerticalLayout(card, 24, 10);
|
||||
|
||||
var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary);
|
||||
|
|
@ -345,10 +355,11 @@ namespace Vanguard.UI
|
|||
UiKit.SetPreferredHeight(_searchInput, 44f);
|
||||
_searchInput.onValueChanged.AddListener(value => _draftFilter.Search = value);
|
||||
|
||||
_unownedChips = BuildFilterSection(card, "Отображение", 44f);
|
||||
_rarityChips = BuildFilterSection(card, "Редкость", 44f);
|
||||
_gradeChips = BuildFilterSection(card, "Ранг", 44f);
|
||||
_triggerChips = BuildFilterSection(card, "Триггер", 44f);
|
||||
_clanChips = BuildFilterSection(card, "Клан", 260f, flexible: true);
|
||||
_clanChips = BuildFilterSection(card, "Клан", 220f, flexible: true);
|
||||
|
||||
var footer = UiKit.CreatePanel(card, "Footer", new Color(0f, 0f, 0f, 0f));
|
||||
UiKit.SetPreferredHeight(footer, 48f);
|
||||
|
|
@ -433,18 +444,19 @@ namespace Vanguard.UI
|
|||
_root, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.78f));
|
||||
|
||||
var card = UiKit.CreateCenteredCard(
|
||||
overlay, "ConfirmCard", new Vector2(640f, 300f), PanelFill);
|
||||
overlay, "ConfirmCard", new Vector2(640f, 320f), PanelFill);
|
||||
UiKit.AddVerticalLayout(card, 30, 12);
|
||||
|
||||
var title = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, TextPrimary);
|
||||
title.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(title, 42f);
|
||||
_confirmTitle = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, TextPrimary);
|
||||
_confirmTitle.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(_confirmTitle, 42f);
|
||||
|
||||
_confirmText = UiKit.CreateText(
|
||||
card, "Text", string.Empty, 19, TextMuted);
|
||||
_confirmText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(_confirmText, 70f);
|
||||
|
||||
var yes = UiKit.CreateButton(card, "Yes", "Удалить", ConfirmDelete, 20);
|
||||
var yes = UiKit.CreateButton(card, "Yes", "Подтвердить", ConfirmAction, 20);
|
||||
UiKit.SetPreferredHeight(yes, 48f);
|
||||
|
||||
var no = UiKit.CreateButton(card, "No", "Отмена", CancelDelete, 20);
|
||||
|
|
@ -877,8 +889,26 @@ namespace Vanguard.UI
|
|||
ClearChildren(_collectionContent);
|
||||
_countTexts.Clear();
|
||||
|
||||
var items = _collection
|
||||
.Select(item => new { Item = item, Meta = MetaOf(item) })
|
||||
var ownedByPrinting = new Dictionary<string, CollectionItemDto>(StringComparer.Ordinal);
|
||||
foreach (var item in _collection)
|
||||
ownedByPrinting[item.PrintingId] = item;
|
||||
|
||||
var source = _filter.IncludeUnowned
|
||||
? _catalog.Values.Select(meta => new
|
||||
{
|
||||
Meta = meta,
|
||||
Item = ownedByPrinting.TryGetValue(meta.PrintingId, out var owned)
|
||||
? owned
|
||||
: new CollectionItemDto
|
||||
{
|
||||
PrintingId = meta.PrintingId,
|
||||
NameId = meta.NameId,
|
||||
Count = 0,
|
||||
},
|
||||
})
|
||||
: _collection.Select(item => new { Meta = MetaOf(item), Item = item });
|
||||
|
||||
var items = source
|
||||
.Where(pair => pair.Meta != null && _filter.Matches(pair.Meta))
|
||||
.OrderBy(pair => pair.Meta.Clan, StringComparer.Ordinal)
|
||||
.ThenBy(pair => pair.Meta.Grade)
|
||||
|
|
@ -886,6 +916,8 @@ namespace Vanguard.UI
|
|||
|
||||
foreach (var pair in items)
|
||||
AddCollectionCell(pair.Item, pair.Meta);
|
||||
|
||||
UpdateDismantleButton();
|
||||
}
|
||||
|
||||
private void AddCollectionCell(CollectionItemDto item, CardCatalogItemDto meta)
|
||||
|
|
@ -899,7 +931,7 @@ namespace Vanguard.UI
|
|||
frame.effectColor = CardArt.ClanColor(meta.Clan);
|
||||
frame.effectDistance = new Vector2(2f, -2f);
|
||||
|
||||
if (_deckOpen)
|
||||
if (_deckOpen && item.Count > 0)
|
||||
{
|
||||
var button = cell.gameObject.AddComponent<Button>();
|
||||
button.targetGraphic = cell.GetComponent<Image>();
|
||||
|
|
@ -908,10 +940,13 @@ namespace Vanguard.UI
|
|||
|
||||
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
||||
UiKit.SetPreferredHeight(art, 234f);
|
||||
var artImage = art.GetComponent<Image>();
|
||||
CardImageService.Apply(
|
||||
art.GetComponent<Image>(),
|
||||
artImage,
|
||||
meta.ImageKey,
|
||||
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||
if (item.Count <= 0)
|
||||
artImage.color = new Color(0.45f, 0.45f, 0.45f, 1f);
|
||||
|
||||
var qty = UiKit.CreatePanel(cell, "Qty", new Color(0.02f, 0.05f, 0.12f, 0.95f));
|
||||
UiKit.SetPreferredHeight(qty, 46f);
|
||||
|
|
@ -943,7 +978,9 @@ namespace Vanguard.UI
|
|||
var atCap = _deckOpen
|
||||
&& (used >= 4 || (meta != null && IsTrigger(meta.Trigger) && triggerCap));
|
||||
pair.Value.text = CountText(item);
|
||||
pair.Value.color = atCap
|
||||
pair.Value.color = item.Count <= 0
|
||||
? new Color(0.50f, 0.50f, 0.50f)
|
||||
: atCap
|
||||
? new Color(1f, 0.55f, 0.45f)
|
||||
: new Color(0.90f, 0.86f, 0.70f);
|
||||
}
|
||||
|
|
@ -1032,15 +1069,16 @@ namespace Vanguard.UI
|
|||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
private void ResetFilters()
|
||||
{
|
||||
_filter.Clear();
|
||||
UpdateFilterSummary();
|
||||
RebuildCollection();
|
||||
}
|
||||
|
||||
private void RebuildFilterChips()
|
||||
{
|
||||
ClearChildren(_unownedChips);
|
||||
Chip(
|
||||
_unownedChips,
|
||||
"Показывать вне коллекции",
|
||||
_draftFilter.IncludeUnowned,
|
||||
ToggleUnowned,
|
||||
360f);
|
||||
|
||||
ClearChildren(_rarityChips);
|
||||
foreach (var rarity in new[] { "C", "R", "RR", "RRR", "SP" })
|
||||
{
|
||||
|
|
@ -1130,6 +1168,12 @@ namespace Vanguard.UI
|
|||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
private void ToggleUnowned()
|
||||
{
|
||||
_draftFilter.IncludeUnowned = !_draftFilter.IncludeUnowned;
|
||||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
private static Button Chip(
|
||||
Transform parent,
|
||||
string label,
|
||||
|
|
@ -1174,6 +1218,8 @@ namespace Vanguard.UI
|
|||
parts.Add("Триггер: " + string.Join(", ", _filter.Triggers.OrderBy(item => item, StringComparer.Ordinal)));
|
||||
if (!string.IsNullOrWhiteSpace(_filter.Search))
|
||||
parts.Add("Поиск: " + _filter.Search.Trim());
|
||||
if (_filter.IncludeUnowned)
|
||||
parts.Add("вне коллекции");
|
||||
|
||||
_filterSummary.text = string.Join(" · ", parts);
|
||||
}
|
||||
|
|
@ -1420,25 +1466,88 @@ namespace Vanguard.UI
|
|||
|
||||
private void RequestDelete(DeckDto deck)
|
||||
{
|
||||
_pendingDelete = deck;
|
||||
_confirmTitle.text = "Удалить колоду?";
|
||||
_confirmText.text = deck.Name + "\nКолода будет удалена без восстановления.";
|
||||
_confirmAction = () => DeleteDeck(deck);
|
||||
ShowConfirm();
|
||||
}
|
||||
|
||||
private void ConfirmDismantleExtras()
|
||||
{
|
||||
var plan = DeckListModel.PlanExtras(_collection);
|
||||
if (plan.IsEmpty || _busy)
|
||||
return;
|
||||
|
||||
_confirmTitle.text = "Распылить лишние карты?";
|
||||
_confirmText.text = "Будет распылено " + plan.CardCount
|
||||
+ " карт на " + plan.Dust + " пыли.";
|
||||
_confirmAction = () => DismantleExtras(plan);
|
||||
ShowConfirm();
|
||||
}
|
||||
|
||||
private void ShowConfirm()
|
||||
{
|
||||
_confirmOverlay.SetAsLastSibling();
|
||||
_confirmOverlay.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void CancelDelete()
|
||||
{
|
||||
_pendingDelete = null;
|
||||
_confirmAction = null;
|
||||
_confirmOverlay.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ConfirmDelete()
|
||||
private void ConfirmAction()
|
||||
{
|
||||
var deck = _pendingDelete;
|
||||
var action = _confirmAction;
|
||||
_confirmAction = null;
|
||||
_confirmOverlay.gameObject.SetActive(false);
|
||||
_pendingDelete = null;
|
||||
if (deck != null)
|
||||
DeleteDeck(deck);
|
||||
action?.Invoke();
|
||||
}
|
||||
|
||||
private async void DismantleExtras(DismantlePlan plan)
|
||||
{
|
||||
if (_busy || plan == null || plan.IsEmpty)
|
||||
return;
|
||||
|
||||
_busy = true;
|
||||
UpdateDismantleButton();
|
||||
SetStatus("Распыляем…", false);
|
||||
try
|
||||
{
|
||||
await _auth.EnsureAccessAsync();
|
||||
foreach (var item in plan.Items)
|
||||
await _meta.DismantleAsync(item.PrintingId, item.Count);
|
||||
|
||||
await ReloadCollectionAsync(null);
|
||||
SetStatus(
|
||||
"Распылено " + plan.CardCount + " карт на " + plan.Dust + " пыли.",
|
||||
false);
|
||||
}
|
||||
catch (ApiException exception)
|
||||
{
|
||||
SetStatus(exception.Message, true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
SetStatus("Не удалось распылить карты.", true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
UpdateDismantleButton();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDismantleButton()
|
||||
{
|
||||
if (_dismantleAllButton == null)
|
||||
return;
|
||||
|
||||
var plan = DeckListModel.PlanExtras(_collection);
|
||||
_dismantleAllButton.gameObject.SetActive(!plan.IsEmpty);
|
||||
_dismantleAllButton.interactable = !plan.IsEmpty && !_busy;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ карточка
|
||||
|
|
@ -1500,7 +1609,7 @@ namespace Vanguard.UI
|
|||
_craftButton.interactable = canChange && !_busy && _dust >= craftCost;
|
||||
SetButtonLabel(
|
||||
_dismantleButton,
|
||||
canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
|
||||
canChange ? "Разобрать (+" + DeckListModel.DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
|
||||
SetButtonLabel(
|
||||
_craftButton,
|
||||
canChange
|
||||
|
|
@ -1586,7 +1695,12 @@ namespace Vanguard.UI
|
|||
_dust = collection.Balances != null ? collection.Balances.Dust : 0;
|
||||
RebuildCollection();
|
||||
RefreshCounts();
|
||||
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
|
||||
UpdateDismantleButton();
|
||||
|
||||
_selectedItem = printingId == null
|
||||
? null
|
||||
: _collection.Find(item => item.PrintingId == printingId);
|
||||
|
||||
if (_selectedItem == null)
|
||||
{
|
||||
_cardOverlay.gameObject.SetActive(false);
|
||||
|
|
@ -1638,15 +1752,6 @@ namespace Vanguard.UI
|
|||
private CardCatalogItemDto MetaOf(CollectionItemDto item) =>
|
||||
_catalog.TryGetValue(item.PrintingId, out var meta) ? meta : null;
|
||||
|
||||
private static int DustFor(string rarity) => rarity switch
|
||||
{
|
||||
"C" => 5,
|
||||
"R" => 10,
|
||||
"RR" => 25,
|
||||
"RRR" => 100,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static int CraftCostFor(string rarity) => rarity switch
|
||||
{
|
||||
"C" => 20,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ namespace Vanguard.UI
|
|||
public HashSet<string> Triggers { get; } = new HashSet<string>(StringComparer.Ordinal);
|
||||
public HashSet<string> Rarities { get; } = new HashSet<string>(StringComparer.Ordinal);
|
||||
public string Search { get; set; } = string.Empty;
|
||||
public bool IncludeUnowned { get; set; }
|
||||
|
||||
public bool IsEmpty => Clans.Count == 0 && Grades.Count == 0 && Triggers.Count == 0
|
||||
&& Rarities.Count == 0 && string.IsNullOrWhiteSpace(Search);
|
||||
&& Rarities.Count == 0 && string.IsNullOrWhiteSpace(Search) && !IncludeUnowned;
|
||||
|
||||
public bool Matches(CardCatalogItemDto meta)
|
||||
{
|
||||
|
|
@ -54,6 +55,7 @@ namespace Vanguard.UI
|
|||
Triggers.Clear();
|
||||
Rarities.Clear();
|
||||
Search = string.Empty;
|
||||
IncludeUnowned = false;
|
||||
}
|
||||
|
||||
public void CopyFrom(DeckFilter other)
|
||||
|
|
@ -67,6 +69,7 @@ namespace Vanguard.UI
|
|||
Triggers.UnionWith(other.Triggers);
|
||||
Rarities.UnionWith(other.Rarities);
|
||||
Search = other.Search ?? string.Empty;
|
||||
IncludeUnowned = other.IncludeUnowned;
|
||||
}
|
||||
|
||||
public DeckFilter Clone()
|
||||
|
|
@ -98,6 +101,35 @@ namespace Vanguard.UI
|
|||
public bool IsValid => Count == 50 && Triggers == 16 && MaxCopiesOk;
|
||||
}
|
||||
|
||||
public sealed class DismantlePlanItem
|
||||
{
|
||||
public string PrintingId { get; set; } = string.Empty;
|
||||
public int Count { get; set; }
|
||||
public int Dust { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Лишние копии сверх 4 на название, которые можно распылить.</summary>
|
||||
public sealed class DismantlePlan
|
||||
{
|
||||
public List<DismantlePlanItem> Items { get; } = new List<DismantlePlanItem>();
|
||||
public int CardCount { get; private set; }
|
||||
public int Dust { get; private set; }
|
||||
|
||||
public bool IsEmpty => Items.Count == 0;
|
||||
|
||||
public void Add(string printingId, int count, int dust)
|
||||
{
|
||||
Items.Add(new DismantlePlanItem
|
||||
{
|
||||
PrintingId = printingId,
|
||||
Count = count,
|
||||
Dust = dust,
|
||||
});
|
||||
CardCount += count;
|
||||
Dust += dust;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Чистые правила фильтрации, группировки и проверки колоды.</summary>
|
||||
public static class DeckListModel
|
||||
{
|
||||
|
|
@ -172,5 +204,48 @@ namespace Vanguard.UI
|
|||
public static bool IsTrigger(string trigger) =>
|
||||
!string.IsNullOrEmpty(trigger)
|
||||
&& !string.Equals(trigger, "None", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// План распыления лишних копий: всё сверх 4 на название. SP/TD не
|
||||
/// распыляются, поэтому пропускаются. Сначала берутся карты с меньшей
|
||||
/// ценой распыления, чтобы сохранить ценные.
|
||||
/// </summary>
|
||||
public static DismantlePlan PlanExtras(IEnumerable<CollectionItemDto> collection)
|
||||
{
|
||||
var plan = new DismantlePlan();
|
||||
if (collection == null)
|
||||
return plan;
|
||||
|
||||
foreach (var group in collection.GroupBy(item => item.NameId))
|
||||
{
|
||||
var total = group.Sum(item => item.Count);
|
||||
var extra = total - 4;
|
||||
if (extra <= 0)
|
||||
continue;
|
||||
|
||||
foreach (var item in group
|
||||
.Where(item => DustFor(item.Rarity) > 0 && item.Count > 0)
|
||||
.OrderBy(item => DustFor(item.Rarity)))
|
||||
{
|
||||
if (extra <= 0)
|
||||
break;
|
||||
|
||||
var take = Math.Min(extra, item.Count);
|
||||
plan.Add(item.PrintingId, take, take * DustFor(item.Rarity));
|
||||
extra -= take;
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
public static int DustFor(string rarity) => rarity switch
|
||||
{
|
||||
"C" => 5,
|
||||
"R" => 10,
|
||||
"RR" => 25,
|
||||
"RRR" => 100,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,6 +440,18 @@ namespace Vanguard.UI
|
|||
["есть"] = "owned",
|
||||
["в колоде"] = "in deck",
|
||||
["Открыть"] = "Open",
|
||||
["Распылить всё"] = "Dismantle all",
|
||||
["Показывать вне коллекции"] = "Show cards not owned",
|
||||
["Распылить лишние карты?"] = "Dismantle excess cards?",
|
||||
["Будет распылено "] = "Will dismantle ",
|
||||
[" карт на "] = " cards for ",
|
||||
[" пыли."] = " dust.",
|
||||
["Распыляем…"] = "Dismantling…",
|
||||
["Распылено "] = "Dismantled ",
|
||||
["Не удалось распылить карты."] = "Could not dismantle the cards.",
|
||||
["вне коллекции"] = "not owned",
|
||||
["Подтвердить"] = "Confirm",
|
||||
["Отображение"] = "Display",
|
||||
};
|
||||
|
||||
private static readonly KeyValuePair<string, string>[] Phrases = Exact
|
||||
|
|
|
|||
|
|
@ -141,6 +141,24 @@ namespace Vanguard.Tests
|
|||
Assert.AreEqual("missing", groups[1].NameId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlanExtras_Takes_Copies_Above_Four_And_Skips_SP()
|
||||
{
|
||||
var collection = new List<CollectionItemDto>
|
||||
{
|
||||
new CollectionItemDto { PrintingId = "a", NameId = "A", Rarity = "C", Count = 5 },
|
||||
new CollectionItemDto { PrintingId = "b", NameId = "B", Rarity = "SP", Count = 6 },
|
||||
new CollectionItemDto { PrintingId = "c", NameId = "C", Rarity = "RR", Count = 3 },
|
||||
};
|
||||
|
||||
var plan = DeckListModel.PlanExtras(collection);
|
||||
|
||||
Assert.AreEqual(1, plan.CardCount);
|
||||
Assert.AreEqual(5, plan.Dust);
|
||||
Assert.AreEqual(1, plan.Items.Count);
|
||||
Assert.AreEqual("a", plan.Items[0].PrintingId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_Requires_Count_Triggers_And_Copy_Limit()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue