Add shared card list overlays, zone blocks and opponent hand backs
CI / test (push) Successful in 2m16s
Details
CI / test (push) Successful in 2m16s
Details
This commit is contained in:
parent
1ee8e7fb31
commit
309b56e4d7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,27 @@ namespace Vanguard.Board3D
|
|||
return new HandPose(new Vector3(x, y, z), yaw);
|
||||
}
|
||||
|
||||
/// <summary>Веер рубашек противника у дальнего края стола.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ближайшая цель для броска карты. Возвращает false, если карта
|
||||
/// отпущена мимо кругов и общего круга защиты.
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ namespace Vanguard.Board3D
|
|||
Vanguard = Card("BT01/020EN", 101),
|
||||
RearGuards = new CardView[5],
|
||||
Hand = new List<CardView>(),
|
||||
HandCount = 3,
|
||||
Guardian = new List<CardView>(),
|
||||
Soul = new List<CardView> { Card("BT01/021EN", 131) },
|
||||
FaceDownDamage = 1,
|
||||
|
|
|
|||
|
|
@ -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<CardSpec> 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BoardSlotKey> _highlightSlots = new HashSet<BoardSlotKey>();
|
||||
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<Image>();
|
||||
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<LayoutElement>();
|
||||
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<CardView> 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<LayoutElement>();
|
||||
rowElement.preferredHeight = 106f;
|
||||
UiKit.AddHorizontalLayout(row, 6, 10);
|
||||
|
||||
var art = UiKit.CreatePanel(row, "Art", Color.white);
|
||||
var artElement = art.gameObject.AddComponent<LayoutElement>();
|
||||
artElement.preferredWidth = 64f;
|
||||
artElement.preferredHeight = 92f;
|
||||
artElement.flexibleWidth = 0f;
|
||||
|
||||
_catalog.TryGetValue(card.PrintingId, out var meta);
|
||||
CardImageService.Apply(
|
||||
art.GetComponent<Image>(),
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7f1073196b40e8f498b8c27ce6722408
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Vanguard.Api.Contracts;
|
||||
|
||||
namespace Vanguard.UI.Overlays
|
||||
{
|
||||
/// <summary>Крупная карточка с полным текстом. Для скрытых карт показывает рубашку.</summary>
|
||||
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<string, CardCatalogItemDto> _catalog;
|
||||
private Action _onBack;
|
||||
|
||||
public CardDetailOverlay(
|
||||
Transform parent,
|
||||
Func<string, CardCatalogItemDto> 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<LayoutElement>();
|
||||
bodyElement.flexibleHeight = 1f;
|
||||
bodyElement.minHeight = 500f;
|
||||
UiKit.AddHorizontalLayout(body, 0, 16);
|
||||
|
||||
var artPanel = UiKit.CreatePanel(body, "Art", Color.white);
|
||||
var artElement = artPanel.gameObject.AddComponent<LayoutElement>();
|
||||
artElement.preferredWidth = 300f;
|
||||
artElement.preferredHeight = 460f;
|
||||
artElement.flexibleWidth = 0f;
|
||||
_art = artPanel.GetComponent<Image>();
|
||||
_art.raycastTarget = false;
|
||||
|
||||
var info = UiKit.CreatePanel(body, "Info", Color.clear);
|
||||
var infoElement = info.gameObject.AddComponent<LayoutElement>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0fafe71d7dd0d2e479eea533f73ea531
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
using System.Collections.Generic;
|
||||
using Vanguard.Board3D;
|
||||
using Vanguard.Shared.Protocol;
|
||||
|
||||
namespace Vanguard.UI.Overlays
|
||||
{
|
||||
/// <summary>Один тайл в общем списке карт. Закрытые карты не раскрывают printingId.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Запрос на общий горизонтальный список карт.</summary>
|
||||
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<CardListEntry> Entries { get; } = new List<CardListEntry>();
|
||||
public string EmptyText { get; set; } = "В этой зоне нет карт.";
|
||||
}
|
||||
|
||||
/// <summary>Собирает содержимое зоны для CardListView без раскрытия скрытых данных.</summary>
|
||||
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<CardView>();
|
||||
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<CardView> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5790362145aee4c41994f11e199cb921
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Единый горизонтальный список карт для Deck/Drop/Soul/Damage/Trigger/Hand.
|
||||
/// Порядок задаёт адаптер: первой идёт карта, лежащая сверху зоны.
|
||||
/// </summary>
|
||||
public sealed class CardListView
|
||||
{
|
||||
private readonly RectTransform _root;
|
||||
private readonly RectTransform _content;
|
||||
private readonly Text _title;
|
||||
private readonly Func<string, CardCatalogItemDto> _catalog;
|
||||
private Action<CardListEntry> _onEntry;
|
||||
private Action _onClose;
|
||||
|
||||
public CardListView(
|
||||
Transform parent,
|
||||
Func<string, CardCatalogItemDto> 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<LayoutElement>();
|
||||
scrollElement.flexibleHeight = 1f;
|
||||
scrollElement.minHeight = 400f;
|
||||
|
||||
_root.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public bool IsVisible => _root.gameObject.activeSelf;
|
||||
|
||||
public void Show(
|
||||
CardListRequest request,
|
||||
Action<CardListEntry> 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<LayoutElement>();
|
||||
layout.preferredWidth = 126f;
|
||||
layout.preferredHeight = 178f;
|
||||
layout.flexibleWidth = 0f;
|
||||
|
||||
var image = tile.GetComponent<Image>();
|
||||
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<Image>().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<Button>();
|
||||
button.targetGraphic = image;
|
||||
button.onClick.AddListener(() => _onEntry?.Invoke(entry));
|
||||
}
|
||||
|
||||
private static void ClearChildren(Transform parent)
|
||||
{
|
||||
for (var i = parent.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = parent.GetChild(i).gameObject;
|
||||
if (Application.isPlaying)
|
||||
UnityEngine.Object.Destroy(child);
|
||||
else
|
||||
UnityEngine.Object.DestroyImmediate(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6cbcb3db6c46da24b9a93d48776a71da
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using Vanguard.Api.Contracts;
|
||||
using Vanguard.Board3D;
|
||||
using Vanguard.Shared.Protocol;
|
||||
|
||||
namespace Vanguard.UI.Overlays
|
||||
{
|
||||
/// <summary>
|
||||
/// Один стек экранов поверх боя: список карт → детали карты.
|
||||
/// Возврат из деталей всегда ведёт обратно в тот же список.
|
||||
/// </summary>
|
||||
public sealed class OverlayNavigationService
|
||||
{
|
||||
private readonly RectTransform _root;
|
||||
private readonly CardListView _list;
|
||||
private readonly CardDetailOverlay _detail;
|
||||
private CardListRequest _currentRequest;
|
||||
|
||||
public OverlayNavigationService(
|
||||
Transform parent,
|
||||
Func<string, CardCatalogItemDto> catalog)
|
||||
{
|
||||
_root = UiKit.CreatePanel(parent, "BattleOverlays", Color.clear);
|
||||
_root.gameObject.SetActive(false);
|
||||
|
||||
_list = new CardListView(_root, catalog);
|
||||
_detail = new CardDetailOverlay(_root, catalog);
|
||||
}
|
||||
|
||||
public bool IsOpen => _root.gameObject.activeSelf;
|
||||
|
||||
public event Action<bool> OpenChanged;
|
||||
|
||||
public void OpenZone(PlayerView player, int viewer, BoardZone zone)
|
||||
{
|
||||
OpenList(CardZoneContent.Build(player, viewer, zone));
|
||||
}
|
||||
|
||||
public void OpenList(CardListRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
return;
|
||||
|
||||
_currentRequest = request;
|
||||
_detail.Hide();
|
||||
_list.Show(request, OpenDetail, CloseAll);
|
||||
_root.SetAsLastSibling();
|
||||
_root.gameObject.SetActive(true);
|
||||
OpenChanged?.Invoke(true);
|
||||
}
|
||||
|
||||
private void OpenDetail(CardListEntry entry)
|
||||
{
|
||||
_list.Hide();
|
||||
_detail.Show(entry, BackToList);
|
||||
_root.SetAsLastSibling();
|
||||
_root.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void BackToList()
|
||||
{
|
||||
_detail.Hide();
|
||||
if (_currentRequest == null)
|
||||
{
|
||||
CloseAll();
|
||||
return;
|
||||
}
|
||||
|
||||
_list.Show(_currentRequest, OpenDetail, CloseAll);
|
||||
}
|
||||
|
||||
public void CloseAll()
|
||||
{
|
||||
_list?.Hide();
|
||||
_detail?.Hide();
|
||||
_root.gameObject.SetActive(false);
|
||||
_currentRequest = null;
|
||||
OpenChanged?.Invoke(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_root == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying)
|
||||
UnityEngine.Object.Destroy(_root.gameObject);
|
||||
else
|
||||
UnityEngine.Object.DestroyImmediate(_root.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0793b24a2d44b8d4e8450ce1cad7c8ef
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Vanguard.Board3D;
|
||||
using Vanguard.Shared.Protocol;
|
||||
|
||||
namespace Vanguard.UI.Overlays
|
||||
{
|
||||
/// <summary>
|
||||
/// Кликабельные блоки зон: рука, колода, дроп, душа, урон, защита, триггер.
|
||||
/// Своя панель снизу слева, противника — сверху справа.
|
||||
/// </summary>
|
||||
public sealed class ZoneBlockStripView
|
||||
{
|
||||
private static readonly BoardZone[] Zones =
|
||||
{
|
||||
BoardZone.Hand,
|
||||
BoardZone.Deck,
|
||||
BoardZone.Drop,
|
||||
BoardZone.Soul,
|
||||
BoardZone.Damage,
|
||||
BoardZone.Guardian,
|
||||
BoardZone.Trigger,
|
||||
};
|
||||
|
||||
private readonly Dictionary<BoardZone, Text> _labels =
|
||||
new Dictionary<BoardZone, Text>();
|
||||
private Text _header;
|
||||
|
||||
public RectTransform Root { get; private set; }
|
||||
public event Action<BoardZone> ZoneClicked;
|
||||
|
||||
public void Build(
|
||||
Transform parent,
|
||||
string name,
|
||||
Vector2 anchor,
|
||||
Vector2 pivot,
|
||||
Vector2 position,
|
||||
Vector2 size)
|
||||
{
|
||||
Root = UiKit.CreatePanel(parent, name, new Color(0.02f, 0.035f, 0.055f, 0.82f));
|
||||
Root.anchorMin = anchor;
|
||||
Root.anchorMax = anchor;
|
||||
Root.pivot = pivot;
|
||||
Root.anchoredPosition = position;
|
||||
Root.sizeDelta = size;
|
||||
|
||||
var image = Root.GetComponent<Image>();
|
||||
if (image != null)
|
||||
image.raycastTarget = false;
|
||||
|
||||
UiKit.AddVerticalLayout(Root, 8, 4);
|
||||
|
||||
_header = UiKit.CreateText(
|
||||
Root,
|
||||
"Header",
|
||||
string.Empty,
|
||||
14,
|
||||
new Color(0.86f, 0.91f, 1f),
|
||||
TextAnchor.MiddleLeft);
|
||||
_header.fontStyle = FontStyle.Bold;
|
||||
UiKit.MakeLiteral(_header);
|
||||
UiKit.SetPreferredHeight(_header, 20f);
|
||||
|
||||
var firstRow = CreateRow("Row1");
|
||||
var secondRow = CreateRow("Row2");
|
||||
|
||||
for (var i = 0; i < Zones.Length; i++)
|
||||
{
|
||||
var zone = Zones[i];
|
||||
var row = i < 4 ? firstRow : secondRow;
|
||||
var button = UiKit.CreateButton(
|
||||
row,
|
||||
"Zone_" + zone,
|
||||
string.Empty,
|
||||
() => ZoneClicked?.Invoke(zone),
|
||||
13);
|
||||
var element = UiKit.SetPreferredHeight(button, 32f);
|
||||
element.preferredWidth = i < 4 ? 80f : 108f;
|
||||
_labels[zone] = button.GetComponentInChildren<Text>();
|
||||
UiKit.MakeLiteral(_labels[zone]);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(PlayerView player, bool self)
|
||||
{
|
||||
if (Root == null)
|
||||
return;
|
||||
|
||||
_header.text = Loc.Translate(self ? "ВЫ" : "ПРОТИВНИК")
|
||||
+ (player != null && !string.IsNullOrEmpty(player.Name)
|
||||
? " · " + player.Name
|
||||
: string.Empty);
|
||||
|
||||
var hand = player != null
|
||||
? Mathf.Max(player.HandCount, player.Hand?.Count ?? 0)
|
||||
: 0;
|
||||
var deck = player?.DeckCount ?? 0;
|
||||
var drop = player?.Drop?.Count ?? 0;
|
||||
var soul = player?.Soul?.Count ?? 0;
|
||||
var damage = player != null
|
||||
? player.FaceDownDamage + (player.FaceUpDamage?.Count ?? 0)
|
||||
: 0;
|
||||
var guardian = player?.Guardian?.Count ?? 0;
|
||||
var trigger = player?.Trigger?.Count ?? 0;
|
||||
|
||||
Set(BoardZone.Hand, "Рука", hand);
|
||||
Set(BoardZone.Deck, "Колода", deck);
|
||||
Set(BoardZone.Drop, "Дроп", drop);
|
||||
Set(BoardZone.Soul, "Душа", soul);
|
||||
Set(BoardZone.Damage, "Урон", damage);
|
||||
Set(BoardZone.Guardian, "Защита", guardian);
|
||||
Set(BoardZone.Trigger, "Триг.", trigger);
|
||||
}
|
||||
|
||||
private RectTransform CreateRow(string name)
|
||||
{
|
||||
var row = UiKit.CreatePanel(Root, name, Color.clear);
|
||||
var element = row.gameObject.AddComponent<LayoutElement>();
|
||||
element.preferredHeight = 32f;
|
||||
UiKit.AddHorizontalLayout(row, 0, 4);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void Set(BoardZone zone, string label, int count)
|
||||
{
|
||||
if (_labels.TryGetValue(zone, out var text) && text != null)
|
||||
text.text = Loc.Translate(label) + " " + count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 12e4949ad2c32ff4194ab689bb9408db
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using Vanguard.Board3D;
|
||||
using Vanguard.Shared.Protocol;
|
||||
using Vanguard.UI;
|
||||
using Vanguard.UI.Overlays;
|
||||
|
||||
namespace Vanguard.Tests
|
||||
{
|
||||
public sealed class CardOverlayTests
|
||||
{
|
||||
[Test]
|
||||
public void Zone_Content_Shows_Last_Added_First_And_Hides_Opponent_Hand()
|
||||
{
|
||||
var first = Card("BT01/001EN", 1);
|
||||
var last = Card("BT01/002EN", 2);
|
||||
var player = new PlayerView
|
||||
{
|
||||
Index = 0,
|
||||
Hand = new List<CardView> { first, last },
|
||||
HandCount = 2,
|
||||
Drop = new List<CardView> { first, last },
|
||||
FaceUpDamage = new List<CardView> { first },
|
||||
FaceDownDamage = 2,
|
||||
Soul = new List<CardView> { first, last },
|
||||
};
|
||||
|
||||
var drop = CardZoneContent.Build(player, viewer: 0, BoardZone.Drop);
|
||||
Assert.AreSame(last, drop.Entries[0].Card);
|
||||
|
||||
var ownHand = CardZoneContent.Build(player, viewer: 0, BoardZone.Hand);
|
||||
Assert.AreSame(last, ownHand.Entries[0].Card);
|
||||
|
||||
var opponentHand = CardZoneContent.Build(player, viewer: 1, BoardZone.Hand);
|
||||
Assert.AreEqual(2, opponentHand.Entries.Count);
|
||||
Assert.IsTrue(opponentHand.Entries.All(entry => entry.Hidden));
|
||||
|
||||
var damage = CardZoneContent.Build(player, viewer: 0, BoardZone.Damage);
|
||||
Assert.AreEqual(3, damage.Entries.Count);
|
||||
Assert.IsTrue(damage.Entries.Skip(1).All(entry => entry.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CardList_Opens_Detail_And_Returns_To_List()
|
||||
{
|
||||
var canvas = UiKit.CreateCanvas("Overlay Test Canvas");
|
||||
var overlays = new OverlayNavigationService(canvas.transform, _ => null);
|
||||
try
|
||||
{
|
||||
var request = new CardListRequest("Тест", 0, BoardZone.Drop);
|
||||
request.Entries.Add(new CardListEntry(Card("BT01/001EN", 1)));
|
||||
request.Entries.Add(new CardListEntry(null, true, "Закрытая карта"));
|
||||
|
||||
overlays.OpenList(request);
|
||||
Assert.IsTrue(overlays.IsOpen);
|
||||
Assert.IsTrue(Exists("CardListView"));
|
||||
|
||||
var tile = Resources.FindObjectsOfTypeAll<Button>()
|
||||
.FirstOrDefault(button => button.name.StartsWith("Card_"));
|
||||
Assert.IsNotNull(tile);
|
||||
tile.onClick.Invoke();
|
||||
|
||||
Assert.IsTrue(Exists("CardDetailOverlay"));
|
||||
|
||||
var back = Resources.FindObjectsOfTypeAll<Button>()
|
||||
.FirstOrDefault(button => button.name == "Back");
|
||||
Assert.IsNotNull(back);
|
||||
back.onClick.Invoke();
|
||||
Assert.IsTrue(Exists("CardListView"));
|
||||
Assert.IsFalse(Exists("CardDetailOverlay"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
overlays.Dispose();
|
||||
Object.DestroyImmediate(canvas.gameObject);
|
||||
var eventSystem = Object.FindAnyObjectByType<EventSystem>();
|
||||
if (eventSystem != null)
|
||||
Object.DestroyImmediate(eventSystem.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static CardView Card(string id, int instanceId) => new CardView
|
||||
{
|
||||
PrintingId = id,
|
||||
InstanceId = instanceId,
|
||||
FaceUp = true,
|
||||
Power = 8000,
|
||||
BasePower = 8000,
|
||||
Critical = 1,
|
||||
BaseCritical = 1,
|
||||
};
|
||||
|
||||
private static bool Exists(string name) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == name && go.activeInHierarchy);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a82c53475672b7d47b6e681f28487279
|
||||
|
|
@ -237,19 +237,18 @@ namespace Vanguard.Tests
|
|||
card.Ref.Zone == BoardZone.Guardian
|
||||
&& card.Ref.InstanceId == 7));
|
||||
|
||||
var selfStats = Resources.FindObjectsOfTypeAll<Text>()
|
||||
.FirstOrDefault(text => text.name == "Stats"
|
||||
&& text.transform.parent != null
|
||||
&& text.transform.parent.name == "SelfStats");
|
||||
var opponentStats = Resources.FindObjectsOfTypeAll<Text>()
|
||||
.FirstOrDefault(text => text.name == "Stats"
|
||||
&& text.transform.parent != null
|
||||
&& text.transform.parent.name == "OpponentStats");
|
||||
Assert.IsNotNull(selfStats);
|
||||
Assert.IsNotNull(opponentStats);
|
||||
StringAssert.Contains("Рука 1", selfStats.text);
|
||||
StringAssert.Contains("Душа 1", selfStats.text);
|
||||
StringAssert.Contains("Рука 0", opponentStats.text);
|
||||
var selfZoneRoot = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.FirstOrDefault(go => go.name == "ZoneBlocksSelf");
|
||||
var opponentZoneRoot = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.FirstOrDefault(go => go.name == "ZoneBlocksOpponent");
|
||||
Assert.IsNotNull(selfZoneRoot);
|
||||
Assert.IsNotNull(opponentZoneRoot);
|
||||
|
||||
var selfLabels = selfZoneRoot.GetComponentsInChildren<Text>();
|
||||
var opponentLabels = opponentZoneRoot.GetComponentsInChildren<Text>();
|
||||
Assert.IsTrue(selfLabels.Any(text => text.text.Contains("Рука 1")));
|
||||
Assert.IsTrue(selfLabels.Any(text => text.text.Contains("Душа 1")));
|
||||
Assert.IsTrue(opponentLabels.Any(text => text.text.Contains("Рука 0")));
|
||||
|
||||
Cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ namespace Vanguard.Tests
|
|||
Assert.IsTrue(cards.Any(card =>
|
||||
card.Ref.Zone == BoardZone.Hand
|
||||
&& card.Ref.Player == 0));
|
||||
var opponentHand = cards
|
||||
.Where(card => card.Ref.Zone == BoardZone.Hand
|
||||
&& card.Ref.Player == 1)
|
||||
.ToList();
|
||||
Assert.AreEqual(3, opponentHand.Count);
|
||||
Assert.IsTrue(opponentHand.All(card => !card.IsFaceUp));
|
||||
Assert.IsTrue(cards.Any(card =>
|
||||
card.Ref.Zone == BoardZone.Damage
|
||||
&& card.Ref.Player == 0));
|
||||
|
|
@ -90,7 +96,8 @@ namespace Vanguard.Tests
|
|||
board.Render(BuildView(you: 0), new BoardRenderState());
|
||||
|
||||
var hand = ActiveCards()
|
||||
.Where(card => card.Ref.Zone == BoardZone.Hand)
|
||||
.Where(card => card.Ref.Zone == BoardZone.Hand
|
||||
&& card.Ref.Player == 0)
|
||||
.ToList();
|
||||
Assert.AreEqual(2, hand.Count);
|
||||
|
||||
|
|
@ -235,6 +242,7 @@ namespace Vanguard.Tests
|
|||
Vanguard = Card("BT01/010EN", 21),
|
||||
RearGuards = new CardView[5],
|
||||
Hand = new List<CardView>(),
|
||||
HandCount = 3,
|
||||
Guardian = new List<CardView>(),
|
||||
Soul = new List<CardView>(),
|
||||
FaceUpDamage = new List<CardView>(),
|
||||
|
|
|
|||
|
|
@ -883,9 +883,31 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||||
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
||||
проверяется `SceneSmoke.RunPlaymat`.
|
||||
- Проверки: EditMode **20/20** (геометрия, зеркальность, правила, рендер
|
||||
- Проверки: EditMode **22/22** (геометрия, зеркальность, правила, рендер
|
||||
3D-карт, пул, рука в поле зрения, обмен колоды/дропа, триггер-зона),
|
||||
`SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=22 hand=5/5 fieldVisible=True`.
|
||||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
||||
модели карт/стола — процедурные примитивы.
|
||||
|
||||
## 2026-10-23 — UX Phase 1: единые списки карт и зоны
|
||||
|
||||
- Добавлен общий `CardListView` (Screen Space, горизонтальный ScrollRect,
|
||||
порядок «последняя добавленная первой») и `CardDetailOverlay` (арт, имя,
|
||||
клан/тип/грейд, P/Shield/C, триггер, текст). Навигация:
|
||||
`OverlayNavigationService` — стек «список → детали → назад», закрытие
|
||||
возвращает к списку.
|
||||
- Зоны стали кликабельными блоками UI: `Рука / Колода / Дроп / Душа / Урон /
|
||||
Защита / Триггер`; своя панель снизу слева, противника — сверху справа.
|
||||
3D-стопки и правый клик по VC используют тот же `CardListView`.
|
||||
- Скрытые данные: колода — только закрытые слоты по количеству; рука
|
||||
противника — только рубашки; закрытый урон — «Закрытая карта». Рука
|
||||
противника дополнительно рисуется веером закрытых карт у дальнего края
|
||||
стола (вариант 1), identities не запрашиваются.
|
||||
- Старые `SoulOverlay`/`ShowDrop`/`ShowSoul` удалены; единый оверлей
|
||||
используется для Deck/Drop/Soul/Damage/Guardian/Trigger/Hand.
|
||||
- Проверки: клиент EditMode **22/22** (включая `CardListView`/Detail и
|
||||
зоны), `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
- Дальше по плану: Phase 2 — `InputLockService` + `AnimationConfig` +
|
||||
контекстное меню; Phase 3 — Power/Shield и стрелка; Phase 4 — Guardian
|
||||
стопка, Trigger mirror, debug-подписи.
|
||||
|
|
|
|||
|
|
@ -80,12 +80,16 @@
|
|||
Над колодой — зона проверки триггера. Под картами V/R — динамические
|
||||
`P/C`.
|
||||
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
||||
противника — сверху справа. Душа — юниты под VC: отдельной стопки нет,
|
||||
противника — сверху справа. Блоки `Рука/Колода/Дроп/Душа/Урон/Защита/Триггер`
|
||||
кликабельны и открывают общий горизонтальный `CardListView`; из списка
|
||||
открывается `CardDetailOverlay`. Для колоды и руки противника показываются
|
||||
только закрытые карты. Душа — юниты под VC: отдельной стопки нет,
|
||||
ПКМ по VC открывает список. Дроп по умолчанию пуст; сверху показывает
|
||||
последнюю карту, ЛКМ — список всего дропа. Колода и дроп поменяны
|
||||
местами (колода ближе к игроку). Справа от круга защиты — панель фаз:
|
||||
конец муллигана (авто после обмена), скип смены, конец главной/боевой;
|
||||
боевая завершается сама, если нет атак и активаций.
|
||||
боевая завершается сама, если нет атак и активаций. Рука противника —
|
||||
веер закрытых карт у дальнего края стола, только рубашки.
|
||||
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
||||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||
Counter Blast — открытую карту до активации способности.
|
||||
|
|
@ -148,12 +152,12 @@ curl http://127.0.0.1:18080/health
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **20/20** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **22/22** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона),
|
||||
`SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=22 hand=5/5 fieldVisible=True`.
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
CardListView/Detail, зоны и рука противника), `SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||
|
|
@ -179,6 +183,12 @@ client/Assets/Vanguard/Runtime/UI/
|
|||
LoginView.cs MetaView.cs OnboardingView.cs
|
||||
DeckBuilderView.cs QueueView.cs BattleView.cs
|
||||
UiKit.cs Localization.cs
|
||||
client/Assets/Vanguard/Runtime/UI/Overlays/
|
||||
OverlayNavigationService.cs стек список → детали
|
||||
CardListView.cs общий горизонтальный список
|
||||
CardDetailOverlay.cs крупная карта и текст
|
||||
CardListModels.cs зоны и скрытые карты
|
||||
ZoneBlockStripView.cs кликабельные блоки зон
|
||||
client/Assets/Vanguard/Runtime/Board3D/
|
||||
Config/PlaymatLayoutConfig.cs ScriptableObject-раскладка
|
||||
Data/BoardTypes.cs зоны, круги, CardState3D, refs
|
||||
|
|
@ -211,10 +221,12 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
|||
|
||||
## Следующие шаги
|
||||
|
||||
1. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||
2. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
||||
3. Звук и настройки, DOTween.
|
||||
4. Лаунчер WPF.
|
||||
1. UX-план 3D-поля: Phase 2 — InputLock/AnimationConfig и контекстное меню;
|
||||
затем Power/Shield + стрелка, затем Guardian/Trigger/подписи.
|
||||
2. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||
3. 3D-наполнение: модели карт/стола, prefab-экраны, анимации retire/ghost.
|
||||
4. Звук и настройки, DOTween.
|
||||
5. Лаунчер WPF.
|
||||
|
||||
Тренировка с ботом и боевой UI с артами/анимациями/локализацией уже есть.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue