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 int _pendingBoosterSlot = -1;
|
||||
private int? _triggerPowerSlot;
|
||||
private readonly HashSet<int> _mulliganSelected = new HashSet<int>();
|
||||
private readonly Dictionary<string, RectTransform> _cardPanels =
|
||||
new Dictionary<string, RectTransform>(StringComparer.Ordinal);
|
||||
private readonly List<CardAnimation> _cardAnimations = new List<CardAnimation>();
|
||||
|
|
@ -205,6 +206,7 @@ namespace Vanguard.UI
|
|||
_result.gameObject.SetActive(false);
|
||||
_pendingIntents.Clear();
|
||||
_highlightKeys.Clear();
|
||||
_mulliganSelected.Clear();
|
||||
_pendingBoosterSlot = -1;
|
||||
_triggerPowerSlot = null;
|
||||
_showAllIntents = false;
|
||||
|
|
@ -1181,16 +1183,123 @@ namespace Vanguard.UI
|
|||
{
|
||||
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.preferredWidth = 174f;
|
||||
element.minWidth = 158f;
|
||||
element.flexibleWidth = 0f;
|
||||
UiKit.AddVerticalLayout(column, 0, 5);
|
||||
UiKit.AddVerticalLayout(column, 0, 4);
|
||||
|
||||
AddPile(column, "УРОН",
|
||||
player.FaceDownDamage + player.FaceUpDamage.Count,
|
||||
new Color(0.48f, 0.12f, 0.12f), 118f);
|
||||
AddDamageZone(column, player);
|
||||
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)
|
||||
|
|
@ -1335,6 +1444,14 @@ namespace Vanguard.UI
|
|||
|
||||
var highlighted = _highlightKeys.Contains(
|
||||
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>();
|
||||
button.targetGraphic = panel.GetComponent<Image>();
|
||||
button.onClick.AddListener(() => OnBoardTargetClicked(new BoardTarget(
|
||||
|
|
@ -1364,13 +1481,6 @@ namespace Vanguard.UI
|
|||
: new Vector2(0f, 250f);
|
||||
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)
|
||||
{
|
||||
var panelImage = panel.GetComponent<Image>();
|
||||
|
|
@ -1381,13 +1491,16 @@ namespace Vanguard.UI
|
|||
var empty = UiKit.CreateText(
|
||||
panel, "EmptyLabel", "Пусто", 12,
|
||||
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;
|
||||
}
|
||||
|
||||
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
||||
UiKit.SetPreferredHeight(art, compact ? 46f : 58f);
|
||||
UiKit.Stretch(art);
|
||||
var artImage = art.GetComponent<Image>();
|
||||
artImage.raycastTarget = false;
|
||||
if (card.FaceUp)
|
||||
{
|
||||
CardImageService.Apply(
|
||||
|
|
@ -1404,60 +1517,65 @@ namespace Vanguard.UI
|
|||
artImage.preserveAspect = false;
|
||||
}
|
||||
|
||||
if (card.FaceUp)
|
||||
{
|
||||
var artName = UiKit.CreateText(
|
||||
art, "ArtName", meta != null ? Loc.CardName(meta) : card.PrintingId, 10,
|
||||
new Color(1f, 1f, 1f, 0.94f), TextAnchor.MiddleCenter);
|
||||
UiKit.MakeLiteral(artName);
|
||||
UiKit.Stretch(artName.rectTransform);
|
||||
artName.rectTransform.offsetMin = new Vector2(3f, 3f);
|
||||
artName.rectTransform.offsetMax = new Vector2(-3f, -2f);
|
||||
artName.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
var slotText = UiKit.CreateText(
|
||||
panel, "Slot", slot, 9,
|
||||
new Color(0.92f, 0.95f, 1f), TextAnchor.UpperLeft);
|
||||
var slotRect = slotText.rectTransform;
|
||||
slotRect.anchorMin = new Vector2(0f, 1f);
|
||||
slotRect.anchorMax = new Vector2(0f, 1f);
|
||||
slotRect.pivot = new Vector2(0f, 1f);
|
||||
slotRect.anchoredPosition = new Vector2(4f, -3f);
|
||||
slotRect.sizeDelta = new Vector2(42f, 14f);
|
||||
slotText.raycastTarget = false;
|
||||
|
||||
if (meta != null)
|
||||
var shade = UiKit.CreatePanel(panel, "Shade", new Color(0f, 0f, 0f, 0.72f));
|
||||
var shadeRect = shade;
|
||||
shadeRect.anchorMin = new Vector2(0f, 0f);
|
||||
shadeRect.anchorMax = new Vector2(1f, 0f);
|
||||
shadeRect.pivot = new Vector2(0.5f, 0f);
|
||||
shadeRect.anchoredPosition = Vector2.zero;
|
||||
shadeRect.sizeDelta = new Vector2(0f, compact ? 40f : 44f);
|
||||
shade.GetComponent<Image>().raycastTarget = false;
|
||||
|
||||
if (card.FaceUp && meta != null)
|
||||
{
|
||||
var grade = UiKit.CreateText(
|
||||
art, "Grade", "G" + meta.Grade, 10,
|
||||
Color.white, TextAnchor.MiddleCenter);
|
||||
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(-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);
|
||||
gradeRect.anchoredPosition = new Vector2(-4f, -3f);
|
||||
gradeRect.sizeDelta = new Vector2(28f, 14f);
|
||||
grade.raycastTarget = false;
|
||||
}
|
||||
|
||||
if (card.Rested)
|
||||
if (!card.FaceUp)
|
||||
{
|
||||
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 hidden = UiKit.CreateText(
|
||||
panel, "Hidden", "?", 26, new Color(1f, 1f, 1f, 0.8f));
|
||||
UiKit.Stretch(hidden.rectTransform);
|
||||
hidden.raycastTarget = false;
|
||||
}
|
||||
|
||||
var name = UiKit.CreateText(
|
||||
panel, "Name",
|
||||
meta != null ? Loc.CardName(meta) : card.PrintingId,
|
||||
10,
|
||||
compact ? 9 : 10,
|
||||
card.FaceUp ? Color.white : Muted,
|
||||
TextAnchor.UpperLeft);
|
||||
TextAnchor.LowerLeft);
|
||||
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.verticalOverflow = VerticalWrapMode.Truncate;
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(name, compact ? 22f : 24f);
|
||||
name.raycastTarget = false;
|
||||
|
||||
var stats = UiKit.CreateText(
|
||||
panel, "Stats",
|
||||
|
|
@ -1465,12 +1583,21 @@ namespace Vanguard.UI
|
|||
? "P " + card.Power + " · C " + card.Critical
|
||||
+ (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty)
|
||||
: "СКРЫТА",
|
||||
10,
|
||||
compact ? 8 : 9,
|
||||
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, 16f);
|
||||
TextAnchor.LowerLeft);
|
||||
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
|
||||
|
|
@ -1485,11 +1612,65 @@ namespace Vanguard.UI
|
|||
private string CardName(string 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)
|
||||
{
|
||||
if (_view == null || _sending)
|
||||
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>();
|
||||
|
||||
if (_pendingIntents.Any(intent => intent is ResolveTriggerIntent)
|
||||
|
|
@ -1849,6 +2030,30 @@ namespace Vanguard.UI
|
|||
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)
|
||||
{
|
||||
var hint = UiKit.CreateText(
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ namespace Vanguard.Tests
|
|||
Resources.Load<Texture2D>("CardArt/td01_005"),
|
||||
"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.
|
||||
lobby.Dispose();
|
||||
store.Dispose();
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ public sealed class GameEngine
|
|||
return false;
|
||||
|
||||
p.Soul.Remove(card);
|
||||
card.Rested = true;
|
||||
card.Rested = false;
|
||||
p.RearGuards[slot] = card;
|
||||
|
||||
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
||||
|
|
@ -1131,7 +1131,7 @@ public sealed class GameEngine
|
|||
return;
|
||||
}
|
||||
|
||||
card.Rested = true;
|
||||
card.Rested = false;
|
||||
p.RearGuards[slot] = card;
|
||||
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
|
||||
|
||||
|
|
@ -1794,7 +1794,7 @@ public sealed class GameEngine
|
|||
|
||||
var card = player.Hand[index];
|
||||
player.Hand.RemoveAt(index);
|
||||
card.Rested = true;
|
||||
card.Rested = false;
|
||||
player.RearGuards[slot] = card;
|
||||
|
||||
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
|
||||
|
|
@ -2326,10 +2326,17 @@ public sealed class GameEngine
|
|||
Draw(player, 1);
|
||||
break;
|
||||
case TriggerType.Heal:
|
||||
Heal(player);
|
||||
if (TryBeginHeal(player, pending, command))
|
||||
return CommandResult.Ok();
|
||||
break;
|
||||
}
|
||||
|
||||
return CompleteTrigger(pending, command);
|
||||
}
|
||||
|
||||
private CommandResult CompleteTrigger(PendingTrigger pending, ResolveTriggerCommand command)
|
||||
{
|
||||
var player = pending.Player;
|
||||
State.PendingTrigger = null;
|
||||
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
|
||||
|
||||
|
|
@ -2353,6 +2360,65 @@ public sealed class GameEngine
|
|||
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)
|
||||
{
|
||||
if (State.IsFinished)
|
||||
|
|
@ -2609,20 +2675,6 @@ public sealed class GameEngine
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -33,12 +33,23 @@ public sealed record UnitTarget(int Player, int Slot);
|
|||
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
|
||||
/// Пока выбор не сделан, движок не продолжает разрешение способности.
|
||||
/// </summary>
|
||||
public enum ChoiceTargetKind
|
||||
{
|
||||
None,
|
||||
Unit,
|
||||
Hand,
|
||||
Damage,
|
||||
Deck,
|
||||
Other,
|
||||
}
|
||||
|
||||
public sealed class PendingChoice
|
||||
{
|
||||
public required int Player { get; init; }
|
||||
public required string Prompt { get; init; }
|
||||
public required IReadOnlyList<ChoiceOption> Options { get; init; }
|
||||
public required Action<int> Resolve { get; init; }
|
||||
public ChoiceTargetKind Kind { get; init; }
|
||||
}
|
||||
|
||||
public enum TriggerContext
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class GameEngineTests
|
|||
private const string Nike = "BT01/056EN"; // Critical
|
||||
private const string Flogal = "BT01/046EN"; // Stand
|
||||
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 Nehalem = "BT01/022EN"; // Kagero
|
||||
private const string Overlord = "BT01/004EN"; // Kagero, 11000
|
||||
|
|
@ -99,13 +100,29 @@ public class GameEngineTests
|
|||
}
|
||||
|
||||
[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();
|
||||
Give(engine, 0, Marron);
|
||||
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
|
||||
[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]
|
||||
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue