Lay out full battle field with damage, deck, and guard zones
CI / test (push) Successful in 1m44s
Details
CI / test (push) Successful in 1m44s
Details
This commit is contained in:
parent
0e4737ac22
commit
cd2d40e4d4
|
|
@ -272,7 +272,8 @@
|
|||
отдельная 3D-сцена `Battle`, а не текущий Canvas
|
||||
(`docs/client-visual-direction.md`).
|
||||
|
||||
- [x] Поле: Vanguard + 5 кругов Арьергарда, рука, damage/soul/drop-счётчики.
|
||||
- [x] Поле: `R V R` / `R R R` у каждого игрока, damage/soul слева,
|
||||
колода/дроп справа, зеркально у противника, зона защиты между полями.
|
||||
- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер,
|
||||
сила/крит; REST/скрытая карта.
|
||||
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
||||
|
|
|
|||
|
|
@ -552,9 +552,9 @@ namespace Vanguard.UI
|
|||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You)
|
||||
?? (_view.Players.Count > _view.You ? _view.Players[_view.You] : null);
|
||||
|
||||
AddFieldBlock(opponent, false);
|
||||
AddCenterPanel(_view);
|
||||
AddFieldBlock(self, true);
|
||||
AddSideField(opponent, false);
|
||||
AddGuardZone(_view);
|
||||
AddSideField(self, true);
|
||||
AddHand(self);
|
||||
RebuildIntents(_view);
|
||||
UpdateClock();
|
||||
|
|
@ -563,7 +563,7 @@ namespace Vanguard.UI
|
|||
ShowResult(_view);
|
||||
}
|
||||
|
||||
private void AddFieldBlock(PlayerView player, bool self)
|
||||
private void AddSideField(PlayerView player, bool self)
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
|
|
@ -573,8 +573,8 @@ namespace Vanguard.UI
|
|||
self ? "SelfField" : "OpponentField",
|
||||
new Color(0.05f, 0.07f, 0.11f, 0.96f));
|
||||
var blockElement = block.gameObject.AddComponent<LayoutElement>();
|
||||
blockElement.preferredHeight = self ? 214f : 204f;
|
||||
UiKit.AddVerticalLayout(block, 7, 3);
|
||||
blockElement.preferredHeight = 334f;
|
||||
UiKit.AddVerticalLayout(block, 5, 3);
|
||||
|
||||
var header = UiKit.CreateText(
|
||||
block,
|
||||
|
|
@ -589,75 +589,160 @@ namespace Vanguard.UI
|
|||
self ? Color.white : new Color(0.94f, 0.82f, 0.70f),
|
||||
TextAnchor.MiddleLeft);
|
||||
header.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(header, 25f);
|
||||
UiKit.SetPreferredHeight(header, 24f);
|
||||
|
||||
var row = UiKit.CreatePanel(block, "Field", new Color(0f, 0f, 0f, 0f));
|
||||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||||
rowElement.flexibleHeight = 1f;
|
||||
rowElement.minHeight = 166f;
|
||||
UiKit.AddHorizontalLayout(row, 3, 7);
|
||||
var body = UiKit.CreatePanel(block, "Body", new Color(0f, 0f, 0f, 0f));
|
||||
var bodyElement = body.gameObject.AddComponent<LayoutElement>();
|
||||
bodyElement.flexibleHeight = 1f;
|
||||
bodyElement.minHeight = 294f;
|
||||
UiKit.AddHorizontalLayout(body, 0, 8);
|
||||
|
||||
AddCardBox(row, player.Vanguard, "V", compact: false, player.Index, -1);
|
||||
for (var i = 0; i < 5; i++)
|
||||
if (self)
|
||||
{
|
||||
var card = player.RearGuards != null && i < player.RearGuards.Length
|
||||
? player.RearGuards[i]
|
||||
: null;
|
||||
AddCardBox(row, card, "R" + (i + 1), compact: false, player.Index, i);
|
||||
AddDamageColumn(body, player);
|
||||
AddFieldGrid(body, player, self);
|
||||
AddPileColumn(body, player);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddPileColumn(body, player);
|
||||
AddFieldGrid(body, player, self);
|
||||
AddDamageColumn(body, player);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCenterPanel(MatchView view)
|
||||
private void AddFieldGrid(Transform parent, PlayerView player, bool self)
|
||||
{
|
||||
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 grid = UiKit.CreatePanel(parent, "FieldGrid", new Color(0f, 0f, 0f, 0f));
|
||||
var gridElement = grid.gameObject.AddComponent<LayoutElement>();
|
||||
gridElement.flexibleWidth = 1f;
|
||||
gridElement.flexibleHeight = 1f;
|
||||
|
||||
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),
|
||||
var layout = grid.gameObject.AddComponent<GridLayoutGroup>();
|
||||
layout.cellSize = new Vector2(140f, 140f);
|
||||
layout.spacing = new Vector2(6f, 6f);
|
||||
layout.padding = new RectOffset(4, 4, 4, 4);
|
||||
layout.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
||||
layout.constraintCount = 3;
|
||||
layout.childAlignment = TextAnchor.MiddleCenter;
|
||||
layout.startCorner = GridLayoutGroup.Corner.UpperLeft;
|
||||
layout.startAxis = GridLayoutGroup.Axis.Horizontal;
|
||||
|
||||
// Первый ряд: R V R. Второй ряд: R R R.
|
||||
// Для противника ряды зеркальны: его V-ряд ближе к центру.
|
||||
var slots = self
|
||||
? new[] { 3, -1, 4, 0, 1, 2 }
|
||||
: new[] { 0, 1, 2, 3, -1, 4 };
|
||||
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var card = CardAt(player.Index, slot);
|
||||
AddCardBox(
|
||||
grid,
|
||||
card,
|
||||
slot == -1 ? "V" : "R" + (slot + 1),
|
||||
compact: false,
|
||||
player.Index,
|
||||
slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDamageColumn(Transform parent, PlayerView player)
|
||||
{
|
||||
var column = UiKit.CreatePanel(parent, "DamageColumn", new Color(0f, 0f, 0f, 0f));
|
||||
var element = column.gameObject.AddComponent<LayoutElement>();
|
||||
element.preferredWidth = 88f;
|
||||
element.minWidth = 80f;
|
||||
element.flexibleWidth = 0f;
|
||||
UiKit.AddVerticalLayout(column, 0, 5);
|
||||
|
||||
AddPile(column, "УРОН",
|
||||
player.FaceDownDamage + player.FaceUpDamage.Count,
|
||||
new Color(0.48f, 0.12f, 0.12f), 118f);
|
||||
AddPile(column, "SOUL", player.Soul.Count,
|
||||
new Color(0.26f, 0.18f, 0.42f), 96f);
|
||||
}
|
||||
|
||||
private void AddPileColumn(Transform parent, PlayerView player)
|
||||
{
|
||||
var column = UiKit.CreatePanel(parent, "PileColumn", new Color(0f, 0f, 0f, 0f));
|
||||
var element = column.gameObject.AddComponent<LayoutElement>();
|
||||
element.preferredWidth = 88f;
|
||||
element.minWidth = 80f;
|
||||
element.flexibleWidth = 0f;
|
||||
UiKit.AddVerticalLayout(column, 0, 5);
|
||||
|
||||
AddPile(column, "КОЛОДА", player.DeckCount,
|
||||
new Color(0.12f, 0.22f, 0.42f), 118f);
|
||||
AddPile(column, "ДРОП", player.Drop.Count,
|
||||
new Color(0.20f, 0.20f, 0.26f), 96f);
|
||||
}
|
||||
|
||||
private void AddPile(Transform parent, string title, int count, Color color, float height)
|
||||
{
|
||||
var pile = UiKit.CreatePanel(parent, "Pile_" + title, color);
|
||||
var element = pile.gameObject.AddComponent<LayoutElement>();
|
||||
element.preferredHeight = height;
|
||||
element.flexibleWidth = 1f;
|
||||
|
||||
var outline = pile.gameObject.AddComponent<Outline>();
|
||||
outline.effectColor = new Color(1f, 1f, 1f, 0.15f);
|
||||
outline.effectDistance = new Vector2(1f, -1f);
|
||||
UiKit.AddVerticalLayout(pile, 5, 1);
|
||||
|
||||
var label = UiKit.CreateText(
|
||||
pile, "Title", title, 11, new Color(1f, 1f, 1f, 0.75f));
|
||||
UiKit.SetPreferredHeight(label, 18f);
|
||||
|
||||
var value = UiKit.CreateText(
|
||||
pile, "Count", count.ToString(), 26, Color.white);
|
||||
value.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(value, 38f);
|
||||
}
|
||||
|
||||
private void AddGuardZone(MatchView view)
|
||||
{
|
||||
var guard = UiKit.CreatePanel(
|
||||
_board, "GuardZone", new Color(0.08f, 0.11f, 0.18f, 0.98f));
|
||||
var element = guard.gameObject.AddComponent<LayoutElement>();
|
||||
element.preferredHeight = 72f;
|
||||
UiKit.AddHorizontalLayout(guard, 14, 12);
|
||||
|
||||
var label = UiKit.CreateText(
|
||||
guard,
|
||||
"Label",
|
||||
"ЗОНА ЗАЩИТЫ",
|
||||
18,
|
||||
new Color(0.82f, 0.88f, 1f),
|
||||
TextAnchor.MiddleLeft);
|
||||
main.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(main, 26f);
|
||||
label.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(label, 32f, 0f);
|
||||
|
||||
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 shield = UiKit.CreateText(
|
||||
guard,
|
||||
"Shield",
|
||||
"Щит: " + (view.Battle != null ? view.Battle.GuardShield.ToString() : "—"),
|
||||
18,
|
||||
new Color(0.55f, 0.86f, 1f),
|
||||
TextAnchor.MiddleCenter);
|
||||
UiKit.SetPreferredHeight(shield, 32f, 0f);
|
||||
|
||||
var detail = UiKit.CreateText(
|
||||
panel, "Detail", string.Join(" · ", details), 14,
|
||||
Muted, TextAnchor.MiddleLeft);
|
||||
detail.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(detail, 28f);
|
||||
var guardian = UiKit.CreateText(
|
||||
guard,
|
||||
"Guardian",
|
||||
"Проверок: " + (view.Battle != null ? view.Battle.ChecksRemaining : 0)
|
||||
+ " · защита между полями",
|
||||
14,
|
||||
Muted,
|
||||
TextAnchor.MiddleRight);
|
||||
UiKit.SetPreferredHeight(guardian, 32f);
|
||||
}
|
||||
|
||||
private void AddHand(PlayerView self)
|
||||
{
|
||||
var hand = UiKit.CreatePanel(_board, "HandBlock", new Color(0.05f, 0.07f, 0.11f, 0.96f));
|
||||
var handElement = hand.gameObject.AddComponent<LayoutElement>();
|
||||
handElement.preferredHeight = 188f;
|
||||
handElement.preferredHeight = 164f;
|
||||
UiKit.AddVerticalLayout(hand, 6, 3);
|
||||
|
||||
var header = UiKit.CreateText(
|
||||
|
|
@ -670,7 +755,7 @@ namespace Vanguard.UI
|
|||
var scroll = UiKit.CreateHorizontalScrollView(hand, "HandScroll", out var content, 6f);
|
||||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||||
scrollElement.flexibleHeight = 1f;
|
||||
scrollElement.minHeight = 130f;
|
||||
scrollElement.minHeight = 124f;
|
||||
|
||||
if (self?.Hand == null || self.Hand.Count == 0)
|
||||
{
|
||||
|
|
@ -714,8 +799,8 @@ namespace Vanguard.UI
|
|||
? 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.preferredWidth = compact ? 104f : 140f;
|
||||
element.preferredHeight = compact ? 124f : 140f;
|
||||
element.flexibleWidth = 0f;
|
||||
|
||||
var highlighted = _highlightKeys.Contains(
|
||||
|
|
@ -740,21 +825,20 @@ namespace Vanguard.UI
|
|||
|
||||
if (card == null)
|
||||
{
|
||||
var emptyArt = UiKit.CreatePanel(panel, "EmptyArt", new Color(0.04f, 0.06f, 0.09f, 1f));
|
||||
UiKit.SetPreferredHeight(emptyArt, compact ? 48f : 64f);
|
||||
var panelImage = panel.GetComponent<Image>();
|
||||
panelImage.sprite = CardArt.Zone;
|
||||
panelImage.color = Color.white;
|
||||
panelImage.preserveAspect = true;
|
||||
|
||||
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);
|
||||
panel, "EmptyLabel", "Пусто", 12,
|
||||
new Color(1f, 1f, 1f, 0.40f), TextAnchor.MiddleCenter);
|
||||
UiKit.SetPreferredHeight(empty, compact ? 100f : 116f);
|
||||
return;
|
||||
}
|
||||
|
||||
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
||||
UiKit.SetPreferredHeight(art, compact ? 48f : 64f);
|
||||
UiKit.SetPreferredHeight(art, compact ? 46f : 58f);
|
||||
var artImage = art.GetComponent<Image>();
|
||||
artImage.sprite = card.FaceUp
|
||||
? CardArt.ForCard(
|
||||
|
|
@ -815,7 +899,7 @@ namespace Vanguard.UI
|
|||
TextAnchor.UpperLeft);
|
||||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(name, compact ? 26f : 28f);
|
||||
UiKit.SetPreferredHeight(name, compact ? 22f : 24f);
|
||||
|
||||
var stats = UiKit.CreateText(
|
||||
panel, "Stats",
|
||||
|
|
@ -823,12 +907,12 @@ namespace Vanguard.UI
|
|||
? "P " + card.Power + " · C " + card.Critical
|
||||
+ (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty)
|
||||
: "СКРЫТА",
|
||||
11,
|
||||
10,
|
||||
card.FaceUp && card.Power != card.BasePower
|
||||
? new Color(1f, 0.82f, 0.42f)
|
||||
: new Color(0.72f, 0.78f, 0.92f),
|
||||
TextAnchor.MiddleLeft);
|
||||
UiKit.SetPreferredHeight(stats, 18f);
|
||||
UiKit.SetPreferredHeight(stats, 16f);
|
||||
}
|
||||
|
||||
private static string TriggerShort(string trigger) => trigger switch
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace Vanguard.UI
|
|||
private static readonly Dictionary<string, Sprite> Cache =
|
||||
new Dictionary<string, Sprite>();
|
||||
private static Sprite _back;
|
||||
private static Sprite _zone;
|
||||
|
||||
public static Color ClanColor(string clan) => clan switch
|
||||
{
|
||||
|
|
@ -89,6 +90,63 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
public static Sprite Zone
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_zone != null)
|
||||
return _zone;
|
||||
|
||||
_zone = ZoneTexture();
|
||||
return _zone;
|
||||
}
|
||||
}
|
||||
|
||||
private static Sprite ZoneTexture()
|
||||
{
|
||||
const int size = 64;
|
||||
var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Bilinear,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
};
|
||||
|
||||
var pixels = new Color[size * size];
|
||||
var center = (size - 1) * 0.5f;
|
||||
for (var y = 0; y < size; y++)
|
||||
{
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var distance = Mathf.Sqrt((x - center) * (x - center) + (y - center) * (y - center));
|
||||
if (distance > 30f)
|
||||
{
|
||||
pixels[y * size + x] = Color.clear;
|
||||
}
|
||||
else if (distance > 26f)
|
||||
{
|
||||
pixels[y * size + x] = new Color(0.38f, 0.52f, 0.78f, 0.9f);
|
||||
}
|
||||
else
|
||||
{
|
||||
var glow = Mathf.Clamp01(1f - distance / 26f) * 0.08f;
|
||||
pixels[y * size + x] = new Color(
|
||||
0.04f + glow,
|
||||
0.07f + glow,
|
||||
0.12f + glow,
|
||||
0.95f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
texture.Apply();
|
||||
return Sprite.Create(
|
||||
texture,
|
||||
new Rect(0f, 0f, size, size),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
100f);
|
||||
}
|
||||
|
||||
private static Sprite Gradient(Color bottom, Color top, Color accent, int variant)
|
||||
{
|
||||
const int width = 32;
|
||||
|
|
|
|||
|
|
@ -747,5 +747,7 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
действий. Коллекция использует те же арты.
|
||||
- Боевой UX: клик по карте/зоне, подсветка целей, отдельная группировка
|
||||
действий, журнал последних событий и подсказки выбора.
|
||||
- Поле: `R V R` / `R R R` у каждого игрока, зона урона слева, колода и
|
||||
дроп справа, зеркально у противника, зона защиты между полями.
|
||||
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
||||
live-практика с ботом на стенде.
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@
|
|||
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
||||
без истории, MMR и наград.
|
||||
- Боевой экран: процедурные карты с арт-градиентом по клану, рамкой,
|
||||
именем, рангом, триггером, силой/критом и рубашкой; поля Vanguard +
|
||||
5 Арьергардов у обоих, рука, damage/soul/drop, REST/скрытые карты.
|
||||
именем, рангом, триггером, силой/критом и рубашкой.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
||||
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
||||
полями — зона защиты со щитом и проверками.
|
||||
- Взаимодействие в бою: клик по картам/зонам, подсветка целей,
|
||||
группировка действий (карты → цели, глобальные кнопки отдельно),
|
||||
журнал последних событий.
|
||||
|
|
|
|||
Loading…
Reference in New Issue