Add procedural card graphics and battle board
CI / test (push) Successful in 5m15s
Details
CI / test (push) Successful in 5m15s
Details
This commit is contained in:
parent
e0deecacd5
commit
0e528586b4
|
|
@ -266,13 +266,15 @@
|
||||||
|
|
||||||
## 8. Клиент: боевая сцена `[20–30 дней]`
|
## 8. Клиент: боевая сцена `[20–30 дней]`
|
||||||
|
|
||||||
**Первый gray-box есть:** доска, рука, сила/крит, часы, все легальные
|
**Визуальный каркас есть:** доска, рука, карточные рамки/арт-заглушки
|
||||||
интенты кнопками, сдача и результат. Арты и анимации — следующие.
|
по клану, сила/крит, часы, все легальные интенты кнопками, сдача и
|
||||||
Финальный визуал — отдельная 3D-сцена `Battle`, а не текущий Canvas
|
результат. Реальные арты и анимации — следующие. Финальный визуал —
|
||||||
|
отдельная 3D-сцена `Battle`, а не текущий Canvas
|
||||||
(`docs/client-visual-direction.md`).
|
(`docs/client-visual-direction.md`).
|
||||||
|
|
||||||
- [x] Поле: Vanguard, 5 кругов Арьергарда, рука, damage/soul/drop-счётчики
|
- [x] Поле: Vanguard + 5 кругов Арьергарда, рука, damage/soul/drop-счётчики.
|
||||||
(без артов и анимаций).
|
- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер,
|
||||||
|
сила/крит; REST/скрытая карта.
|
||||||
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
||||||
- [x] Ввод: кнопки легальных интентов; подсветка целей — дальше.
|
- [x] Ввод: кнопки легальных интентов; подсветка целей — дальше.
|
||||||
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
| CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` |
|
| CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` |
|
||||||
| EF-миграции | готово (`InitialCreate`) |
|
| EF-миграции | готово (`InitialCreate`) |
|
||||||
| Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` |
|
| Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` |
|
||||||
| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, gray-box бой |
|
| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, бой с процедурными картами |
|
||||||
| Лаунчер | не начато |
|
| Лаунчер | не начато |
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private readonly MetaService _meta;
|
private readonly MetaService _meta;
|
||||||
private readonly Action _onExit;
|
private readonly Action _onExit;
|
||||||
|
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
|
||||||
|
new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal);
|
||||||
|
private bool _catalogLoading;
|
||||||
|
|
||||||
private readonly RectTransform _root;
|
private readonly RectTransform _root;
|
||||||
private readonly RectTransform _board;
|
private readonly RectTransform _board;
|
||||||
|
|
@ -138,6 +141,7 @@ namespace Vanguard.UI
|
||||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||||
SetStatus("Подключаемся к матчу…", Muted);
|
SetStatus("Подключаемся к матчу…", Muted);
|
||||||
_root.gameObject.SetActive(true);
|
_root.gameObject.SetActive(true);
|
||||||
|
_ = LoadCatalogAsync();
|
||||||
_ = ConnectAsync();
|
_ = ConnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,6 +195,31 @@ namespace Vanguard.UI
|
||||||
return overlay;
|
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()
|
private async Task ConnectAsync()
|
||||||
{
|
{
|
||||||
if (_connecting || _info == null)
|
if (_connecting || _info == null)
|
||||||
|
|
@ -430,9 +459,9 @@ namespace Vanguard.UI
|
||||||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You)
|
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You)
|
||||||
?? (_view.Players.Count > _view.You ? _view.Players[_view.You] : null);
|
?? (_view.Players.Count > _view.You ? _view.Players[_view.You] : null);
|
||||||
|
|
||||||
AddPlayerBlock(opponent, false);
|
AddFieldBlock(opponent, false);
|
||||||
AddMatchInfo(_view);
|
AddCenterPanel(_view);
|
||||||
AddPlayerBlock(self, true);
|
AddFieldBlock(self, true);
|
||||||
AddHand(self);
|
AddHand(self);
|
||||||
RebuildIntents(_view);
|
RebuildIntents(_view);
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
|
|
@ -441,167 +470,270 @@ namespace Vanguard.UI
|
||||||
ShowResult(_view);
|
ShowResult(_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPlayerBlock(PlayerView player, bool self)
|
private void AddFieldBlock(PlayerView player, bool self)
|
||||||
{
|
{
|
||||||
if (player == null)
|
if (player == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var block = UiKit.CreatePanel(
|
var block = UiKit.CreatePanel(
|
||||||
_board,
|
_board,
|
||||||
self ? "SelfBlock" : "OpponentBlock",
|
self ? "SelfField" : "OpponentField",
|
||||||
Panel);
|
new Color(0.05f, 0.07f, 0.11f, 0.96f));
|
||||||
var blockElement = block.gameObject.AddComponent<LayoutElement>();
|
var blockElement = block.gameObject.AddComponent<LayoutElement>();
|
||||||
blockElement.preferredHeight = self ? 250f : 230f;
|
blockElement.preferredHeight = self ? 214f : 204f;
|
||||||
UiKit.AddVerticalLayout(block, 8, 4);
|
UiKit.AddVerticalLayout(block, 7, 3);
|
||||||
|
|
||||||
var header = UiKit.CreateText(
|
var header = UiKit.CreateText(
|
||||||
block,
|
block,
|
||||||
"Header",
|
"Header",
|
||||||
(self ? "Вы: " : "Противник: ") + player.Name
|
(self ? "ВЫ" : "ПРОТИВНИК") + " · " + player.Name
|
||||||
+ " · рука " + player.HandCount
|
+ " рука " + player.HandCount
|
||||||
+ " · колода " + player.DeckCount
|
+ " · колода " + player.DeckCount
|
||||||
+ " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count)
|
+ " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count)
|
||||||
+ " · soul " + player.Soul.Count
|
+ " · soul " + player.Soul.Count
|
||||||
+ " · drop " + player.Drop.Count,
|
+ " · drop " + player.Drop.Count,
|
||||||
17,
|
15,
|
||||||
self ? Color.white : new Color(0.86f, 0.80f, 0.72f),
|
self ? Color.white : new Color(0.94f, 0.82f, 0.70f),
|
||||||
TextAnchor.MiddleLeft);
|
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 row = UiKit.CreatePanel(block, "Field", new Color(0f, 0f, 0f, 0f));
|
||||||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||||||
rowElement.flexibleHeight = 1f;
|
rowElement.flexibleHeight = 1f;
|
||||||
rowElement.minHeight = 150f;
|
rowElement.minHeight = 166f;
|
||||||
UiKit.AddHorizontalLayout(row, 0, 6);
|
UiKit.AddHorizontalLayout(row, 3, 7);
|
||||||
|
|
||||||
AddCardBox(row, player.Vanguard, "Vanguard");
|
AddCardBox(row, player.Vanguard, "V", compact: false);
|
||||||
for (var i = 0; i < 5; i++)
|
for (var i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
var card = player.RearGuards != null && i < player.RearGuards.Length
|
var card = player.RearGuards != null && i < player.RearGuards.Length
|
||||||
? player.RearGuards[i]
|
? player.RearGuards[i]
|
||||||
: null;
|
: 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<LayoutElement>();
|
||||||
|
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<string>();
|
||||||
|
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)
|
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<LayoutElement>();
|
var handElement = hand.gameObject.AddComponent<LayoutElement>();
|
||||||
handElement.preferredHeight = 190f;
|
handElement.preferredHeight = 188f;
|
||||||
UiKit.AddVerticalLayout(hand, 8, 4);
|
UiKit.AddVerticalLayout(hand, 6, 3);
|
||||||
|
|
||||||
var header = UiKit.CreateText(
|
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);
|
UiKit.SetPreferredHeight(header, 22f);
|
||||||
|
|
||||||
var row = UiKit.CreatePanel(hand, "Cards", new Color(0f, 0f, 0f, 0f));
|
var scroll = UiKit.CreateHorizontalScrollView(hand, "HandScroll", out var content, 6f);
|
||||||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||||||
rowElement.flexibleHeight = 1f;
|
scrollElement.flexibleHeight = 1f;
|
||||||
UiKit.AddHorizontalLayout(row, 0, 6);
|
scrollElement.minHeight = 130f;
|
||||||
|
|
||||||
if (self?.Hand == null || self.Hand.Count == 0)
|
if (self?.Hand == null || self.Hand.Count == 0)
|
||||||
{
|
{
|
||||||
var empty = UiKit.CreateText(row, "Empty", "Рука пуста", 18, Muted);
|
var empty = UiKit.CreateText(
|
||||||
UiKit.SetPreferredHeight(empty, 120f);
|
content, "Empty", "Рука пуста", 17, Muted);
|
||||||
|
UiKit.SetPreferredHeight(empty, 110f);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var card in self.Hand)
|
foreach (var card in self.Hand)
|
||||||
AddCardBox(row, card, "Рука");
|
AddCardBox(content, card, "Рука", compact: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddMatchInfo(MatchView view)
|
private void AddInfoLine(Transform parent, string text)
|
||||||
{
|
|
||||||
var info = UiKit.CreatePanel(_board, "MatchInfo", Panel);
|
|
||||||
var element = info.gameObject.AddComponent<LayoutElement>();
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var line = UiKit.CreateText(
|
var line = UiKit.CreateText(
|
||||||
parent, "Info", text, 16, Muted, TextAnchor.MiddleLeft);
|
parent, "Info", text, 16, Muted, TextAnchor.MiddleLeft);
|
||||||
UiKit.SetPreferredHeight(line, 22f);
|
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 meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found)
|
||||||
var element = panel.gameObject.AddComponent<LayoutElement>();
|
? found
|
||||||
element.preferredWidth = 132f;
|
: null;
|
||||||
element.preferredHeight = 148f;
|
var clanColor = CardArt.ClanColor(meta != null ? meta.Clan : "None");
|
||||||
element.flexibleWidth = 0f;
|
var frameColor = card == null
|
||||||
UiKit.AddVerticalLayout(panel, 5, 2);
|
? new Color(0.16f, 0.18f, 0.24f)
|
||||||
|
: CardArt.ClanColor(meta != null ? meta.Clan : "None");
|
||||||
|
|
||||||
var slotText = UiKit.CreateText(panel, "Slot", slot, 12, Muted);
|
var panel = UiKit.CreatePanel(
|
||||||
UiKit.SetPreferredHeight(slotText, 16f);
|
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<LayoutElement>();
|
||||||
|
element.preferredWidth = compact ? 106f : 124f;
|
||||||
|
element.preferredHeight = compact ? 142f : 162f;
|
||||||
|
element.flexibleWidth = 0f;
|
||||||
|
|
||||||
|
var outline = panel.gameObject.AddComponent<Outline>();
|
||||||
|
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)
|
if (card == null)
|
||||||
{
|
{
|
||||||
var empty = UiKit.CreateText(panel, "Empty", "Пусто", 14, new Color(1f, 1f, 1f, 0.35f));
|
var emptyArt = UiKit.CreatePanel(panel, "EmptyArt", new Color(0.04f, 0.06f, 0.09f, 1f));
|
||||||
UiKit.SetPreferredHeight(empty, 60f);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var printing = UiKit.CreateText(
|
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
||||||
panel, "Printing", card.PrintingId, 12, Color.white, TextAnchor.UpperCenter);
|
UiKit.SetPreferredHeight(art, compact ? 48f : 64f);
|
||||||
printing.horizontalOverflow = HorizontalWrapMode.Wrap;
|
var artImage = art.GetComponent<Image>();
|
||||||
UiKit.SetPreferredHeight(printing, 42f);
|
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(
|
var stats = UiKit.CreateText(
|
||||||
panel, "Stats", card.Power + " / " + card.Critical, 16, Color.white);
|
panel, "Stats",
|
||||||
UiKit.SetPreferredHeight(stats, 22f);
|
card.FaceUp
|
||||||
|
? "P " + card.Power + " · C " + card.Critical
|
||||||
var stateText = (card.Rested ? "REST " : string.Empty)
|
+ (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty)
|
||||||
+ (card.FaceUp ? string.Empty : "FACE-DOWN");
|
: "СКРЫТА",
|
||||||
if (!string.IsNullOrEmpty(stateText))
|
11,
|
||||||
{
|
card.FaceUp && card.Power != card.BasePower
|
||||||
var state = UiKit.CreateText(panel, "State", stateText.Trim(), 12, new Color(1f, 0.76f, 0.38f));
|
? new Color(1f, 0.82f, 0.42f)
|
||||||
UiKit.SetPreferredHeight(state, 18f);
|
: 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)
|
private void RebuildIntents(MatchView view)
|
||||||
{
|
{
|
||||||
ClearChildren(_intents);
|
ClearChildren(_intents);
|
||||||
|
|
@ -681,7 +813,13 @@ namespace Vanguard.UI
|
||||||
private static void ClearChildren(Transform parent)
|
private static void ClearChildren(Transform parent)
|
||||||
{
|
{
|
||||||
for (var i = parent.childCount - 1; i >= 0; i--)
|
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)
|
private static string SlotName(int slot)
|
||||||
|
|
@ -699,14 +837,14 @@ namespace Vanguard.UI
|
||||||
case FinishMulliganIntent _: return "Закончить маллиган";
|
case FinishMulliganIntent _: return "Закончить маллиган";
|
||||||
case MulliganIntent mulligan: return "Маллиган: " + mulligan.HandIndexes.Count + " карт";
|
case MulliganIntent mulligan: return "Маллиган: " + mulligan.HandIndexes.Count + " карт";
|
||||||
case SkipRideIntent _: return "Пропустить Ride";
|
case SkipRideIntent _: return "Пропустить Ride";
|
||||||
case RideIntent ride: return "Ride: " + ride.PrintingId;
|
case RideIntent ride: return "Ride: " + CardName(ride.PrintingId);
|
||||||
case CallIntent call: return "Call " + call.PrintingId + " → " + SlotName(call.Slot);
|
case CallIntent call: return "Call " + CardName(call.PrintingId) + " → " + SlotName(call.Slot);
|
||||||
case MoveIntent move: return "Move " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot);
|
case MoveIntent move: return "Move " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot);
|
||||||
case EnterBattleIntent _: return "В фазу боя";
|
case EnterBattleIntent _: return "В фазу боя";
|
||||||
case AttackIntent attack:
|
case AttackIntent attack:
|
||||||
return "Атака " + SlotName(attack.AttackerSlot) + " → " + SlotName(attack.TargetSlot)
|
return "Атака " + SlotName(attack.AttackerSlot) + " → " + SlotName(attack.TargetSlot)
|
||||||
+ (attack.BoosterSlot >= 0 ? " + boost" : string.Empty);
|
+ (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 InterceptIntent intercept: return "Intercept: " + SlotName(intercept.Slot);
|
||||||
case PassGuardIntent _: return "Пропустить Guard";
|
case PassGuardIntent _: return "Пропустить Guard";
|
||||||
case ResolveTriggerIntent trigger:
|
case ResolveTriggerIntent trigger:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Vanguard.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Процедурные карточные текстуры без внешних артов: цвет клана,
|
||||||
|
/// градиент, рубашка. Позже заменяется Addressables-артом.
|
||||||
|
/// </summary>
|
||||||
|
public static class CardArt
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, Sprite> Cache =
|
||||||
|
new Dictionary<string, Sprite>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f6f3e2a1d9b4c7e8a5f0b3c2d1e6a94
|
||||||
|
|
@ -69,6 +69,7 @@ namespace Vanguard.UI
|
||||||
new Dictionary<string, Text>(StringComparer.Ordinal);
|
new Dictionary<string, Text>(StringComparer.Ordinal);
|
||||||
|
|
||||||
private DeckDto _editing;
|
private DeckDto _editing;
|
||||||
|
private int _dust;
|
||||||
private string _filterClan = "Все";
|
private string _filterClan = "Все";
|
||||||
private string _filterTrigger = "Все";
|
private string _filterTrigger = "Все";
|
||||||
private int _filterGrade = -1;
|
private int _filterGrade = -1;
|
||||||
|
|
@ -503,7 +504,9 @@ namespace Vanguard.UI
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var meta = _selectedMeta;
|
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;
|
_cardArtName.text = meta.NameEn;
|
||||||
_cardTitle.text = meta.NameEn;
|
_cardTitle.text = meta.NameEn;
|
||||||
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
|
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
|
||||||
|
|
@ -519,18 +522,22 @@ namespace Vanguard.UI
|
||||||
? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn)
|
? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn)
|
||||||
: meta.TextRu;
|
: meta.TextRu;
|
||||||
_cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0)
|
_cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0)
|
||||||
+ " · в колоде: " + UsedByName(meta.NameId);
|
+ " · в колоде: " + UsedByName(meta.NameId)
|
||||||
|
+ " · пыль: " + _dust;
|
||||||
|
|
||||||
var canChange = CanChange(meta.Rarity);
|
var canChange = CanChange(meta.Rarity);
|
||||||
|
var craftCost = CraftCostFor(meta.Rarity);
|
||||||
_dismantleButton.interactable = canChange && !_busy
|
_dismantleButton.interactable = canChange && !_busy
|
||||||
&& _selectedItem != null && _selectedItem.Count > 0;
|
&& _selectedItem != null && _selectedItem.Count > 0;
|
||||||
_craftButton.interactable = canChange && !_busy;
|
_craftButton.interactable = canChange && !_busy && _dust >= craftCost;
|
||||||
SetButtonLabel(
|
SetButtonLabel(
|
||||||
_dismantleButton,
|
_dismantleButton,
|
||||||
canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
|
canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
|
||||||
SetButtonLabel(
|
SetButtonLabel(
|
||||||
_craftButton,
|
_craftButton,
|
||||||
canChange ? "Скрафтить (−" + CraftCostFor(meta.Rarity) + " пыли)" : "Нельзя скрафтить");
|
canChange
|
||||||
|
? "Скрафтить (−" + craftCost + " пыли)"
|
||||||
|
: "Нельзя скрафтить");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CanChange(string rarity) =>
|
private static bool CanChange(string rarity) =>
|
||||||
|
|
@ -608,6 +615,7 @@ namespace Vanguard.UI
|
||||||
var collection = await _meta.GetCollectionAsync();
|
var collection = await _meta.GetCollectionAsync();
|
||||||
_collection.Clear();
|
_collection.Clear();
|
||||||
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
|
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
|
||||||
|
_dust = collection.Balances != null ? collection.Balances.Dust : 0;
|
||||||
RebuildCollectionGrids();
|
RebuildCollectionGrids();
|
||||||
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
|
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
|
||||||
if (_selectedItem == null)
|
if (_selectedItem == null)
|
||||||
|
|
@ -685,6 +693,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
_collection.Clear();
|
_collection.Clear();
|
||||||
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
|
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
|
||||||
|
_dust = collection.Balances != null ? collection.Balances.Dust : 0;
|
||||||
_catalog.Clear();
|
_catalog.Clear();
|
||||||
foreach (var item in catalog)
|
foreach (var item in catalog)
|
||||||
_catalog[item.PrintingId] = item;
|
_catalog[item.PrintingId] = item;
|
||||||
|
|
@ -695,8 +704,9 @@ namespace Vanguard.UI
|
||||||
BuildDeckList();
|
BuildDeckList();
|
||||||
RebuildCollectionGrids();
|
RebuildCollectionGrids();
|
||||||
_listStatus.text = _decks.Count == 0
|
_listStatus.text = _decks.Count == 0
|
||||||
? "Колод пока нет. Создайте первую. ПКМ по карте — подробности."
|
? "Колод пока нет. Пыль: " + _dust + ". ПКМ по карте — подробности."
|
||||||
: "Карт в коллекции: " + _collection.Count + " · ПКМ по карте — подробности.";
|
: "Карт в коллекции: " + _collection.Count + " · пыль " + _dust
|
||||||
|
+ " · ПКМ по карте — подробности.";
|
||||||
}
|
}
|
||||||
catch (ApiException exception)
|
catch (ApiException exception)
|
||||||
{
|
{
|
||||||
|
|
@ -855,6 +865,10 @@ namespace Vanguard.UI
|
||||||
UiKit.AddVerticalLayout(cell, 6, 2);
|
UiKit.AddVerticalLayout(cell, 6, 2);
|
||||||
AddRightClick(cell.gameObject, item.PrintingId, meta);
|
AddRightClick(cell.gameObject, item.PrintingId, meta);
|
||||||
|
|
||||||
|
var frame = cell.gameObject.AddComponent<Outline>();
|
||||||
|
frame.effectColor = CardArt.ClanColor(meta.Clan);
|
||||||
|
frame.effectDistance = new Vector2(2f, -2f);
|
||||||
|
|
||||||
if (onAdd != null)
|
if (onAdd != null)
|
||||||
{
|
{
|
||||||
var button = cell.gameObject.AddComponent<Button>();
|
var button = cell.gameObject.AddComponent<Button>();
|
||||||
|
|
@ -862,8 +876,12 @@ namespace Vanguard.UI
|
||||||
button.onClick.AddListener(() => onAdd(item));
|
button.onClick.AddListener(() => onAdd(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
var art = UiKit.CreatePanel(cell, "Art", ClanColor(meta.Clan));
|
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
||||||
UiKit.SetPreferredHeight(art, 112f);
|
UiKit.SetPreferredHeight(art, 112f);
|
||||||
|
var artImage = art.GetComponent<Image>();
|
||||||
|
artImage.sprite = CardArt.ForCard(meta.NameId, meta.Clan);
|
||||||
|
artImage.color = Color.white;
|
||||||
|
artImage.preserveAspect = false;
|
||||||
|
|
||||||
var artName = UiKit.CreateText(
|
var artName = UiKit.CreateText(
|
||||||
art, "ArtName", meta.NameEn, 12,
|
art, "ArtName", meta.NameEn, 12,
|
||||||
|
|
@ -943,8 +961,14 @@ namespace Vanguard.UI
|
||||||
AddRightClick(row.gameObject, printingId, meta);
|
AddRightClick(row.gameObject, printingId, meta);
|
||||||
|
|
||||||
var art = UiKit.CreatePanel(
|
var art = UiKit.CreatePanel(
|
||||||
row, "Art", ClanColor(meta != null ? meta.Clan : "None"));
|
row, "Art", Color.white);
|
||||||
SetFixedSize(art, 36f, 50f);
|
SetFixedSize(art, 36f, 50f);
|
||||||
|
var artImage = art.GetComponent<Image>();
|
||||||
|
artImage.sprite = CardArt.ForCard(
|
||||||
|
meta != null ? meta.NameId : printingId,
|
||||||
|
meta != null ? meta.Clan : "None");
|
||||||
|
artImage.color = Color.white;
|
||||||
|
artImage.preserveAspect = false;
|
||||||
|
|
||||||
var artText = UiKit.CreateText(
|
var artText = UiKit.CreateText(
|
||||||
art, "ArtText", meta != null ? "G" + meta.Grade : "?", 13,
|
art, "ArtText", meta != null ? "G" + meta.Grade : "?", 13,
|
||||||
|
|
@ -1344,21 +1368,6 @@ namespace Vanguard.UI
|
||||||
text.text = label;
|
text.text = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Color ClanColor(string clan) => clan switch
|
|
||||||
{
|
|
||||||
"RoyalPaladin" => new Color(0.22f, 0.42f, 0.78f),
|
|
||||||
"OracleThinkTank" => new Color(0.72f, 0.58f, 0.22f),
|
|
||||||
"Kagero" => new Color(0.66f, 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.28f, 0.20f, 0.36f),
|
|
||||||
"Granblue" => new Color(0.16f, 0.46f, 0.54f),
|
|
||||||
"Megacolony" => new Color(0.40f, 0.48f, 0.18f),
|
|
||||||
_ => new Color(0.25f, 0.30f, 0.40f),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static void SetFixedSize(Component component, float width, float height)
|
private static void SetFixedSize(Component component, float width, float height)
|
||||||
{
|
{
|
||||||
var element = component.gameObject.GetComponent<LayoutElement>()
|
var element = component.gameObject.GetComponent<LayoutElement>()
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ http://localhost:5080
|
||||||
интенты, ответы на ping, закрытие и переподключение.
|
интенты, ответы на ping, закрытие и переподключение.
|
||||||
- Боевой gray-box: доска, рука, часы, кнопки всех легальных интентов,
|
- Боевой gray-box: доска, рука, часы, кнопки всех легальных интентов,
|
||||||
сдача с подтверждением и экран результата.
|
сдача с подтверждением и экран результата.
|
||||||
|
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
||||||
|
триггер, сила/крит, REST и рубашка для скрытых карт.
|
||||||
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
||||||
|
|
||||||
## Что дальше
|
## Что дальше
|
||||||
|
|
|
||||||
|
|
@ -741,3 +741,9 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
- Клиент: кнопка «С ботом» в редакторе колоды; `MetaService.StartPracticeAsync`,
|
- Клиент: кнопка «С ботом» в редакторе колоды; `MetaService.StartPracticeAsync`,
|
||||||
переход в `Battle`. Серверный E2E-тест `PracticeBotE2ETests` проходит
|
переход в `Battle`. Серверный E2E-тест `PracticeBotE2ETests` проходит
|
||||||
полный матч против бота.
|
полный матч против бота.
|
||||||
|
- Визуал: `CardArt` генерирует процедурные градиентные арты по клану и
|
||||||
|
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
||||||
|
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
||||||
|
действий. Коллекция использует те же арты.
|
||||||
|
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
||||||
|
live-практика с ботом на стенде.
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,13 @@
|
||||||
- Тренировка с ботом: кнопка «С ботом» в редакторе колоды; сервер
|
- Тренировка с ботом: кнопка «С ботом» в редакторе колоды; сервер
|
||||||
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
||||||
без истории, MMR и наград.
|
без истории, MMR и наград.
|
||||||
|
- Боевой экран: процедурные карты с арт-градиентом по клану, рамкой,
|
||||||
|
именем, рангом, триггером, силой/критом и рубашкой; поля Vanguard +
|
||||||
|
5 Арьергардов у обоих, рука, damage/soul/drop, REST/скрытые карты.
|
||||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||||
- Не готово: арты/модели/анимации, локализация RU/EN, настройки, DOTween.
|
- Не готово: реальные арты/модели/анимации, локализация RU/EN, настройки,
|
||||||
|
DOTween.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,9 @@
|
||||||
|
|
||||||
## Ограничения и следующие шаги
|
## Ограничения и следующие шаги
|
||||||
|
|
||||||
- Пока нет 3D-ассетов: карты в gray-box показываются по `printingId`.
|
- Пока нет 3D-ассетов: карты показываются процедурными градиентными
|
||||||
Арт и 3D-модели будут подключаться по `cardId` через Addressables.
|
артами по клану и рубашкой (имя/ранг/триггер/сила). Арт и 3D-модели
|
||||||
|
будут подключаться по `cardId` через Addressables.
|
||||||
- Ближайший визуальный шаг — наполнить сцены моделями/артом и перевести
|
- Ближайший визуальный шаг — наполнить сцены моделями/артом и перевести
|
||||||
UI на prefab-экраны; логика и `SceneFlow` уже готовы.
|
UI на prefab-экраны; логика и `SceneFlow` уже готовы.
|
||||||
- Боевые анимации, камера и 3D-карты — после стабилизации сетевого
|
- Боевые анимации, камера и 3D-карты — после стабилизации сетевого
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue