diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index cb84aca..b8651fe 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -266,13 +266,15 @@ ## 8. Клиент: боевая сцена `[20–30 дней]` -**Первый gray-box есть:** доска, рука, сила/крит, часы, все легальные -интенты кнопками, сдача и результат. Арты и анимации — следующие. -Финальный визуал — отдельная 3D-сцена `Battle`, а не текущий Canvas +**Визуальный каркас есть:** доска, рука, карточные рамки/арт-заглушки +по клану, сила/крит, часы, все легальные интенты кнопками, сдача и +результат. Реальные арты и анимации — следующие. Финальный визуал — +отдельная 3D-сцена `Battle`, а не текущий Canvas (`docs/client-visual-direction.md`). -- [x] Поле: Vanguard, 5 кругов Арьергарда, рука, damage/soul/drop-счётчики - (без артов и анимаций). +- [x] Поле: Vanguard + 5 кругов Арьергарда, рука, damage/soul/drop-счётчики. +- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер, + сила/крит; REST/скрытая карта. - [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»). - [x] Ввод: кнопки легальных интентов; подсветка целей — дальше. - [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером. diff --git a/README.md b/README.md index d5d7b22..616fa35 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ | CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` | | EF-миграции | готово (`InitialCreate`) | | Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` | -| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, gray-box бой | +| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, бой с процедурными картами | | Лаунчер | не начато | ## Требования diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index 8fc828f..c4c9afe 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -27,6 +27,9 @@ namespace Vanguard.UI private readonly MetaService _meta; private readonly Action _onExit; + private readonly Dictionary _catalog = + new Dictionary(StringComparer.Ordinal); + private bool _catalogLoading; private readonly RectTransform _root; private readonly RectTransform _board; @@ -138,6 +141,7 @@ namespace Vanguard.UI _topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId; SetStatus("Подключаемся к матчу…", Muted); _root.gameObject.SetActive(true); + _ = LoadCatalogAsync(); _ = ConnectAsync(); } @@ -191,6 +195,31 @@ namespace Vanguard.UI return overlay; } + private async Task LoadCatalogAsync() + { + if (_catalogLoading || _catalog.Count > 0) + return; + + _catalogLoading = true; + try + { + var items = await _meta.GetCatalogAsync(); + foreach (var item in items) + _catalog[item.PrintingId] = item; + + if (_root.gameObject.activeSelf && _view != null) + Render(); + } + catch (Exception exception) + { + Debug.LogWarning("Battle catalog load failed: " + exception.Message); + } + finally + { + _catalogLoading = false; + } + } + private async Task ConnectAsync() { if (_connecting || _info == null) @@ -430,9 +459,9 @@ namespace Vanguard.UI var self = _view.Players.FirstOrDefault(player => player.Index == _view.You) ?? (_view.Players.Count > _view.You ? _view.Players[_view.You] : null); - AddPlayerBlock(opponent, false); - AddMatchInfo(_view); - AddPlayerBlock(self, true); + AddFieldBlock(opponent, false); + AddCenterPanel(_view); + AddFieldBlock(self, true); AddHand(self); RebuildIntents(_view); UpdateClock(); @@ -441,167 +470,270 @@ namespace Vanguard.UI ShowResult(_view); } - private void AddPlayerBlock(PlayerView player, bool self) + private void AddFieldBlock(PlayerView player, bool self) { if (player == null) return; var block = UiKit.CreatePanel( _board, - self ? "SelfBlock" : "OpponentBlock", - Panel); + self ? "SelfField" : "OpponentField", + new Color(0.05f, 0.07f, 0.11f, 0.96f)); var blockElement = block.gameObject.AddComponent(); - blockElement.preferredHeight = self ? 250f : 230f; - UiKit.AddVerticalLayout(block, 8, 4); + blockElement.preferredHeight = self ? 214f : 204f; + UiKit.AddVerticalLayout(block, 7, 3); var header = UiKit.CreateText( block, "Header", - (self ? "Вы: " : "Противник: ") + player.Name - + " · рука " + player.HandCount - + " · колода " + player.DeckCount - + " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count) - + " · soul " + player.Soul.Count - + " · drop " + player.Drop.Count, - 17, - self ? Color.white : new Color(0.86f, 0.80f, 0.72f), + (self ? "ВЫ" : "ПРОТИВНИК") + " · " + player.Name + + " рука " + player.HandCount + + " · колода " + player.DeckCount + + " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count) + + " · soul " + player.Soul.Count + + " · drop " + player.Drop.Count, + 15, + self ? Color.white : new Color(0.94f, 0.82f, 0.70f), TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(header, 26f); + header.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(header, 25f); var row = UiKit.CreatePanel(block, "Field", new Color(0f, 0f, 0f, 0f)); var rowElement = row.gameObject.AddComponent(); rowElement.flexibleHeight = 1f; - rowElement.minHeight = 150f; - UiKit.AddHorizontalLayout(row, 0, 6); + rowElement.minHeight = 166f; + UiKit.AddHorizontalLayout(row, 3, 7); - AddCardBox(row, player.Vanguard, "Vanguard"); + AddCardBox(row, player.Vanguard, "V", compact: false); for (var i = 0; i < 5; i++) { var card = player.RearGuards != null && i < player.RearGuards.Length ? player.RearGuards[i] : null; - AddCardBox(row, card, "R" + (i + 1)); + AddCardBox(row, card, "R" + (i + 1), compact: false); } } + private void AddCenterPanel(MatchView view) + { + var panel = UiKit.CreatePanel(_board, "MatchInfo", new Color(0.06f, 0.09f, 0.15f, 0.98f)); + var element = panel.gameObject.AddComponent(); + element.preferredHeight = 76f; + UiKit.AddVerticalLayout(panel, 8, 2); + + var active = view.ActivePlayer == view.You ? "решаете вы" : "решает противник"; + var main = UiKit.CreateText( + panel, + "Main", + "Ход " + view.TurnNumber + " · " + view.Phase + " · " + view.Step + + " · " + active, + 17, + view.ActivePlayer == view.You + ? new Color(0.55f, 0.86f, 1f) + : new Color(1f, 0.78f, 0.52f), + TextAnchor.MiddleLeft); + main.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(main, 26f); + + var details = new List(); + if (view.Battle != null) + details.Add("Бой: " + SlotName(view.Battle.AttackerSlot) + + " → " + SlotName(view.Battle.TargetSlot) + + " · щит " + view.Battle.GuardShield + + " · проверок " + view.Battle.ChecksRemaining); + if (view.Choice != null) + details.Add(view.Choice.Prompt); + if (view.Ability != null) + details.Add("Способность: " + CardName(view.Ability.PrintingId)); + if (view.Trigger != null) + details.Add("Триггер: " + CardName(view.Trigger.PrintingId) + + " · " + view.Trigger.Trigger); + if (view.OpponentChoosing) + details.Add("Противник выбирает действие…"); + if (details.Count == 0) + details.Add("Выберите действие в панели справа."); + + var detail = UiKit.CreateText( + panel, "Detail", string.Join(" · ", details), 14, + Muted, TextAnchor.MiddleLeft); + detail.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.SetPreferredHeight(detail, 28f); + } + private void AddHand(PlayerView self) { - var hand = UiKit.CreatePanel(_board, "HandBlock", Panel); + var hand = UiKit.CreatePanel(_board, "HandBlock", new Color(0.05f, 0.07f, 0.11f, 0.96f)); var handElement = hand.gameObject.AddComponent(); - handElement.preferredHeight = 190f; - UiKit.AddVerticalLayout(hand, 8, 4); + handElement.preferredHeight = 188f; + UiKit.AddVerticalLayout(hand, 6, 3); var header = UiKit.CreateText( - hand, "Header", "Рука", 17, Color.white, TextAnchor.MiddleLeft); + hand, "Header", + "РУКА · " + (self?.Hand?.Count ?? 0) + " карт", + 16, Color.white, TextAnchor.MiddleLeft); + header.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(header, 22f); - var row = UiKit.CreatePanel(hand, "Cards", new Color(0f, 0f, 0f, 0f)); - var rowElement = row.gameObject.AddComponent(); - rowElement.flexibleHeight = 1f; - UiKit.AddHorizontalLayout(row, 0, 6); + var scroll = UiKit.CreateHorizontalScrollView(hand, "HandScroll", out var content, 6f); + var scrollElement = scroll.gameObject.AddComponent(); + scrollElement.flexibleHeight = 1f; + scrollElement.minHeight = 130f; if (self?.Hand == null || self.Hand.Count == 0) { - var empty = UiKit.CreateText(row, "Empty", "Рука пуста", 18, Muted); - UiKit.SetPreferredHeight(empty, 120f); + var empty = UiKit.CreateText( + content, "Empty", "Рука пуста", 17, Muted); + UiKit.SetPreferredHeight(empty, 110f); return; } foreach (var card in self.Hand) - AddCardBox(row, card, "Рука"); + AddCardBox(content, card, "Рука", compact: true); } - private void AddMatchInfo(MatchView view) - { - var info = UiKit.CreatePanel(_board, "MatchInfo", Panel); - var element = info.gameObject.AddComponent(); - element.preferredHeight = 150f; - UiKit.AddVerticalLayout(info, 10, 3); - - var active = view.ActivePlayer == view.You ? "вы" : "противник"; - AddInfoLine(info, "Фаза: " + view.Phase + " · шаг: " + view.Step - + " · ход " + view.TurnNumber + " · решает: " + active); - - if (view.Battle != null) - { - AddInfoLine( - info, - "Атака: " + SlotName(view.Battle.AttackerSlot) - + " → " + SlotName(view.Battle.TargetSlot) - + " · щит " + view.Battle.GuardShield - + " · проверок " + view.Battle.ChecksRemaining - + (view.Battle.Nullified ? " · отменено" : string.Empty)); - } - - if (view.Choice != null) - AddInfoLine(info, "Выбор: " + view.Choice.Prompt); - - if (view.Ability != null) - AddInfoLine( - info, - "Способность: " + view.Ability.PrintingId - + (view.Ability.Optional ? " (опционально)" : string.Empty)); - - if (view.Trigger != null) - { - AddInfoLine( - info, - "Триггер: " + view.Trigger.PrintingId - + " / " + view.Trigger.Trigger - + " / урон " + view.Trigger.RemainingDamage); - } - - if (view.Order != null && view.Order.Options.Count > 0) - AddInfoLine(info, "Нужно выбрать порядок эффектов: " + view.Order.Options.Count); - - if (view.OpponentChoosing) - AddInfoLine(info, "Противник что-то выбирает…"); - } - - private static void AddInfoLine(Transform parent, string text) + private void AddInfoLine(Transform parent, string text) { var line = UiKit.CreateText( parent, "Info", text, 16, Muted, TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(line, 22f); } - private static void AddCardBox(Transform parent, CardView card, string slot) + private void AddCardBox(Transform parent, CardView card, string slot, bool compact) { - var panel = UiKit.CreatePanel(parent, "Card_" + slot, Card); - var element = panel.gameObject.AddComponent(); - element.preferredWidth = 132f; - element.preferredHeight = 148f; - element.flexibleWidth = 0f; - UiKit.AddVerticalLayout(panel, 5, 2); + var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found) + ? found + : null; + var clanColor = CardArt.ClanColor(meta != null ? meta.Clan : "None"); + var frameColor = card == null + ? new Color(0.16f, 0.18f, 0.24f) + : CardArt.ClanColor(meta != null ? meta.Clan : "None"); - var slotText = UiKit.CreateText(panel, "Slot", slot, 12, Muted); - UiKit.SetPreferredHeight(slotText, 16f); + var panel = UiKit.CreatePanel( + parent, "Card_" + slot, card == null + ? new Color(0.06f, 0.08f, 0.12f, 0.9f) + : card.FaceUp + ? new Color(0.09f, 0.11f, 0.16f, 1f) + : new Color(0.05f, 0.08f, 0.14f, 1f)); + var element = panel.gameObject.AddComponent(); + element.preferredWidth = compact ? 106f : 124f; + element.preferredHeight = compact ? 142f : 162f; + element.flexibleWidth = 0f; + + var outline = panel.gameObject.AddComponent(); + outline.effectColor = frameColor; + outline.effectDistance = new Vector2(2f, -2f); + UiKit.AddVerticalLayout(panel, 4, 2); + + var slotText = UiKit.CreateText( + panel, "Slot", slot, 10, + new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft); + UiKit.SetPreferredHeight(slotText, 14f); if (card == null) { - var empty = UiKit.CreateText(panel, "Empty", "Пусто", 14, new Color(1f, 1f, 1f, 0.35f)); - UiKit.SetPreferredHeight(empty, 60f); + var emptyArt = UiKit.CreatePanel(panel, "EmptyArt", new Color(0.04f, 0.06f, 0.09f, 1f)); + UiKit.SetPreferredHeight(emptyArt, compact ? 48f : 64f); + var empty = UiKit.CreateText( + emptyArt, "Empty", "—", 22, + new Color(1f, 1f, 1f, 0.18f)); + UiKit.Stretch(empty.rectTransform); + var emptyLabel = UiKit.CreateText( + panel, "EmptyLabel", "Пусто", 11, + new Color(1f, 1f, 1f, 0.28f)); + UiKit.SetPreferredHeight(emptyLabel, 24f); return; } - var printing = UiKit.CreateText( - panel, "Printing", card.PrintingId, 12, Color.white, TextAnchor.UpperCenter); - printing.horizontalOverflow = HorizontalWrapMode.Wrap; - UiKit.SetPreferredHeight(printing, 42f); + var art = UiKit.CreatePanel(panel, "Art", Color.white); + UiKit.SetPreferredHeight(art, compact ? 48f : 64f); + var artImage = art.GetComponent(); + artImage.sprite = card.FaceUp + ? CardArt.ForCard( + meta != null ? meta.NameId : card.PrintingId, + meta != null ? meta.Clan : "None") + : CardArt.Back; + artImage.color = Color.white; + artImage.preserveAspect = false; + + if (card.FaceUp) + { + var artName = UiKit.CreateText( + art, "ArtName", meta != null ? meta.NameEn : card.PrintingId, 10, + new Color(1f, 1f, 1f, 0.94f), TextAnchor.MiddleCenter); + UiKit.Stretch(artName.rectTransform); + artName.rectTransform.offsetMin = new Vector2(3f, 3f); + artName.rectTransform.offsetMax = new Vector2(-3f, -2f); + artName.horizontalOverflow = HorizontalWrapMode.Wrap; + + if (meta != null) + { + var grade = UiKit.CreateText( + art, "Grade", "G" + meta.Grade, 10, + Color.white, TextAnchor.MiddleCenter); + var gradeRect = grade.rectTransform; + gradeRect.anchorMin = new Vector2(1f, 1f); + gradeRect.anchorMax = new Vector2(1f, 1f); + gradeRect.pivot = new Vector2(1f, 1f); + gradeRect.anchoredPosition = new Vector2(-3f, -3f); + gradeRect.sizeDelta = new Vector2(25f, 16f); + } + } + else + { + var hidden = UiKit.CreateText( + art, "Hidden", "?", 26, new Color(1f, 1f, 1f, 0.8f)); + UiKit.Stretch(hidden.rectTransform); + } + + if (card.Rested) + { + var rested = UiKit.CreateText( + art, "Rested", "REST", 10, + new Color(1f, 0.55f, 0.35f), TextAnchor.MiddleCenter); + var restedRect = rested.rectTransform; + restedRect.anchorMin = new Vector2(0f, 0f); + restedRect.anchorMax = new Vector2(0f, 0f); + restedRect.pivot = new Vector2(0f, 0f); + restedRect.anchoredPosition = new Vector2(3f, 3f); + restedRect.sizeDelta = new Vector2(34f, 16f); + } + + var name = UiKit.CreateText( + panel, "Name", + meta != null ? meta.NameEn : card.PrintingId, + 10, + card.FaceUp ? Color.white : Muted, + TextAnchor.UpperLeft); + name.horizontalOverflow = HorizontalWrapMode.Wrap; + name.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(name, compact ? 26f : 28f); var stats = UiKit.CreateText( - panel, "Stats", card.Power + " / " + card.Critical, 16, Color.white); - UiKit.SetPreferredHeight(stats, 22f); - - var stateText = (card.Rested ? "REST " : string.Empty) - + (card.FaceUp ? string.Empty : "FACE-DOWN"); - if (!string.IsNullOrEmpty(stateText)) - { - var state = UiKit.CreateText(panel, "State", stateText.Trim(), 12, new Color(1f, 0.76f, 0.38f)); - UiKit.SetPreferredHeight(state, 18f); - } + panel, "Stats", + card.FaceUp + ? "P " + card.Power + " · C " + card.Critical + + (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty) + : "СКРЫТА", + 11, + card.FaceUp && card.Power != card.BasePower + ? new Color(1f, 0.82f, 0.42f) + : new Color(0.72f, 0.78f, 0.92f), + TextAnchor.MiddleLeft); + UiKit.SetPreferredHeight(stats, 18f); } + private static string TriggerShort(string trigger) => trigger switch + { + "Critical" => "K", + "Draw" => "D", + "Stand" => "S", + "Heal" => "H", + _ => "—", + }; + + private string CardName(string printingId) => + _catalog.TryGetValue(printingId, out var meta) ? meta.NameEn : printingId; + private void RebuildIntents(MatchView view) { ClearChildren(_intents); @@ -681,7 +813,13 @@ namespace Vanguard.UI private static void ClearChildren(Transform parent) { for (var i = parent.childCount - 1; i >= 0; i--) - UnityEngine.Object.Destroy(parent.GetChild(i).gameObject); + { + var child = parent.GetChild(i).gameObject; + if (Application.isPlaying) + UnityEngine.Object.Destroy(child); + else + UnityEngine.Object.DestroyImmediate(child); + } } private static string SlotName(int slot) @@ -699,14 +837,14 @@ namespace Vanguard.UI case FinishMulliganIntent _: return "Закончить маллиган"; case MulliganIntent mulligan: return "Маллиган: " + mulligan.HandIndexes.Count + " карт"; case SkipRideIntent _: return "Пропустить Ride"; - case RideIntent ride: return "Ride: " + ride.PrintingId; - case CallIntent call: return "Call " + call.PrintingId + " → " + SlotName(call.Slot); + case RideIntent ride: return "Ride: " + CardName(ride.PrintingId); + case CallIntent call: return "Call " + CardName(call.PrintingId) + " → " + SlotName(call.Slot); case MoveIntent move: return "Move " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot); case EnterBattleIntent _: return "В фазу боя"; case AttackIntent attack: return "Атака " + SlotName(attack.AttackerSlot) + " → " + SlotName(attack.TargetSlot) + (attack.BoosterSlot >= 0 ? " + boost" : string.Empty); - case GuardIntent guard: return "Guard: " + guard.PrintingId; + case GuardIntent guard: return "Guard: " + CardName(guard.PrintingId); case InterceptIntent intercept: return "Intercept: " + SlotName(intercept.Slot); case PassGuardIntent _: return "Пропустить Guard"; case ResolveTriggerIntent trigger: diff --git a/client/Assets/Vanguard/Runtime/UI/CardArt.cs b/client/Assets/Vanguard/Runtime/UI/CardArt.cs new file mode 100644 index 0000000..3f93b88 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/CardArt.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Vanguard.UI +{ + /// + /// Процедурные карточные текстуры без внешних артов: цвет клана, + /// градиент, рубашка. Позже заменяется Addressables-артом. + /// + public static class CardArt + { + private static readonly Dictionary Cache = + new Dictionary(); + private static Sprite _back; + + public static Color ClanColor(string clan) => clan switch + { + "RoyalPaladin" => new Color(0.20f, 0.40f, 0.78f), + "OracleThinkTank" => new Color(0.72f, 0.58f, 0.22f), + "Kagero" => new Color(0.68f, 0.18f, 0.18f), + "Nubatama" => new Color(0.38f, 0.22f, 0.62f), + "Tachikaze" => new Color(0.22f, 0.56f, 0.34f), + "NovaGrappler" => new Color(0.78f, 0.40f, 0.16f), + "SpikeBrothers" => new Color(0.52f, 0.34f, 0.20f), + "DarkIrregulars" => new Color(0.30f, 0.18f, 0.38f), + "Granblue" => new Color(0.16f, 0.46f, 0.54f), + "Megacolony" => new Color(0.40f, 0.48f, 0.18f), + _ => new Color(0.28f, 0.34f, 0.46f), + }; + + public static Sprite ForClan(string clan) + { + var key = "clan:" + clan; + if (Cache.TryGetValue(key, out var sprite)) + return sprite; + + var color = ClanColor(clan); + sprite = Gradient( + Color.Lerp(color, Color.black, 0.55f), + Color.Lerp(color, Color.white, 0.18f), + accent: Color.Lerp(color, Color.white, 0.35f), + variant: 0); + Cache[key] = sprite; + return sprite; + } + + public static Sprite ForCard(string nameId, string clan) + { + var key = "card:" + nameId; + if (Cache.TryGetValue(key, out var sprite)) + return sprite; + + var color = ClanColor(clan); + var hash = StableHash(nameId ?? string.Empty); + var accent = Color.HSVToRGB( + (hash % 360) / 360f, + 0.62f, + 0.98f); + sprite = Gradient( + Color.Lerp(color, Color.black, 0.58f), + Color.Lerp(color, Color.white, 0.16f), + accent, + (int)(hash % 5)); + Cache[key] = sprite; + return sprite; + } + + private static uint StableHash(string value) + { + var hash = 2166136261u; + foreach (var ch in value) + { + hash ^= ch; + hash *= 16777619u; + } + + return hash; + } + + public static Sprite Back + { + get + { + if (_back != null) + return _back; + + _back = BackTexture(); + return _back; + } + } + + private static Sprite Gradient(Color bottom, Color top, Color accent, int variant) + { + const int width = 32; + const int height = 48; + var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp, + }; + + var pixels = new Color[width * height]; + for (var y = 0; y < height; y++) + { + var t = y / (float)(height - 1); + var row = Color.Lerp(bottom, top, t); + for (var x = 0; x < width; x++) + { + var stripe = ((x + y + variant * 3) % 11) < 2 ? 0.12f : 0f; + var glow = Mathf.Clamp01(1f - Mathf.Abs(x - width * 0.5f) / (width * 0.5f)) * 0.12f; + + var cx = x - width * 0.5f; + var cy = y - height * 0.38f; + var radius = 6f + variant; + var sigil = Mathf.Abs(cx) + Mathf.Abs(cy) < radius ? 0.22f : 0f; + if (variant == 2 && Mathf.Abs(cx) < 2f && Mathf.Abs(cy) < 9f) + sigil += 0.18f; + + pixels[y * width + x] = Color.Lerp(row, accent, Mathf.Clamp01(stripe + glow + sigil)); + } + } + + texture.SetPixels(pixels); + texture.Apply(); + return Sprite.Create( + texture, + new Rect(0f, 0f, width, height), + new Vector2(0.5f, 0.5f), + 100f); + } + + private static Sprite BackTexture() + { + const int width = 32; + const int height = 48; + var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Point, + wrapMode = TextureWrapMode.Clamp, + }; + + var pixels = new Color[width * height]; + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var border = x == 0 || y == 0 || x == width - 1 || y == height - 1; + var diamond = (x + y) % 8 == 0 || (x - y + 64) % 8 == 0; + pixels[y * width + x] = border + ? new Color(0.42f, 0.56f, 0.82f) + : diamond + ? new Color(0.12f, 0.22f, 0.42f) + : new Color(0.05f, 0.09f, 0.17f); + } + } + + texture.SetPixels(pixels); + texture.Apply(); + return Sprite.Create( + texture, + new Rect(0f, 0f, width, height), + new Vector2(0.5f, 0.5f), + 100f); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/CardArt.cs.meta b/client/Assets/Vanguard/Runtime/UI/CardArt.cs.meta new file mode 100644 index 0000000..608d9b5 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/CardArt.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f6f3e2a1d9b4c7e8a5f0b3c2d1e6a94 diff --git a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs index f77812c..8988ac8 100644 --- a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs +++ b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs @@ -69,6 +69,7 @@ namespace Vanguard.UI new Dictionary(StringComparer.Ordinal); private DeckDto _editing; + private int _dust; private string _filterClan = "Все"; private string _filterTrigger = "Все"; private int _filterGrade = -1; @@ -503,7 +504,9 @@ namespace Vanguard.UI return; var meta = _selectedMeta; - _cardArt.color = ClanColor(meta.Clan); + _cardArt.sprite = CardArt.ForCard(meta.NameId, meta.Clan); + _cardArt.color = Color.white; + _cardArt.preserveAspect = false; _cardArtName.text = meta.NameEn; _cardTitle.text = meta.NameEn; _cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ") @@ -519,18 +522,22 @@ namespace Vanguard.UI ? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn) : meta.TextRu; _cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0) - + " · в колоде: " + UsedByName(meta.NameId); + + " · в колоде: " + UsedByName(meta.NameId) + + " · пыль: " + _dust; var canChange = CanChange(meta.Rarity); + var craftCost = CraftCostFor(meta.Rarity); _dismantleButton.interactable = canChange && !_busy && _selectedItem != null && _selectedItem.Count > 0; - _craftButton.interactable = canChange && !_busy; + _craftButton.interactable = canChange && !_busy && _dust >= craftCost; SetButtonLabel( _dismantleButton, canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать"); SetButtonLabel( _craftButton, - canChange ? "Скрафтить (−" + CraftCostFor(meta.Rarity) + " пыли)" : "Нельзя скрафтить"); + canChange + ? "Скрафтить (−" + craftCost + " пыли)" + : "Нельзя скрафтить"); } private static bool CanChange(string rarity) => @@ -608,6 +615,7 @@ namespace Vanguard.UI var collection = await _meta.GetCollectionAsync(); _collection.Clear(); _collection.AddRange(collection.Items ?? new List()); + _dust = collection.Balances != null ? collection.Balances.Dust : 0; RebuildCollectionGrids(); _selectedItem = _collection.Find(item => item.PrintingId == printingId); if (_selectedItem == null) @@ -685,6 +693,7 @@ namespace Vanguard.UI _collection.Clear(); _collection.AddRange(collection.Items ?? new List()); + _dust = collection.Balances != null ? collection.Balances.Dust : 0; _catalog.Clear(); foreach (var item in catalog) _catalog[item.PrintingId] = item; @@ -695,8 +704,9 @@ namespace Vanguard.UI BuildDeckList(); RebuildCollectionGrids(); _listStatus.text = _decks.Count == 0 - ? "Колод пока нет. Создайте первую. ПКМ по карте — подробности." - : "Карт в коллекции: " + _collection.Count + " · ПКМ по карте — подробности."; + ? "Колод пока нет. Пыль: " + _dust + ". ПКМ по карте — подробности." + : "Карт в коллекции: " + _collection.Count + " · пыль " + _dust + + " · ПКМ по карте — подробности."; } catch (ApiException exception) { @@ -855,6 +865,10 @@ namespace Vanguard.UI UiKit.AddVerticalLayout(cell, 6, 2); AddRightClick(cell.gameObject, item.PrintingId, meta); + var frame = cell.gameObject.AddComponent(); + frame.effectColor = CardArt.ClanColor(meta.Clan); + frame.effectDistance = new Vector2(2f, -2f); + if (onAdd != null) { var button = cell.gameObject.AddComponent