Improve battle cards, mulligan and damage zone
CI / test (push) Failing after 4m25s
Details
CI / test (push) Failing after 4m25s
Details
This commit is contained in:
parent
0e2907803a
commit
f5b7f3e2d8
|
|
@ -71,6 +71,7 @@ namespace Vanguard.UI
|
||||||
private readonly HashSet<string> _highlightKeys = new HashSet<string>(StringComparer.Ordinal);
|
private readonly HashSet<string> _highlightKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
private int _pendingBoosterSlot = -1;
|
private int _pendingBoosterSlot = -1;
|
||||||
private int? _triggerPowerSlot;
|
private int? _triggerPowerSlot;
|
||||||
|
private readonly HashSet<int> _mulliganSelected = new HashSet<int>();
|
||||||
private readonly Dictionary<string, RectTransform> _cardPanels =
|
private readonly Dictionary<string, RectTransform> _cardPanels =
|
||||||
new Dictionary<string, RectTransform>(StringComparer.Ordinal);
|
new Dictionary<string, RectTransform>(StringComparer.Ordinal);
|
||||||
private readonly List<CardAnimation> _cardAnimations = new List<CardAnimation>();
|
private readonly List<CardAnimation> _cardAnimations = new List<CardAnimation>();
|
||||||
|
|
@ -205,6 +206,7 @@ namespace Vanguard.UI
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_highlightKeys.Clear();
|
_highlightKeys.Clear();
|
||||||
|
_mulliganSelected.Clear();
|
||||||
_pendingBoosterSlot = -1;
|
_pendingBoosterSlot = -1;
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
_showAllIntents = false;
|
_showAllIntents = false;
|
||||||
|
|
@ -1181,16 +1183,123 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
var column = UiKit.CreatePanel(parent, "DamageColumn", new Color(0f, 0f, 0f, 0f));
|
var column = UiKit.CreatePanel(parent, "DamageColumn", new Color(0f, 0f, 0f, 0f));
|
||||||
var element = column.gameObject.AddComponent<LayoutElement>();
|
var element = column.gameObject.AddComponent<LayoutElement>();
|
||||||
element.preferredWidth = 88f;
|
element.preferredWidth = 174f;
|
||||||
element.minWidth = 80f;
|
element.minWidth = 158f;
|
||||||
element.flexibleWidth = 0f;
|
element.flexibleWidth = 0f;
|
||||||
UiKit.AddVerticalLayout(column, 0, 5);
|
UiKit.AddVerticalLayout(column, 0, 4);
|
||||||
|
|
||||||
AddPile(column, "УРОН",
|
AddDamageZone(column, player);
|
||||||
player.FaceDownDamage + player.FaceUpDamage.Count,
|
|
||||||
new Color(0.48f, 0.12f, 0.12f), 118f);
|
|
||||||
AddPile(column, "SOUL", player.Soul.Count,
|
AddPile(column, "SOUL", player.Soul.Count,
|
||||||
new Color(0.26f, 0.18f, 0.42f), 96f);
|
new Color(0.26f, 0.18f, 0.42f), 78f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDamageZone(Transform parent, PlayerView player)
|
||||||
|
{
|
||||||
|
var count = player.FaceDownDamage + player.FaceUpDamage.Count;
|
||||||
|
var zone = UiKit.CreatePanel(
|
||||||
|
parent, "DamageZone", new Color(0.16f, 0.05f, 0.06f, 0.9f));
|
||||||
|
var element = zone.gameObject.AddComponent<LayoutElement>();
|
||||||
|
element.preferredHeight = 206f;
|
||||||
|
element.flexibleWidth = 1f;
|
||||||
|
UiKit.AddVerticalLayout(zone, 4, 2);
|
||||||
|
|
||||||
|
var title = UiKit.CreateText(
|
||||||
|
zone, "Title", "УРОН · " + count, 11,
|
||||||
|
new Color(1f, 0.78f, 0.72f), TextAnchor.MiddleCenter);
|
||||||
|
title.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(title, 18f);
|
||||||
|
|
||||||
|
var grid = UiKit.CreatePanel(zone, "DamageCards", new Color(0f, 0f, 0f, 0f));
|
||||||
|
var gridElement = grid.gameObject.AddComponent<LayoutElement>();
|
||||||
|
gridElement.flexibleHeight = 1f;
|
||||||
|
gridElement.preferredHeight = 176f;
|
||||||
|
|
||||||
|
var damageChoice = IsDamageChoice();
|
||||||
|
var displayIndex = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < player.FaceDownDamage; i++, displayIndex++)
|
||||||
|
{
|
||||||
|
AddDamageCard(grid, displayIndex, null, false, damageChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var card in player.FaceUpDamage)
|
||||||
|
{
|
||||||
|
AddDamageCard(grid, displayIndex, card, true, damageChoice);
|
||||||
|
displayIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDamageCard(
|
||||||
|
RectTransform parent,
|
||||||
|
int displayIndex,
|
||||||
|
CardView card,
|
||||||
|
bool faceUp,
|
||||||
|
bool damageChoice)
|
||||||
|
{
|
||||||
|
var panel = UiKit.CreatePanel(parent, "Damage_" + displayIndex, Color.white);
|
||||||
|
var rect = panel;
|
||||||
|
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||||
|
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||||
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||||
|
rect.sizeDelta = new Vector2(58f, 78f);
|
||||||
|
rect.anchoredPosition = new Vector2(
|
||||||
|
displayIndex % 2 == 0 ? -42f : 42f,
|
||||||
|
58f - (displayIndex / 2) * 58f);
|
||||||
|
rect.localRotation = Quaternion.Euler(0f, 0f, -90f);
|
||||||
|
|
||||||
|
var image = panel.GetComponent<Image>();
|
||||||
|
image.raycastTarget = true;
|
||||||
|
var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found)
|
||||||
|
? found
|
||||||
|
: null;
|
||||||
|
if (faceUp)
|
||||||
|
{
|
||||||
|
CardImageService.Apply(
|
||||||
|
image,
|
||||||
|
meta != null ? meta.ImageKey : null,
|
||||||
|
CardArt.ForCard(
|
||||||
|
meta != null ? meta.NameId : card?.PrintingId ?? "damage",
|
||||||
|
meta != null ? meta.Clan : "None"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
image.sprite = CardArt.Back;
|
||||||
|
image.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outline = panel.gameObject.AddComponent<Outline>();
|
||||||
|
outline.effectColor = damageChoice
|
||||||
|
? new Color(0.98f, 0.82f, 0.42f)
|
||||||
|
: new Color(0.42f, 0.18f, 0.18f);
|
||||||
|
outline.effectDistance = damageChoice
|
||||||
|
? new Vector2(3f, -3f)
|
||||||
|
: new Vector2(1f, -1f);
|
||||||
|
|
||||||
|
var button = panel.gameObject.AddComponent<Button>();
|
||||||
|
button.targetGraphic = image;
|
||||||
|
var capturedIndex = displayIndex;
|
||||||
|
button.onClick.AddListener(() => OnDamageClicked(capturedIndex));
|
||||||
|
button.interactable = damageChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDamageChoice() =>
|
||||||
|
_view?.Choice != null
|
||||||
|
&& _view.Choice.Prompt != null
|
||||||
|
&& _view.Choice.Prompt.IndexOf("урон", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
|
||||||
|
private void OnDamageClicked(int displayIndex)
|
||||||
|
{
|
||||||
|
if (_view?.Choice?.Options == null || !IsDamageChoice())
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var i = 0; i < _view.Choice.Options.Count; i++)
|
||||||
|
{
|
||||||
|
if (_view.Choice.Options[i].Slot == displayIndex)
|
||||||
|
{
|
||||||
|
OnIntentClicked(new ChooseIntent { Index = i });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPileColumn(Transform parent, PlayerView player)
|
private void AddPileColumn(Transform parent, PlayerView player)
|
||||||
|
|
@ -1335,6 +1444,14 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
var highlighted = _highlightKeys.Contains(
|
var highlighted = _highlightKeys.Contains(
|
||||||
TargetKey(playerIndex, slotIndex, card != null ? card.PrintingId : null));
|
TargetKey(playerIndex, slotIndex, card != null ? card.PrintingId : null));
|
||||||
|
if (!highlighted
|
||||||
|
&& card != null
|
||||||
|
&& slotIndex == -2
|
||||||
|
&& playerIndex == _view.You
|
||||||
|
&& IsMulliganSelecting())
|
||||||
|
{
|
||||||
|
highlighted = _mulliganSelected.Contains(HandIndexOf(card.InstanceId));
|
||||||
|
}
|
||||||
var button = panel.gameObject.AddComponent<Button>();
|
var button = panel.gameObject.AddComponent<Button>();
|
||||||
button.targetGraphic = panel.GetComponent<Image>();
|
button.targetGraphic = panel.GetComponent<Image>();
|
||||||
button.onClick.AddListener(() => OnBoardTargetClicked(new BoardTarget(
|
button.onClick.AddListener(() => OnBoardTargetClicked(new BoardTarget(
|
||||||
|
|
@ -1364,13 +1481,6 @@ namespace Vanguard.UI
|
||||||
: new Vector2(0f, 250f);
|
: new Vector2(0f, 250f);
|
||||||
AnimateCardIn(panel, offset);
|
AnimateCardIn(panel, offset);
|
||||||
}
|
}
|
||||||
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 panelImage = panel.GetComponent<Image>();
|
var panelImage = panel.GetComponent<Image>();
|
||||||
|
|
@ -1381,13 +1491,16 @@ namespace Vanguard.UI
|
||||||
var empty = UiKit.CreateText(
|
var empty = UiKit.CreateText(
|
||||||
panel, "EmptyLabel", "Пусто", 12,
|
panel, "EmptyLabel", "Пусто", 12,
|
||||||
new Color(1f, 1f, 1f, 0.40f), TextAnchor.MiddleCenter);
|
new Color(1f, 1f, 1f, 0.40f), TextAnchor.MiddleCenter);
|
||||||
UiKit.SetPreferredHeight(empty, compact ? 100f : 116f);
|
UiKit.Stretch(empty.rectTransform);
|
||||||
|
empty.rectTransform.offsetMin = new Vector2(4f, 4f);
|
||||||
|
empty.rectTransform.offsetMax = new Vector2(-4f, -4f);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
||||||
UiKit.SetPreferredHeight(art, compact ? 46f : 58f);
|
UiKit.Stretch(art);
|
||||||
var artImage = art.GetComponent<Image>();
|
var artImage = art.GetComponent<Image>();
|
||||||
|
artImage.raycastTarget = false;
|
||||||
if (card.FaceUp)
|
if (card.FaceUp)
|
||||||
{
|
{
|
||||||
CardImageService.Apply(
|
CardImageService.Apply(
|
||||||
|
|
@ -1404,60 +1517,65 @@ namespace Vanguard.UI
|
||||||
artImage.preserveAspect = false;
|
artImage.preserveAspect = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.FaceUp)
|
var slotText = UiKit.CreateText(
|
||||||
{
|
panel, "Slot", slot, 9,
|
||||||
var artName = UiKit.CreateText(
|
new Color(0.92f, 0.95f, 1f), TextAnchor.UpperLeft);
|
||||||
art, "ArtName", meta != null ? Loc.CardName(meta) : card.PrintingId, 10,
|
var slotRect = slotText.rectTransform;
|
||||||
new Color(1f, 1f, 1f, 0.94f), TextAnchor.MiddleCenter);
|
slotRect.anchorMin = new Vector2(0f, 1f);
|
||||||
UiKit.MakeLiteral(artName);
|
slotRect.anchorMax = new Vector2(0f, 1f);
|
||||||
UiKit.Stretch(artName.rectTransform);
|
slotRect.pivot = new Vector2(0f, 1f);
|
||||||
artName.rectTransform.offsetMin = new Vector2(3f, 3f);
|
slotRect.anchoredPosition = new Vector2(4f, -3f);
|
||||||
artName.rectTransform.offsetMax = new Vector2(-3f, -2f);
|
slotRect.sizeDelta = new Vector2(42f, 14f);
|
||||||
artName.horizontalOverflow = HorizontalWrapMode.Wrap;
|
slotText.raycastTarget = false;
|
||||||
|
|
||||||
if (meta != null)
|
var shade = UiKit.CreatePanel(panel, "Shade", new Color(0f, 0f, 0f, 0.72f));
|
||||||
{
|
var shadeRect = shade;
|
||||||
var grade = UiKit.CreateText(
|
shadeRect.anchorMin = new Vector2(0f, 0f);
|
||||||
art, "Grade", "G" + meta.Grade, 10,
|
shadeRect.anchorMax = new Vector2(1f, 0f);
|
||||||
Color.white, TextAnchor.MiddleCenter);
|
shadeRect.pivot = new Vector2(0.5f, 0f);
|
||||||
var gradeRect = grade.rectTransform;
|
shadeRect.anchoredPosition = Vector2.zero;
|
||||||
gradeRect.anchorMin = new Vector2(1f, 1f);
|
shadeRect.sizeDelta = new Vector2(0f, compact ? 40f : 44f);
|
||||||
gradeRect.anchorMax = new Vector2(1f, 1f);
|
shade.GetComponent<Image>().raycastTarget = false;
|
||||||
gradeRect.pivot = new Vector2(1f, 1f);
|
|
||||||
gradeRect.anchoredPosition = new Vector2(-3f, -3f);
|
if (card.FaceUp && meta != null)
|
||||||
gradeRect.sizeDelta = new Vector2(25f, 16f);
|
{
|
||||||
}
|
var grade = UiKit.CreateText(
|
||||||
|
panel, "Grade", "G" + meta.Grade, 9,
|
||||||
|
Color.white, TextAnchor.UpperRight);
|
||||||
|
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(-4f, -3f);
|
||||||
|
gradeRect.sizeDelta = new Vector2(28f, 14f);
|
||||||
|
grade.raycastTarget = false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (!card.FaceUp)
|
||||||
{
|
{
|
||||||
var hidden = UiKit.CreateText(
|
var hidden = UiKit.CreateText(
|
||||||
art, "Hidden", "?", 26, new Color(1f, 1f, 1f, 0.8f));
|
panel, "Hidden", "?", 26, new Color(1f, 1f, 1f, 0.8f));
|
||||||
UiKit.Stretch(hidden.rectTransform);
|
UiKit.Stretch(hidden.rectTransform);
|
||||||
}
|
hidden.raycastTarget = false;
|
||||||
|
|
||||||
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(
|
var name = UiKit.CreateText(
|
||||||
panel, "Name",
|
panel, "Name",
|
||||||
meta != null ? Loc.CardName(meta) : card.PrintingId,
|
meta != null ? Loc.CardName(meta) : card.PrintingId,
|
||||||
10,
|
compact ? 9 : 10,
|
||||||
card.FaceUp ? Color.white : Muted,
|
card.FaceUp ? Color.white : Muted,
|
||||||
TextAnchor.UpperLeft);
|
TextAnchor.LowerLeft);
|
||||||
UiKit.MakeLiteral(name);
|
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, compact ? 18f : 20f);
|
||||||
|
nameRect.sizeDelta = new Vector2(-8f, 20f);
|
||||||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
name.verticalOverflow = VerticalWrapMode.Truncate;
|
||||||
name.fontStyle = FontStyle.Bold;
|
name.fontStyle = FontStyle.Bold;
|
||||||
UiKit.SetPreferredHeight(name, compact ? 22f : 24f);
|
name.raycastTarget = false;
|
||||||
|
|
||||||
var stats = UiKit.CreateText(
|
var stats = UiKit.CreateText(
|
||||||
panel, "Stats",
|
panel, "Stats",
|
||||||
|
|
@ -1465,12 +1583,21 @@ namespace Vanguard.UI
|
||||||
? "P " + card.Power + " · C " + card.Critical
|
? "P " + card.Power + " · C " + card.Critical
|
||||||
+ (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty)
|
+ (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty)
|
||||||
: "СКРЫТА",
|
: "СКРЫТА",
|
||||||
10,
|
compact ? 8 : 9,
|
||||||
card.FaceUp && card.Power != card.BasePower
|
card.FaceUp && card.Power != card.BasePower
|
||||||
? new Color(1f, 0.82f, 0.42f)
|
? new Color(1f, 0.82f, 0.42f)
|
||||||
: new Color(0.72f, 0.78f, 0.92f),
|
: new Color(0.72f, 0.78f, 0.92f),
|
||||||
TextAnchor.MiddleLeft);
|
TextAnchor.LowerLeft);
|
||||||
UiKit.SetPreferredHeight(stats, 16f);
|
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, 2f);
|
||||||
|
statsRect.sizeDelta = new Vector2(-8f, 14f);
|
||||||
|
stats.raycastTarget = false;
|
||||||
|
|
||||||
|
if (card.Rested)
|
||||||
|
panel.localRotation = Quaternion.Euler(0f, 0f, -90f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string TriggerShort(string trigger) => trigger switch
|
private static string TriggerShort(string trigger) => trigger switch
|
||||||
|
|
@ -1485,11 +1612,65 @@ namespace Vanguard.UI
|
||||||
private string CardName(string printingId) =>
|
private string CardName(string printingId) =>
|
||||||
_catalog.TryGetValue(printingId, out var meta) ? Loc.CardName(meta) : printingId;
|
_catalog.TryGetValue(printingId, out var meta) ? Loc.CardName(meta) : printingId;
|
||||||
|
|
||||||
|
private bool IsMulliganSelecting() =>
|
||||||
|
_view != null
|
||||||
|
&& !_view.IsFinished
|
||||||
|
&& _view.Phase == GamePhase.Mulligan
|
||||||
|
&& _view.ActivePlayer == _view.You;
|
||||||
|
|
||||||
|
private int HandIndexOf(int instanceId)
|
||||||
|
{
|
||||||
|
var hand = _view?.Players.FirstOrDefault(player => player.Index == _view.You)?.Hand;
|
||||||
|
if (hand == null)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < hand.Count; i++)
|
||||||
|
{
|
||||||
|
if (hand[i].InstanceId == instanceId)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleMulligan(int index)
|
||||||
|
{
|
||||||
|
if (!_mulliganSelected.Add(index))
|
||||||
|
_mulliganSelected.Remove(index);
|
||||||
|
|
||||||
|
SetStatus(
|
||||||
|
_mulliganSelected.Count == 0
|
||||||
|
? "Кликните карты, которые вернутся в колоду."
|
||||||
|
: "К обмену выбрано карт: " + _mulliganSelected.Count + ".",
|
||||||
|
new Color(0.98f, 0.82f, 0.42f));
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendMulligan()
|
||||||
|
{
|
||||||
|
if (!IsMulliganSelecting() || _mulliganSelected.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var indexes = _mulliganSelected.OrderBy(index => index).ToList();
|
||||||
|
_mulliganSelected.Clear();
|
||||||
|
OnIntentClicked(new MulliganIntent { HandIndexes = indexes });
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBoardTargetClicked(BoardTarget target)
|
private void OnBoardTargetClicked(BoardTarget target)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending)
|
if (_view == null || _sending)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (IsMulliganSelecting() && target.IsHand && target.Player == _view.You)
|
||||||
|
{
|
||||||
|
var handIndex = HandIndexOf(target.InstanceId);
|
||||||
|
if (handIndex >= 0)
|
||||||
|
{
|
||||||
|
ToggleMulligan(handIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
||||||
if (_pendingIntents.Any(intent => intent is ResolveTriggerIntent)
|
if (_pendingIntents.Any(intent => intent is ResolveTriggerIntent)
|
||||||
|
|
@ -1849,6 +2030,30 @@ namespace Vanguard.UI
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsMulliganSelecting())
|
||||||
|
{
|
||||||
|
var mulliganHint = UiKit.CreateText(
|
||||||
|
_intents,
|
||||||
|
"MulliganHint",
|
||||||
|
"Кликните карты, которые вернутся в колоду.",
|
||||||
|
15,
|
||||||
|
new Color(0.72f, 0.80f, 0.94f),
|
||||||
|
TextAnchor.UpperLeft);
|
||||||
|
mulliganHint.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
UiKit.SetPreferredHeight(mulliganHint, 34f);
|
||||||
|
|
||||||
|
if (_mulliganSelected.Count > 0)
|
||||||
|
{
|
||||||
|
var exchange = UiKit.CreateButton(
|
||||||
|
_intents,
|
||||||
|
"MulliganExchange",
|
||||||
|
"Маллиган: " + _mulliganSelected.Count + " карт",
|
||||||
|
SendMulligan,
|
||||||
|
16);
|
||||||
|
UiKit.SetPreferredHeight(exchange, 44f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (board.Count > 0)
|
if (board.Count > 0)
|
||||||
{
|
{
|
||||||
var hint = UiKit.CreateText(
|
var hint = UiKit.CreateText(
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,12 @@ namespace Vanguard.Tests
|
||||||
Resources.Load<Texture2D>("CardArt/td01_005"),
|
Resources.Load<Texture2D>("CardArt/td01_005"),
|
||||||
"TD01 card art is not imported");
|
"TD01 card art is not imported");
|
||||||
|
|
||||||
|
var artGo = new GameObject("ArtProbe", typeof(RectTransform), typeof(Image));
|
||||||
|
var artImage = artGo.GetComponent<Image>();
|
||||||
|
CardImageService.Apply(artImage, "td01_005", null);
|
||||||
|
Assert.IsNotNull(artImage.sprite, "CardImageService did not load real card art");
|
||||||
|
Object.DestroyImmediate(artGo);
|
||||||
|
|
||||||
// После смены сцены старые views не должны реагировать на PlayerState.
|
// После смены сцены старые views не должны реагировать на PlayerState.
|
||||||
lobby.Dispose();
|
lobby.Dispose();
|
||||||
store.Dispose();
|
store.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -573,7 +573,7 @@ public sealed class GameEngine
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
p.Soul.Remove(card);
|
p.Soul.Remove(card);
|
||||||
card.Rested = true;
|
card.Rested = false;
|
||||||
p.RearGuards[slot] = card;
|
p.RearGuards[slot] = card;
|
||||||
|
|
||||||
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
||||||
|
|
@ -1131,7 +1131,7 @@ public sealed class GameEngine
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.Rested = true;
|
card.Rested = false;
|
||||||
p.RearGuards[slot] = card;
|
p.RearGuards[slot] = card;
|
||||||
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
||||||
|
|
||||||
|
|
@ -1794,7 +1794,7 @@ public sealed class GameEngine
|
||||||
|
|
||||||
var card = player.Hand[index];
|
var card = player.Hand[index];
|
||||||
player.Hand.RemoveAt(index);
|
player.Hand.RemoveAt(index);
|
||||||
card.Rested = true;
|
card.Rested = false;
|
||||||
player.RearGuards[slot] = card;
|
player.RearGuards[slot] = card;
|
||||||
|
|
||||||
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
|
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
|
||||||
|
|
@ -2326,10 +2326,17 @@ public sealed class GameEngine
|
||||||
Draw(player, 1);
|
Draw(player, 1);
|
||||||
break;
|
break;
|
||||||
case TriggerType.Heal:
|
case TriggerType.Heal:
|
||||||
Heal(player);
|
if (TryBeginHeal(player, pending, command))
|
||||||
|
return CommandResult.Ok();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CompleteTrigger(pending, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandResult CompleteTrigger(PendingTrigger pending, ResolveTriggerCommand command)
|
||||||
|
{
|
||||||
|
var player = pending.Player;
|
||||||
State.PendingTrigger = null;
|
State.PendingTrigger = null;
|
||||||
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
|
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
|
||||||
|
|
||||||
|
|
@ -2353,6 +2360,65 @@ public sealed class GameEngine
|
||||||
return CommandResult.Ok();
|
return CommandResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryBeginHeal(int player, PendingTrigger pending, ResolveTriggerCommand command)
|
||||||
|
{
|
||||||
|
var p = State.Players[player];
|
||||||
|
var opponent = State.Players[1 - player];
|
||||||
|
if (p.Damage.Count <= opponent.Damage.Count || p.Damage.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
AskDamageChoice(
|
||||||
|
player,
|
||||||
|
"Выберите карту урона для лечения",
|
||||||
|
actualIndex =>
|
||||||
|
{
|
||||||
|
var card = p.Damage[actualIndex];
|
||||||
|
p.Damage.RemoveAt(actualIndex);
|
||||||
|
p.Drop.Add(card);
|
||||||
|
_events.Add(new HealedEvent(player));
|
||||||
|
});
|
||||||
|
|
||||||
|
var previousContinuation = State.Continuation;
|
||||||
|
State.Continuation = () =>
|
||||||
|
{
|
||||||
|
State.Continuation = previousContinuation;
|
||||||
|
CompleteTrigger(pending, command);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AskDamageChoice(int player, string prompt, Action<int> onChosen)
|
||||||
|
{
|
||||||
|
var damage = State.Players[player].Damage;
|
||||||
|
|
||||||
|
// Закрытый урон показываем первым, затем открытый: так клиент
|
||||||
|
// отрисовывает зону урона и может сопоставить клик с вариантом.
|
||||||
|
var order = damage
|
||||||
|
.Select((card, index) => (card, index))
|
||||||
|
.OrderBy(item => item.card.FaceUp)
|
||||||
|
.ThenBy(item => item.index)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var options = order
|
||||||
|
.Select((item, displayIndex) => new ChoiceOption(
|
||||||
|
displayIndex,
|
||||||
|
item.card.FaceUp
|
||||||
|
? State.Catalog.CardOf(item.card.PrintingId).NameEn
|
||||||
|
: "Закрытая карта"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
State.PendingChoice = new PendingChoice
|
||||||
|
{
|
||||||
|
Player = player,
|
||||||
|
Prompt = prompt,
|
||||||
|
Options = options,
|
||||||
|
Kind = ChoiceTargetKind.Damage,
|
||||||
|
Resolve = displayIndex => onChosen(order[displayIndex].index),
|
||||||
|
};
|
||||||
|
State.Step = Step.Choice;
|
||||||
|
State.ActivePlayer = player;
|
||||||
|
}
|
||||||
|
|
||||||
private void FinishBattle(BattleState battle)
|
private void FinishBattle(BattleState battle)
|
||||||
{
|
{
|
||||||
if (State.IsFinished)
|
if (State.IsFinished)
|
||||||
|
|
@ -2609,20 +2675,6 @@ public sealed class GameEngine
|
||||||
ClearBattle(battle);
|
ClearBattle(battle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Heal(int player)
|
|
||||||
{
|
|
||||||
var p = State.Players[player];
|
|
||||||
var opponent = State.Players[1 - player];
|
|
||||||
|
|
||||||
if (p.Damage.Count <= opponent.Damage.Count || p.Damage.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var card = p.Damage[^1];
|
|
||||||
p.Damage.RemoveAt(p.Damage.Count - 1);
|
|
||||||
p.Drop.Add(card);
|
|
||||||
_events.Add(new HealedEvent(player));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- служебное ----------
|
// ---------- служебное ----------
|
||||||
|
|
||||||
private CardInstance? TakeTop(int player)
|
private CardInstance? TakeTop(int player)
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,23 @@ public sealed record UnitTarget(int Player, int Slot);
|
||||||
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
|
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
|
||||||
/// Пока выбор не сделан, движок не продолжает разрешение способности.
|
/// Пока выбор не сделан, движок не продолжает разрешение способности.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public enum ChoiceTargetKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Unit,
|
||||||
|
Hand,
|
||||||
|
Damage,
|
||||||
|
Deck,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PendingChoice
|
public sealed class PendingChoice
|
||||||
{
|
{
|
||||||
public required int Player { get; init; }
|
public required int Player { get; init; }
|
||||||
public required string Prompt { get; init; }
|
public required string Prompt { get; init; }
|
||||||
public required IReadOnlyList<ChoiceOption> Options { get; init; }
|
public required IReadOnlyList<ChoiceOption> Options { get; init; }
|
||||||
public required Action<int> Resolve { get; init; }
|
public required Action<int> Resolve { get; init; }
|
||||||
|
public ChoiceTargetKind Kind { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TriggerContext
|
public enum TriggerContext
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public class GameEngineTests
|
||||||
private const string Nike = "BT01/056EN"; // Critical
|
private const string Nike = "BT01/056EN"; // Critical
|
||||||
private const string Flogal = "BT01/046EN"; // Stand
|
private const string Flogal = "BT01/046EN"; // Stand
|
||||||
private const string Govannon = "BT01/045EN"; // Draw
|
private const string Govannon = "BT01/045EN"; // Draw
|
||||||
|
private const string Elaine = "BT01/047EN"; // Heal
|
||||||
private const string Iseult = "BT01/011EN"; // Sentinel, Royal Paladin
|
private const string Iseult = "BT01/011EN"; // Sentinel, Royal Paladin
|
||||||
private const string Nehalem = "BT01/022EN"; // Kagero
|
private const string Nehalem = "BT01/022EN"; // Kagero
|
||||||
private const string Overlord = "BT01/004EN"; // Kagero, 11000
|
private const string Overlord = "BT01/004EN"; // Kagero, 11000
|
||||||
|
|
@ -99,13 +100,29 @@ public class GameEngineTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Called_RearGuard_Enters_Rested_And_Slot_Is_Busy()
|
public void Mulligan_Replaces_Selected_Hand_Cards_And_Keeps_Five()
|
||||||
|
{
|
||||||
|
var engine = NewEngine();
|
||||||
|
engine.Start();
|
||||||
|
|
||||||
|
Assert.Equal(Phase.Mulligan, engine.State.Phase);
|
||||||
|
Assert.Equal(5, engine.State.Players[0].Hand.Count);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new MulliganCommand(new[] { 0, 2 })).Accepted);
|
||||||
|
|
||||||
|
Assert.Equal(5, engine.State.Players[0].Hand.Count);
|
||||||
|
Assert.Equal(44, engine.State.Players[0].Deck.Count);
|
||||||
|
Assert.Contains(engine.Events, e => e is MulliganEvent { Player: 0, Count: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Called_RearGuard_Enters_Standing_And_Slot_Is_Busy()
|
||||||
{
|
{
|
||||||
var engine = InMainPhase();
|
var engine = InMainPhase();
|
||||||
Give(engine, 0, Marron);
|
Give(engine, 0, Marron);
|
||||||
|
|
||||||
Assert.True(engine.Execute(new CallCommand(Marron, 0)).Accepted);
|
Assert.True(engine.Execute(new CallCommand(Marron, 0)).Accepted);
|
||||||
Assert.True(engine.State.Players[0].RearGuards[0]!.Rested);
|
Assert.False(engine.State.Players[0].RearGuards[0]!.Rested);
|
||||||
|
|
||||||
Give(engine, 0, Marron);
|
Give(engine, 0, Marron);
|
||||||
var second = engine.Execute(new CallCommand(Marron, 0));
|
var second = engine.Execute(new CallCommand(Marron, 0));
|
||||||
|
|
@ -222,6 +239,35 @@ public class GameEngineTests
|
||||||
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
|
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Heal_Trigger_Asks_For_Damage_Card_And_Heals_Selected_One()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttack();
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||||
|
GiveDamage(engine, 1, 2);
|
||||||
|
GiveDamage(engine, 0, 1);
|
||||||
|
StackDeck(engine.State.Players[1], Elaine);
|
||||||
|
|
||||||
|
var chosen = engine.State.Players[1].Damage[0];
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
Assert.Equal(TriggerType.Heal, engine.State.PendingTrigger!.Trigger);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingChoice);
|
||||||
|
Assert.Equal(ChoiceTargetKind.Damage, engine.State.PendingChoice!.Kind);
|
||||||
|
Assert.Equal(2, engine.State.PendingChoice.Options.Count);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(engine.State.PendingChoice);
|
||||||
|
Assert.Null(engine.State.PendingTrigger);
|
||||||
|
Assert.Null(engine.State.Battle);
|
||||||
|
Assert.Equal(2, engine.State.Players[1].Damage.Count);
|
||||||
|
Assert.Contains(engine.State.Players[1].Drop, card => card.Id == chosen.Id);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue