diff --git a/client/Assets/Vanguard/Editor/SceneSmoke.cs b/client/Assets/Vanguard/Editor/SceneSmoke.cs index 76d226b..531a43f 100644 --- a/client/Assets/Vanguard/Editor/SceneSmoke.cs +++ b/client/Assets/Vanguard/Editor/SceneSmoke.cs @@ -62,7 +62,8 @@ namespace Vanguard.Editor foreach (var card in cards) { var viewport = camera.WorldToViewportPoint(card.transform.position); - if (card.Ref.Zone == Vanguard.Board3D.BoardZone.Hand) + if (card.Ref.Zone == Vanguard.Board3D.BoardZone.Hand + && card.Ref.Player == 0) { handCards++; if (viewport.z > 0f diff --git a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset index c49ff7c..d98f33d 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset @@ -43,6 +43,13 @@ MonoBehaviour: handViewScale: 0.5 handViewRoll: 3.2 handViewArc: 0.02 + opponentHandEnabled: 1 + opponentHandZ: 4.3 + opponentHandHeight: 0.75 + opponentHandSpacing: 0.42 + opponentHandMaxWidth: 2.4 + opponentHandScale: 0.8 + opponentHandRoll: 2.5 guardianRowGap: 0.55 guardianSpacing: 0.44 guardianMaxWidth: 3.6 diff --git a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs index 7a0a5ee..4867e3e 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs @@ -61,6 +61,15 @@ namespace Vanguard.Board3D public float handViewRoll = 3.2f; public float handViewArc = 0.02f; + [Header("Рука противника")] + public bool opponentHandEnabled = true; + public float opponentHandZ = 4.3f; + public float opponentHandHeight = 0.75f; + public float opponentHandSpacing = 0.42f; + public float opponentHandMaxWidth = 2.4f; + public float opponentHandScale = 0.8f; + public float opponentHandRoll = 2.5f; + [Header("Круг защиты")] public float guardianRowGap = 0.55f; public float guardianSpacing = 0.44f; @@ -144,6 +153,12 @@ namespace Vanguard.Board3D handViewScale = Mathf.Clamp(handViewScale, 0.3f, 1.5f); handViewRoll = Mathf.Clamp(handViewRoll, 0f, 20f); handViewArc = Mathf.Max(0f, handViewArc); + opponentHandZ = Mathf.Max(1f, opponentHandZ); + opponentHandHeight = Mathf.Max(0.2f, opponentHandHeight); + opponentHandSpacing = Mathf.Max(0.1f, opponentHandSpacing); + opponentHandMaxWidth = Mathf.Max(0.5f, opponentHandMaxWidth); + opponentHandScale = Mathf.Clamp(opponentHandScale, 0.3f, 1.5f); + opponentHandRoll = Mathf.Clamp(opponentHandRoll, 0f, 20f); } } } diff --git a/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs b/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs index a420372..a4a2a43 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs @@ -133,6 +133,27 @@ namespace Vanguard.Board3D return new HandPose(new Vector3(x, y, z), yaw); } + /// Веер рубашек противника у дальнего края стола. + public HandPose GetOpponentHandPose(int index, int count) + { + var safeCount = Mathf.Max(1, count); + var spacing = _config.opponentHandSpacing; + if (safeCount > 1) + { + spacing = Mathf.Min( + spacing, + _config.opponentHandMaxWidth / (safeCount - 1)); + } + + var offset = index - (safeCount - 1) * 0.5f; + var x = offset * spacing; + var z = _config.opponentHandZ + Mathf.Abs(offset) * 0.015f; + var y = _config.opponentHandHeight + - (1f - Mathf.Abs(offset) / safeCount) * 0.02f; + var roll = -offset * _config.opponentHandRoll; + return new HandPose(new Vector3(x, y, z), roll); + } + /// /// Ближайшая цель для броска карты. Возвращает false, если карта /// отпущена мимо кругов и общего круга защиты. diff --git a/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs b/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs index 54eb2d1..a32492f 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs @@ -174,6 +174,7 @@ namespace Vanguard.Board3D Vanguard = Card("BT01/020EN", 101), RearGuards = new CardView[5], Hand = new List(), + HandCount = 3, Guardian = new List(), Soul = new List { Card("BT01/021EN", 131) }, FaceDownDamage = 1, diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs index efe16b7..753cc46 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs @@ -468,6 +468,7 @@ namespace Vanguard.Board3D } AddHandSpecs(view, state, specs); + AddOpponentHandSpecs(view, specs); } private void AddFieldSpecs( @@ -758,6 +759,33 @@ namespace Vanguard.Board3D } } + private void AddOpponentHandSpecs(MatchView view, List specs) + { + if (!_config.opponentHandEnabled) + return; + + var opponent = view.Players.FirstOrDefault(player => player.Index != view.You); + if (opponent == null || opponent.HandCount <= 0) + return; + + var count = opponent.HandCount; + for (var i = 0; i < count; i++) + { + var pose = _layout.GetOpponentHandPose(i, count); + specs.Add(new CardSpec + { + Key = "OH:" + opponent.Index + ":" + i, + Ref = BoardCardRef.Hand(opponent.Index, i, null), + Art = BoardArt.Fallback(_config.matOpponentColor), + Position = pose.Position, + BaseRotation = Quaternion.Euler(-90f, 0f, pose.Yaw), + FaceUp = false, + Rested = false, + Scale = _config.opponentHandScale, + }); + } + } + private void UpdatePiles(MatchView view) { var self = view.Players.FirstOrDefault(player => player.Index == view.You); @@ -959,6 +987,9 @@ namespace Vanguard.Board3D if (_pressedCard == null || _pressedCardRef.IsEmpty) return; + if (_pressedCardRef.Player != _selfPlayer) + return; + if (!_pressedCardRef.IsField && !_pressedCardRef.IsHand) return; diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index e72d971..a20b46d 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -11,6 +11,7 @@ using Vanguard.Battle; using Vanguard.Board3D; using Vanguard.Meta; using Vanguard.Shared.Protocol; +using Vanguard.UI.Overlays; namespace Vanguard.UI { @@ -75,11 +76,9 @@ namespace Vanguard.UI private readonly HashSet _highlightSlots = new HashSet(); private GameBoard3D _board3D; - private RectTransform _soulOverlay; - private RectTransform _soulContent; - private Text _soulTitle; - private Text _selfStatsText; - private Text _opponentStatsText; + private OverlayNavigationService _overlays; + private ZoneBlockStripView _selfZones; + private ZoneBlockStripView _opponentZones; private RectTransform _phasePanel; private bool _autoFinishMulligan; private float _autoFinishMulliganAt; @@ -186,8 +185,7 @@ namespace Vanguard.UI feedbackRect.anchoredPosition = new Vector2(0f, 130f); _feedback.gameObject.SetActive(false); - BuildSoulOverlay(); - BuildStatsOverlays(); + BuildOverlaysAndZones(); BuildPhasePanel(); _board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default); @@ -235,13 +233,10 @@ namespace Vanguard.UI _skipFeedback.gameObject.SetActive(false); _board3D?.Clear(); _board3D?.SetVisible(true); - if (_soulOverlay != null) - _soulOverlay.gameObject.SetActive(false); + _overlays?.CloseAll(); + _selfZones?.Update(null, self: true); + _opponentZones?.Update(null, self: false); _topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId; - if (_selfStatsText != null) - _selfStatsText.text = string.Empty; - if (_opponentStatsText != null) - _opponentStatsText.text = string.Empty; SetStatus("Подключаемся к матчу…", Muted); _root.gameObject.SetActive(true); _ = LoadCatalogAsync(); @@ -266,8 +261,7 @@ namespace Vanguard.UI _feedback.gameObject.SetActive(false); _flash.gameObject.SetActive(false); _skipFeedback.gameObject.SetActive(false); - if (_soulOverlay != null) - _soulOverlay.gameObject.SetActive(false); + _overlays?.CloseAll(); _board3D?.SetVisible(false); _view = null; } @@ -858,7 +852,7 @@ namespace Vanguard.UI _board3D?.SetSelfPlayer(_view.You); _board3D?.Render(_view, BuildBoardRenderState()); - UpdateStatsOverlay(); + UpdateZoneStrips(); RebuildIntents(_view); RebuildPhaseControls(_view); @@ -977,6 +971,12 @@ namespace Vanguard.UI switch (card.Zone) { case BoardZone.Hand: + if (card.Player != _view.You) + { + OpenZone(BoardZone.Hand, card.Player); + break; + } + OnBoardTargetClicked(new BoardTarget( card.Player, -2, card.PrintingId, card.InstanceId, true)); break; @@ -1083,7 +1083,7 @@ namespace Vanguard.UI return; if (pile.Zone == BoardZone.Soul) - ShowSoul(pile.Player); + OpenZone(BoardZone.Soul, pile.Player); } private void OnBoard3DCardRightClicked(BoardCardRef card) @@ -1092,7 +1092,7 @@ namespace Vanguard.UI return; if (card.Zone == BoardZone.Vanguard) - ShowSoul(card.Player); + OpenZone(BoardZone.Soul, card.Player); } private void OnBoard3DPileClicked(BoardCardRef pile) @@ -1102,22 +1102,18 @@ namespace Vanguard.UI if (pile.Zone == BoardZone.Soul) { - SetStatus("Душа открывается правой кнопкой мыши.", Muted); + SetStatus("Душа открывается правой кнопкой мыши по VC.", Muted); return; } if (pile.Zone == BoardZone.Drop) { - ShowDrop(pile.Player); + OpenZone(BoardZone.Drop, pile.Player); return; } - var player = _view.Players.FirstOrDefault(item => item.Index == pile.Player); - if (player == null) - return; - if (pile.Zone == BoardZone.Deck) - SetStatus("В колоде " + player.DeckCount + " карт.", Muted); + OpenZone(BoardZone.Deck, pile.Player); } private void BuildPhasePanel() @@ -1192,215 +1188,57 @@ namespace Vanguard.UI _ => intent.GetType().Name, }; - private void BuildStatsOverlays() - { _selfStatsText = CreateStatsPanel( - "SelfStats", + private void BuildOverlaysAndZones() + { + _overlays = new OverlayNavigationService(_root, ResolveCatalogItem); + + _selfZones = new ZoneBlockStripView(); + _selfZones.Build( + _root, + "ZoneBlocksSelf", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 24f), - TextAnchor.MiddleLeft); - _opponentStatsText = CreateStatsPanel( - "OpponentStats", + new Vector2(360f, 108f)); + _selfZones.ZoneClicked += zone => OpenZone(zone, _view?.You ?? 0); + + _opponentZones = new ZoneBlockStripView(); + _opponentZones.Build( + _root, + "ZoneBlocksOpponent", new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-488f, -108f), - TextAnchor.MiddleRight); + new Vector2(360f, 108f)); + _opponentZones.ZoneClicked += zone => + OpenZone(zone, _view != null ? 1 - _view.You : 1); } - private Text CreateStatsPanel( - string name, - Vector2 anchor, - Vector2 pivot, - Vector2 position, - TextAnchor alignment) + private void UpdateZoneStrips() { - var panel = UiKit.CreatePanel( - _root, - name, - new Color(0.02f, 0.035f, 0.055f, 0.72f)); - panel.anchorMin = anchor; - panel.anchorMax = anchor; - panel.pivot = pivot; - panel.anchoredPosition = position; - panel.sizeDelta = new Vector2(360f, 58f); - - var image = panel.GetComponent(); - if (image != null) - image.raycastTarget = false; - - var text = UiKit.CreateText( - panel, - "Stats", - string.Empty, - 14, - new Color(0.86f, 0.91f, 1f), - alignment); - UiKit.Stretch(text.rectTransform); - text.rectTransform.offsetMin = new Vector2(12f, 5f); - text.rectTransform.offsetMax = new Vector2(-12f, -5f); - text.horizontalOverflow = HorizontalWrapMode.Wrap; - text.verticalOverflow = VerticalWrapMode.Overflow; - UiKit.MakeLiteral(text); - return text; - } - - private void UpdateStatsOverlay() - { - if (_view == null || _selfStatsText == null || _opponentStatsText == null) + if (_view == null) return; var self = _view.Players.FirstOrDefault(player => player.Index == _view.You); var opponent = _view.Players.FirstOrDefault(player => player.Index != _view.You); - _selfStatsText.text = FormatStats(self, selfSide: true); - _opponentStatsText.text = FormatStats(opponent, selfSide: false); + _selfZones?.Update(self, self: true); + _opponentZones?.Update(opponent, self: false); } - private static string FormatStats(PlayerView player, bool selfSide) + private void OpenZone(BoardZone zone, int playerIndex) { - var title = Loc.Translate(selfSide ? "ВЫ" : "ПРОТИВНИК"); - if (player == null) - return title; + if (_view == null) + return; - var damage = player.FaceDownDamage + (player.FaceUpDamage?.Count ?? 0); - var hand = Mathf.Max(player.HandCount, player.Hand?.Count ?? 0); - return title + " · " + player.Name + "\n" - + Loc.Translate("Рука") + " " + hand - + " " + Loc.Translate("Колода") + " " + player.DeckCount - + " " + Loc.Translate("ДРОП") + " " + (player.Drop?.Count ?? 0) - + " " + Loc.Translate("УРОН") + " " + damage - + " " + Loc.Translate("Душа") + " " + (player.Soul?.Count ?? 0); - } - - private void BuildSoulOverlay() - { - _soulOverlay = UiKit.CreatePanel(_root, "SoulOverlay", new Color(0f, 0f, 0f, 0.74f)); - var panel = UiKit.CreateCenteredCard( - _soulOverlay, - "SoulPanel", - new Vector2(880f, 640f), - Panel); - UiKit.AddVerticalLayout(panel, 18, 10); - - _soulTitle = UiKit.CreateText( - panel, - "SoulTitle", - "Душа", - 24, - Color.white, - TextAnchor.MiddleLeft); - _soulTitle.fontStyle = FontStyle.Bold; - UiKit.SetPreferredHeight(_soulTitle, 34f); - - var scroll = UiKit.CreateScrollView(panel, "SoulScroll", out _soulContent, 6f); - var scrollElement = scroll.gameObject.AddComponent(); - scrollElement.flexibleHeight = 1f; - scrollElement.minHeight = 420f; - - var close = UiKit.CreateButton( - panel, - "SoulClose", - "Закрыть", - () => _soulOverlay.gameObject.SetActive(false), - 18); - UiKit.SetPreferredHeight(close, 46f); - - _soulOverlay.gameObject.SetActive(false); - } - - private void ShowSoul(int playerIndex) - { - var player = _view?.Players.FirstOrDefault(item => item.Index == playerIndex); + var player = _view.Players.FirstOrDefault(item => item.Index == playerIndex); if (player == null) return; - ShowCardList("Душа", player.Name, player.Soul); - } - - private void ShowDrop(int playerIndex) - { - var player = _view?.Players.FirstOrDefault(item => item.Index == playerIndex); - if (player == null) - return; - - ShowCardList("Дроп", player.Name, player.Drop); - } - - private void ShowCardList(string title, string playerName, List cards) - { - if (_soulOverlay == null) - return; - - _soulTitle.text = title + " · " + playerName + " · " + (cards?.Count ?? 0); - ClearChildren(_soulContent); - - if (cards == null || cards.Count == 0) - { - var empty = UiKit.CreateText( - _soulContent, - "CardListEmpty", - "Нет карт.", - 17, - Muted); - UiKit.SetPreferredHeight(empty, 80f); - } - else - { - for (var i = cards.Count - 1; i >= 0; i--) - AddSoulRow(cards[i]); - } - - _soulOverlay.SetAsLastSibling(); - _soulOverlay.gameObject.SetActive(true); - } - - private void AddSoulRow(CardView card) - { - var row = UiKit.CreatePanel( - _soulContent, - "Soul_" + card.InstanceId, - new Color(0.08f, 0.10f, 0.15f, 1f)); - var rowElement = row.gameObject.AddComponent(); - rowElement.preferredHeight = 106f; - UiKit.AddHorizontalLayout(row, 6, 10); - - var art = UiKit.CreatePanel(row, "Art", Color.white); - var artElement = art.gameObject.AddComponent(); - artElement.preferredWidth = 64f; - artElement.preferredHeight = 92f; - artElement.flexibleWidth = 0f; - - _catalog.TryGetValue(card.PrintingId, out var meta); - CardImageService.Apply( - art.GetComponent(), - meta != null ? meta.ImageKey : null, - CardArt.ForCard( - meta != null ? meta.NameId : card.PrintingId, - meta != null ? meta.Clan : "None")); - - var name = UiKit.CreateText( - row, - "Name", - meta != null ? Loc.CardName(meta) : card.PrintingId, - 17, - Color.white, - TextAnchor.MiddleLeft); - UiKit.MakeLiteral(name); - UiKit.SetPreferredHeight(name, 92f); - - if (meta != null) - { - var info = UiKit.CreateText( - row, - "Info", - "G" + meta.Grade + " · P " + card.Power + " · C " + card.Critical, - 14, - Muted, - TextAnchor.MiddleRight); - UiKit.MakeLiteral(info); - UiKit.SetPreferredHeight(info, 92f, 0f); - } + _overlays?.OpenZone(player, _view.You, zone); } + private CardCatalogItemDto ResolveCatalogItem(string printingId) => + _catalog.TryGetValue(printingId, out var item) ? item : null; private void OnDamageClicked(int displayIndex) { if (IsDamageChoice()) diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays.meta b/client/Assets/Vanguard/Runtime/UI/Overlays.meta new file mode 100644 index 0000000..162be1b --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f1073196b40e8f498b8c27ce6722408 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs b/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs new file mode 100644 index 0000000..a7a0ffd --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs @@ -0,0 +1,137 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using Vanguard.Api.Contracts; + +namespace Vanguard.UI.Overlays +{ + /// Крупная карточка с полным текстом. Для скрытых карт показывает рубашку. + public sealed class CardDetailOverlay + { + private readonly RectTransform _root; + private readonly Image _art; + private readonly Text _name; + private readonly Text _meta; + private readonly Text _stats; + private readonly Text _ability; + private readonly Func _catalog; + private Action _onBack; + + public CardDetailOverlay( + Transform parent, + Func catalog) + { + _catalog = catalog; + + _root = UiKit.CreatePanel(parent, "CardDetailOverlay", new Color(0f, 0f, 0f, 0.82f)); + var panel = UiKit.CreateCenteredCard( + _root, + "CardDetailPanel", + new Vector2(900f, 600f), + new Color(0.045f, 0.06f, 0.09f, 1f)); + UiKit.AddVerticalLayout(panel, 16, 10); + + var header = UiKit.CreatePanel(panel, "Header", Color.clear); + UiKit.AddHorizontalLayout(header, 0, 10); + UiKit.SetPreferredHeight(header, 44f); + + _name = UiKit.CreateText(header, "Name", "Карта", 24, Color.white, TextAnchor.MiddleLeft); + _name.fontStyle = FontStyle.Bold; + UiKit.MakeLiteral(_name); + UiKit.SetPreferredHeight(_name, 44f); + + var back = UiKit.CreateButton(header, "Back", "Назад", () => _onBack?.Invoke(), 16); + var backElement = UiKit.SetPreferredHeight(back, 40f, 0f); + backElement.preferredWidth = 110f; + + var close = UiKit.CreateButton(header, "Close", "Закрыть", () => _onBack?.Invoke(), 16); + var closeElement = UiKit.SetPreferredHeight(close, 40f, 0f); + closeElement.preferredWidth = 110f; + + var body = UiKit.CreatePanel(panel, "Body", Color.clear); + var bodyElement = body.gameObject.AddComponent(); + bodyElement.flexibleHeight = 1f; + bodyElement.minHeight = 500f; + UiKit.AddHorizontalLayout(body, 0, 16); + + var artPanel = UiKit.CreatePanel(body, "Art", Color.white); + var artElement = artPanel.gameObject.AddComponent(); + artElement.preferredWidth = 300f; + artElement.preferredHeight = 460f; + artElement.flexibleWidth = 0f; + _art = artPanel.GetComponent(); + _art.raycastTarget = false; + + var info = UiKit.CreatePanel(body, "Info", Color.clear); + var infoElement = info.gameObject.AddComponent(); + infoElement.flexibleWidth = 1f; + infoElement.flexibleHeight = 1f; + UiKit.AddVerticalLayout(info, 0, 6); + + _meta = UiKit.CreateText(info, "Meta", string.Empty, 15, new Color(0.72f, 0.80f, 0.94f), TextAnchor.UpperLeft); + _meta.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.MakeLiteral(_meta); + UiKit.SetPreferredHeight(_meta, 52f); + + _stats = UiKit.CreateText(info, "Stats", string.Empty, 17, Color.white, TextAnchor.UpperLeft); + _stats.fontStyle = FontStyle.Bold; + UiKit.MakeLiteral(_stats); + UiKit.SetPreferredHeight(_stats, 40f); + + _ability = UiKit.CreateText(info, "Ability", string.Empty, 15, Color.white, TextAnchor.UpperLeft); + _ability.horizontalOverflow = HorizontalWrapMode.Wrap; + _ability.verticalOverflow = VerticalWrapMode.Overflow; + UiKit.MakeLiteral(_ability); + UiKit.SetPreferredHeight(_ability, 300f); + + _root.gameObject.SetActive(false); + } + + public bool IsVisible => _root.gameObject.activeSelf; + + public void Show(CardListEntry entry, Action onBack) + { + _onBack = onBack; + var card = entry.Card; + var meta = card != null ? _catalog?.Invoke(card.PrintingId) : null; + + if (entry.Hidden || card == null) + { + _art.sprite = CardArt.Back; + _art.color = Color.white; + _name.text = entry.Label ?? "Закрытая карта"; + _meta.text = "Содержимое этой карты скрыто."; + _stats.text = string.Empty; + _ability.text = string.Empty; + } + else + { + CardImageService.Apply( + _art, + meta != null ? meta.ImageKey : null, + CardArt.ForCard( + meta != null ? meta.NameId : card.PrintingId, + meta != null ? meta.Clan : "None")); + _name.text = meta != null ? Loc.CardName(meta) : card.PrintingId; + _meta.text = meta != null + ? meta.Clan + " · " + meta.Type + " · G" + meta.Grade + + (string.IsNullOrEmpty(meta.Trigger) || meta.Trigger == "None" + ? string.Empty + : " · " + meta.Trigger) + : string.Empty; + _stats.text = "P " + card.Power + + " · Shield " + (meta != null ? meta.Shield : 0) + + " · C " + card.Critical; + _ability.text = meta != null ? Loc.CardText(meta) : string.Empty; + } + + _root.SetAsLastSibling(); + _root.gameObject.SetActive(true); + } + + public void Hide() + { + _root.gameObject.SetActive(false); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs.meta b/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs.meta new file mode 100644 index 0000000..86e8780 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays/CardDetailOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0fafe71d7dd0d2e479eea533f73ea531 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs new file mode 100644 index 0000000..512ad2b --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using Vanguard.Board3D; +using Vanguard.Shared.Protocol; + +namespace Vanguard.UI.Overlays +{ + /// Один тайл в общем списке карт. Закрытые карты не раскрывают printingId. + public sealed class CardListEntry + { + public CardListEntry(CardView card, bool hidden = false, string label = null) + { + Card = card; + Hidden = hidden; + Label = label; + } + + public CardView Card { get; } + public bool Hidden { get; } + public string Label { get; } + } + + /// Запрос на общий горизонтальный список карт. + public sealed class CardListRequest + { + public CardListRequest(string title, int player, BoardZone zone) + { + Title = title; + Player = player; + Zone = zone; + } + + public string Title { get; } + public int Player { get; } + public BoardZone Zone { get; } + public List Entries { get; } = new List(); + public string EmptyText { get; set; } = "В этой зоне нет карт."; + } + + /// Собирает содержимое зоны для CardListView без раскрытия скрытых данных. + public static class CardZoneContent + { + public static CardListRequest Build( + PlayerView player, + int viewer, + BoardZone zone) + { + var request = new CardListRequest(ZoneTitle(zone, player), player?.Index ?? 0, zone); + if (player == null) + return request; + + switch (zone) + { + case BoardZone.Hand: + if (player.Index == viewer) + AddCards(request, player.Hand); + else + AddHidden(request, player.HandCount, "Закрытая карта"); + break; + + case BoardZone.Deck: + // Порядок колоды не покидает сервер: показываем только закрытые слоты. + AddHidden(request, player.DeckCount, "Колода"); + break; + + case BoardZone.Drop: + AddCards(request, player.Drop); + break; + + case BoardZone.Soul: + AddCards(request, player.Soul); + break; + + case BoardZone.Guardian: + AddCards(request, player.Guardian); + break; + + case BoardZone.Trigger: + AddCards(request, player.Trigger); + break; + + case BoardZone.Damage: + AddDamage(request, player); + break; + } + + return request; + } + + private static void AddDamage(CardListRequest request, PlayerView player) + { + var faceUp = player.FaceUpDamage ?? new List(); + for (var i = faceUp.Count - 1; i >= 0; i--) + request.Entries.Add(new CardListEntry(faceUp[i])); + + for (var i = 0; i < player.FaceDownDamage; i++) + request.Entries.Add(new CardListEntry(null, true, "Закрытый урон")); + } + + private static void AddCards(CardListRequest request, List cards) + { + if (cards == null) + return; + + for (var i = cards.Count - 1; i >= 0; i--) + request.Entries.Add(new CardListEntry(cards[i])); + } + + private static void AddHidden(CardListRequest request, int count, string label) + { + for (var i = 0; i < count; i++) + request.Entries.Add(new CardListEntry(null, true, label)); + } + + private static string ZoneTitle(BoardZone zone, PlayerView player) + { + var name = player != null ? player.Name : string.Empty; + var title = zone switch + { + BoardZone.Hand => "Рука", + BoardZone.Deck => "Колода", + BoardZone.Drop => "Дроп", + BoardZone.Soul => "Душа", + BoardZone.Damage => "Урон", + BoardZone.Guardian => "Круг защиты", + BoardZone.Trigger => "Триггер", + BoardZone.Bind => "Bind", + BoardZone.GenerationBreak => "G Zone", + _ => "Зона", + }; + + return string.IsNullOrEmpty(name) ? title : title + " · " + name; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs.meta b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs.meta new file mode 100644 index 0000000..887620d --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListModels.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5790362145aee4c41994f11e199cb921 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/Overlays/CardListView.cs b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListView.cs new file mode 100644 index 0000000..3fe9808 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/Overlays/CardListView.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using Vanguard.Api.Contracts; +using Vanguard.Shared.Protocol; + +namespace Vanguard.UI.Overlays +{ + /// + /// Единый горизонтальный список карт для Deck/Drop/Soul/Damage/Trigger/Hand. + /// Порядок задаёт адаптер: первой идёт карта, лежащая сверху зоны. + /// + public sealed class CardListView + { + private readonly RectTransform _root; + private readonly RectTransform _content; + private readonly Text _title; + private readonly Func _catalog; + private Action _onEntry; + private Action _onClose; + + public CardListView( + Transform parent, + Func catalog) + { + _catalog = catalog; + + _root = UiKit.CreatePanel(parent, "CardListView", new Color(0f, 0f, 0f, 0.78f)); + var panel = UiKit.CreateCenteredCard( + _root, + "CardListPanel", + new Vector2(980f, 560f), + new Color(0.045f, 0.06f, 0.09f, 1f)); + UiKit.AddVerticalLayout(panel, 16, 10); + + var header = UiKit.CreatePanel(panel, "Header", Color.clear); + UiKit.AddHorizontalLayout(header, 0, 10); + UiKit.SetPreferredHeight(header, 44f); + + _title = UiKit.CreateText( + header, + "Title", + "Карты", + 24, + Color.white, + TextAnchor.MiddleLeft); + _title.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(_title, 44f); + + var close = UiKit.CreateButton(header, "Close", "Закрыть", () => _onClose?.Invoke(), 16); + var closeElement = UiKit.SetPreferredHeight(close, 40f, 0f); + closeElement.preferredWidth = 120f; + + var scroll = UiKit.CreateHorizontalScrollView(panel, "CardScroll", out _content, 10f); + var scrollElement = scroll.gameObject.AddComponent(); + scrollElement.flexibleHeight = 1f; + scrollElement.minHeight = 400f; + + _root.gameObject.SetActive(false); + } + + public bool IsVisible => _root.gameObject.activeSelf; + + public void Show( + CardListRequest request, + Action onEntry, + Action onClose) + { + _onEntry = onEntry; + _onClose = onClose; + _title.text = request.Title; + ClearChildren(_content); + + if (request.Entries.Count == 0) + { + var empty = UiKit.CreateText( + _content, + "Empty", + request.EmptyText, + 17, + new Color(0.62f, 0.68f, 0.82f)); + UiKit.SetPreferredHeight(empty, 80f); + } + else + { + foreach (var entry in request.Entries) + AddTile(entry); + } + + _root.SetAsLastSibling(); + _root.gameObject.SetActive(true); + } + + public void Hide() + { + _root.gameObject.SetActive(false); + } + + private void AddTile(CardListEntry entry) + { + var tile = UiKit.CreatePanel( + _content, + "Card_" + (entry.Card != null ? entry.Card.InstanceId.ToString() : "hidden"), + Color.white); + var layout = tile.gameObject.AddComponent(); + layout.preferredWidth = 126f; + layout.preferredHeight = 178f; + layout.flexibleWidth = 0f; + + var image = tile.GetComponent(); + image.raycastTarget = true; + + var meta = entry.Card != null + ? _catalog?.Invoke(entry.Card.PrintingId) + : null; + + if (entry.Hidden) + { + image.sprite = CardArt.Back; + image.color = Color.white; + } + else + { + CardImageService.Apply( + image, + meta != null ? meta.ImageKey : null, + CardArt.ForCard( + meta != null ? meta.NameId : entry.Card.PrintingId, + meta != null ? meta.Clan : "None")); + } + + var shade = UiKit.CreatePanel(tile, "Shade", new Color(0f, 0f, 0f, 0.76f)); + shade.anchorMin = new Vector2(0f, 0f); + shade.anchorMax = new Vector2(1f, 0f); + shade.pivot = new Vector2(0.5f, 0f); + shade.anchoredPosition = Vector2.zero; + shade.sizeDelta = new Vector2(0f, 44f); + shade.GetComponent().raycastTarget = false; + + var name = UiKit.CreateText( + tile, + "Name", + entry.Hidden + ? (entry.Label ?? "Закрытая карта") + : (meta != null ? Loc.CardName(meta) : entry.Card.PrintingId), + 12, + Color.white, + TextAnchor.LowerCenter); + UiKit.MakeLiteral(name); + var nameRect = name.rectTransform; + nameRect.anchorMin = new Vector2(0f, 0f); + nameRect.anchorMax = new Vector2(1f, 0f); + nameRect.pivot = new Vector2(0.5f, 0f); + nameRect.anchoredPosition = new Vector2(0f, 22f); + nameRect.sizeDelta = new Vector2(-8f, 20f); + name.horizontalOverflow = HorizontalWrapMode.Wrap; + name.verticalOverflow = VerticalWrapMode.Truncate; + name.raycastTarget = false; + + var stats = UiKit.CreateText( + tile, + "Stats", + entry.Hidden || entry.Card == null + ? string.Empty + : "P " + entry.Card.Power + " C " + entry.Card.Critical, + 10, + new Color(0.78f, 0.84f, 0.95f), + TextAnchor.LowerCenter); + UiKit.MakeLiteral(stats); + var statsRect = stats.rectTransform; + statsRect.anchorMin = new Vector2(0f, 0f); + statsRect.anchorMax = new Vector2(1f, 0f); + statsRect.pivot = new Vector2(0.5f, 0f); + statsRect.anchoredPosition = new Vector2(0f, 5f); + statsRect.sizeDelta = new Vector2(-8f, 16f); + stats.raycastTarget = false; + + var button = tile.gameObject.AddComponent