Improve guard selection and ability cost payment
CI / test (push) Has been cancelled
Details
CI / test (push) Has been cancelled
Details
This commit is contained in:
parent
4f87af4d23
commit
8618641205
|
|
@ -221,8 +221,10 @@ namespace Vanguard.Board3D
|
||||||
public sealed class BoardRenderState
|
public sealed class BoardRenderState
|
||||||
{
|
{
|
||||||
public HashSet<BoardSlotKey> Highlights { get; } = new HashSet<BoardSlotKey>();
|
public HashSet<BoardSlotKey> Highlights { get; } = new HashSet<BoardSlotKey>();
|
||||||
|
public HashSet<BoardSlotKey> SelectedHighlights { get; } = new HashSet<BoardSlotKey>();
|
||||||
public HashSet<BoardSlotKey> DropTargets { get; } = new HashSet<BoardSlotKey>();
|
public HashSet<BoardSlotKey> DropTargets { get; } = new HashSet<BoardSlotKey>();
|
||||||
public bool GuardianDropAllowed { get; set; }
|
public bool GuardianDropAllowed { get; set; }
|
||||||
|
public HashSet<int> AvailableHandInstanceIds { get; } = new HashSet<int>();
|
||||||
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
||||||
public bool MulliganSelecting { get; set; }
|
public bool MulliganSelecting { get; set; }
|
||||||
public bool DamageChoice { get; set; }
|
public bool DamageChoice { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,12 @@ namespace Vanguard.Board3D
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetHighlighted(bool highlighted)
|
public void SetHighlighted(bool highlighted, Color? color = null)
|
||||||
{
|
{
|
||||||
ApplyColor(highlighted);
|
ApplyColor(highlighted, color ?? _highlightColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyColor(bool highlighted)
|
private void ApplyColor(bool highlighted, Color? color = null)
|
||||||
{
|
{
|
||||||
if (_renderer == null)
|
if (_renderer == null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -93,7 +93,7 @@ namespace Vanguard.Board3D
|
||||||
BoardMaterials.SetColor(
|
BoardMaterials.SetColor(
|
||||||
_block,
|
_block,
|
||||||
_renderer.sharedMaterial,
|
_renderer.sharedMaterial,
|
||||||
highlighted ? _highlightColor : _baseColor);
|
highlighted ? (color ?? _highlightColor) : _baseColor);
|
||||||
_renderer.SetPropertyBlock(_block);
|
_renderer.SetPropertyBlock(_block);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ namespace Vanguard.Board3D
|
||||||
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
||||||
private readonly BoardZoneView[] _triggerZones = new BoardZoneView[2];
|
private readonly BoardZoneView[] _triggerZones = new BoardZoneView[2];
|
||||||
private readonly HashSet<BoardSlotKey> _activeHighlights = new HashSet<BoardSlotKey>();
|
private readonly HashSet<BoardSlotKey> _activeHighlights = new HashSet<BoardSlotKey>();
|
||||||
|
private readonly HashSet<BoardSlotKey> _selectedHighlights = new HashSet<BoardSlotKey>();
|
||||||
private readonly HashSet<BoardSlotKey> _validDropSlots = new HashSet<BoardSlotKey>();
|
private readonly HashSet<BoardSlotKey> _validDropSlots = new HashSet<BoardSlotKey>();
|
||||||
private readonly List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
private readonly List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
||||||
private bool _guardianDropAllowed;
|
private bool _guardianDropAllowed;
|
||||||
|
|
@ -690,9 +691,11 @@ namespace Vanguard.Board3D
|
||||||
Stats = "P" + card.Power + " C" + card.Critical,
|
Stats = "P" + card.Power + " C" + card.Critical,
|
||||||
StatsModified = card.Power != card.BasePower
|
StatsModified = card.Power != card.BasePower
|
||||||
|| card.Critical != card.BaseCritical,
|
|| card.Critical != card.BaseCritical,
|
||||||
Highlight = state.Highlights.Contains(key)
|
Highlight = state.SelectedHighlights.Contains(key)
|
||||||
? _config.highlightColor
|
? _config.selectedColor
|
||||||
: (Color?)null,
|
: state.Highlights.Contains(key)
|
||||||
|
? _config.highlightColor
|
||||||
|
: (Color?)null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -706,13 +709,21 @@ namespace Vanguard.Board3D
|
||||||
var battleCards = view.Battle != null && view.Battle.AttackerPlayer != player.Index
|
var battleCards = view.Battle != null && view.Battle.AttackerPlayer != player.Index
|
||||||
? view.Battle.GuardCards ?? new List<CardView>()
|
? view.Battle.GuardCards ?? new List<CardView>()
|
||||||
: new List<CardView>();
|
: new List<CardView>();
|
||||||
var total = guardians.Count + battleCards.Count;
|
|
||||||
|
// Guardian and battle guard lists contain the same cards after a
|
||||||
|
// guard command; render each physical instance only once.
|
||||||
|
var cards = guardians
|
||||||
|
.Concat(battleCards)
|
||||||
|
.GroupBy(card => card.InstanceId)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList();
|
||||||
|
var total = cards.Count;
|
||||||
if (total == 0)
|
if (total == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (var i = 0; i < guardians.Count; i++)
|
for (var i = 0; i < total; i++)
|
||||||
{
|
{
|
||||||
var card = guardians[i];
|
var card = cards[i];
|
||||||
specs.Add(new CardSpec
|
specs.Add(new CardSpec
|
||||||
{
|
{
|
||||||
Key = "G:" + player.Index + ":" + card.InstanceId,
|
Key = "G:" + player.Index + ":" + card.InstanceId,
|
||||||
|
|
@ -727,27 +738,6 @@ namespace Vanguard.Board3D
|
||||||
Rested = false,
|
Rested = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < battleCards.Count; i++)
|
|
||||||
{
|
|
||||||
var card = battleCards[i];
|
|
||||||
specs.Add(new CardSpec
|
|
||||||
{
|
|
||||||
Key = "BG:" + player.Index + ":" + card.InstanceId,
|
|
||||||
Ref = BoardCardRef.Guardian(player.Index, guardians.Count + i, card),
|
|
||||||
Art = ResolveArt(card.PrintingId),
|
|
||||||
Position = _layout.GuardianPosition(
|
|
||||||
localSeat,
|
|
||||||
guardians.Count + i,
|
|
||||||
total),
|
|
||||||
BaseRotation = Quaternion.Euler(
|
|
||||||
0f,
|
|
||||||
_layout.BaseYaw(localSeat) + 90f,
|
|
||||||
0f),
|
|
||||||
FaceUp = true,
|
|
||||||
Rested = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDamageSpecs(
|
private void AddDamageSpecs(
|
||||||
|
|
@ -843,7 +833,8 @@ namespace Vanguard.Board3D
|
||||||
Color? highlight = null;
|
Color? highlight = null;
|
||||||
if (state.SelectedHandInstanceIds.Contains(card.InstanceId))
|
if (state.SelectedHandInstanceIds.Contains(card.InstanceId))
|
||||||
highlight = _config.selectedColor;
|
highlight = _config.selectedColor;
|
||||||
else if (state.MulliganSelecting)
|
else if (state.AvailableHandInstanceIds.Contains(card.InstanceId)
|
||||||
|
|| state.MulliganSelecting)
|
||||||
highlight = _config.highlightColor;
|
highlight = _config.highlightColor;
|
||||||
|
|
||||||
specs.Add(new CardSpec
|
specs.Add(new CardSpec
|
||||||
|
|
@ -1025,6 +1016,10 @@ namespace Vanguard.Board3D
|
||||||
foreach (var key in state.Highlights)
|
foreach (var key in state.Highlights)
|
||||||
_activeHighlights.Add(key);
|
_activeHighlights.Add(key);
|
||||||
|
|
||||||
|
_selectedHighlights.Clear();
|
||||||
|
foreach (var key in state.SelectedHighlights)
|
||||||
|
_selectedHighlights.Add(key);
|
||||||
|
|
||||||
_validDropSlots.Clear();
|
_validDropSlots.Clear();
|
||||||
foreach (var key in state.DropTargets)
|
foreach (var key in state.DropTargets)
|
||||||
_validDropSlots.Add(key);
|
_validDropSlots.Add(key);
|
||||||
|
|
@ -1037,9 +1032,13 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
foreach (var slot in _slots)
|
foreach (var slot in _slots)
|
||||||
{
|
{
|
||||||
var highlighted = _activeHighlights.Contains(slot.Key)
|
var selected = _selectedHighlights.Contains(slot.Key);
|
||||||
|
var highlighted = selected
|
||||||
|
|| _activeHighlights.Contains(slot.Key)
|
||||||
|| (_dragging && _validDropSlots.Contains(slot.Key));
|
|| (_dragging && _validDropSlots.Contains(slot.Key));
|
||||||
slot.SetHighlighted(highlighted);
|
slot.SetHighlighted(
|
||||||
|
highlighted,
|
||||||
|
selected ? _config.selectedColor : _config.highlightColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_guardianZone != null)
|
if (_guardianZone != null)
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,13 @@ namespace Vanguard.UI
|
||||||
private Action _discardCancelAction;
|
private Action _discardCancelAction;
|
||||||
private int _pendingBoosterSlot = -1;
|
private int _pendingBoosterSlot = -1;
|
||||||
private int _pendingBoostAttackerSlot = -2;
|
private int _pendingBoostAttackerSlot = -2;
|
||||||
|
private readonly Dictionary<int, GuardIntent> _guardSelected =
|
||||||
|
new Dictionary<int, GuardIntent>();
|
||||||
|
private int _guardSelectedInterceptSlot = -1;
|
||||||
|
private int _guardDiscardInstanceId;
|
||||||
|
private string _guardDiscardPrintingId;
|
||||||
|
private ActivateAbilityIntent _pendingAbilityCost;
|
||||||
|
private int _abilityCostDiscardInstanceId;
|
||||||
private int? _triggerPowerSlot;
|
private int? _triggerPowerSlot;
|
||||||
private readonly HashSet<int> _mulliganSelected = new HashSet<int>();
|
private readonly HashSet<int> _mulliganSelected = new HashSet<int>();
|
||||||
private bool _mulliganUsed;
|
private bool _mulliganUsed;
|
||||||
|
|
@ -285,6 +292,8 @@ namespace Vanguard.UI
|
||||||
_abilityDamageIndex = -1;
|
_abilityDamageIndex = -1;
|
||||||
_pendingBoosterSlot = -1;
|
_pendingBoosterSlot = -1;
|
||||||
_pendingBoostAttackerSlot = -2;
|
_pendingBoostAttackerSlot = -2;
|
||||||
|
ClearGuardSelection();
|
||||||
|
ClearAbilityCostSelection();
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
_showAllIntents = false;
|
_showAllIntents = false;
|
||||||
_autoFinishMulligan = false;
|
_autoFinishMulligan = false;
|
||||||
|
|
@ -747,6 +756,8 @@ namespace Vanguard.UI
|
||||||
_autoFinishMulliganAt = 0f;
|
_autoFinishMulliganAt = 0f;
|
||||||
}
|
}
|
||||||
FinishDiscardSelection();
|
FinishDiscardSelection();
|
||||||
|
PruneGuardSelection();
|
||||||
|
PruneAbilityCostSelection();
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
PrepareTriggerSelection();
|
PrepareTriggerSelection();
|
||||||
if (_view.Clock != null)
|
if (_view.Clock != null)
|
||||||
|
|
@ -1088,6 +1099,16 @@ namespace Vanguard.UI
|
||||||
if (_discardChoiceActive)
|
if (_discardChoiceActive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_pendingAbilityCost != null
|
||||||
|
&& intent is not ActivateAbilityIntent
|
||||||
|
&& intent is not ConcedeIntent)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
"Сначала подтвердите оплату способности.",
|
||||||
|
new Color(1f, 0.70f, 0.35f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_abilityDamageIndex >= 0)
|
if (_abilityDamageIndex >= 0)
|
||||||
{
|
{
|
||||||
if (intent is ActivateAbilityIntent activate)
|
if (intent is ActivateAbilityIntent activate)
|
||||||
|
|
@ -1660,9 +1681,13 @@ namespace Vanguard.UI
|
||||||
var state = new BoardRenderState
|
var state = new BoardRenderState
|
||||||
{
|
{
|
||||||
MulliganSelecting = IsMulliganSelecting(),
|
MulliganSelecting = IsMulliganSelecting(),
|
||||||
DamageChoice = IsDamageChoice(),
|
// Damage cards are highlighted only while an ability cost is
|
||||||
|
// being paid, not for ordinary damage choices.
|
||||||
|
DamageChoice = false,
|
||||||
AbilityDamageTargeting = IsAbilityDamageTargeting(),
|
AbilityDamageTargeting = IsAbilityDamageTargeting(),
|
||||||
AbilityDamageIndex = _abilityDamageIndex,
|
AbilityDamageIndex = IsAbilityDamageTargeting()
|
||||||
|
? _abilityDamageIndex
|
||||||
|
: -1,
|
||||||
MatchFinished = _view != null && _view.IsFinished,
|
MatchFinished = _view != null && _view.IsFinished,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1708,6 +1733,64 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_view != null)
|
||||||
|
{
|
||||||
|
var hand = _view.Players
|
||||||
|
.FirstOrDefault(player => player.Index == _view.You)
|
||||||
|
?.Hand ?? new List<CardView>();
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
||||||
|
if (IsGuardStep())
|
||||||
|
{
|
||||||
|
foreach (var guard in legal.OfType<GuardIntent>())
|
||||||
|
{
|
||||||
|
foreach (var card in hand)
|
||||||
|
{
|
||||||
|
if (card.PrintingId == guard.PrintingId)
|
||||||
|
state.AvailableHandInstanceIds.Add(card.InstanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var instanceId in _guardSelected.Keys)
|
||||||
|
if (state.AvailableHandInstanceIds.Contains(instanceId))
|
||||||
|
state.SelectedHandInstanceIds.Add(instanceId);
|
||||||
|
|
||||||
|
foreach (var intercept in legal.OfType<InterceptIntent>())
|
||||||
|
{
|
||||||
|
state.Highlights.Add(new BoardSlotKey(_view.You, intercept.Slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_guardSelectedInterceptSlot >= 0)
|
||||||
|
{
|
||||||
|
state.SelectedHighlights.Add(
|
||||||
|
new BoardSlotKey(_view.You, _guardSelectedInterceptSlot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_discardChoiceActive)
|
||||||
|
{
|
||||||
|
var discardPrintings = new HashSet<string>(
|
||||||
|
_discardAbilityChoices
|
||||||
|
.Select(item => item.DiscardPrintingId)
|
||||||
|
.Concat(_discardGuardChoices
|
||||||
|
.Select(item => item.DiscardPrintingId))
|
||||||
|
.Where(item => !string.IsNullOrEmpty(item)));
|
||||||
|
foreach (var card in hand)
|
||||||
|
{
|
||||||
|
if (discardPrintings.Contains(card.PrintingId))
|
||||||
|
state.AvailableHandInstanceIds.Add(card.InstanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingAbilityCost != null
|
||||||
|
&& _abilityCostDiscardInstanceId != 0
|
||||||
|
&& hand.Any(card =>
|
||||||
|
card.InstanceId == _abilityCostDiscardInstanceId))
|
||||||
|
{
|
||||||
|
state.SelectedHandInstanceIds.Add(_abilityCostDiscardInstanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IsHandChoice())
|
if (IsHandChoice())
|
||||||
{
|
{
|
||||||
var hand = _view.Players
|
var hand = _view.Players
|
||||||
|
|
@ -1769,15 +1852,17 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private bool IsAbilityDamageTargeting()
|
private bool IsAbilityDamageTargeting()
|
||||||
{
|
{
|
||||||
if (_view == null || _view.Choice != null || _view.ActivePlayer != _view.You)
|
if (_view == null
|
||||||
|
|| _view.Choice != null
|
||||||
|
|| _view.ActivePlayer != _view.You
|
||||||
|
|| _pendingAbilityCost == null
|
||||||
|
|| !RequiresCounterBlast(_pendingAbilityCost))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You);
|
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You);
|
||||||
if (self == null || self.FaceUpDamage.Count == 0)
|
return self != null && self.FaceUpDamage.Count > 0;
|
||||||
return false;
|
|
||||||
|
|
||||||
return (_view.LegalIntents ?? new List<IntentPayload>())
|
|
||||||
.Any(intent => intent is ActivateAbilityIntent or ResolveAbilityIntent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBoard3DCardClicked(BoardCardRef card)
|
private void OnBoard3DCardClicked(BoardCardRef card)
|
||||||
|
|
@ -1800,6 +1885,20 @@ namespace Vanguard.UI
|
||||||
if (_inputLock.IsLocked)
|
if (_inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_pendingAbilityCost != null && card.Zone != BoardZone.Damage)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
"Сначала подтвердите оплату способности.",
|
||||||
|
new Color(1f, 0.70f, 0.35f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGuardStep())
|
||||||
|
{
|
||||||
|
ToggleGuardSelection(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var targetSelection = _pendingIntents.Count > 0
|
var targetSelection = _pendingIntents.Count > 0
|
||||||
|| IsMulliganSelecting()
|
|| IsMulliganSelecting()
|
||||||
|| IsDamageChoice()
|
|| IsDamageChoice()
|
||||||
|
|
@ -1858,6 +1957,368 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsGuardStep() =>
|
||||||
|
_view != null
|
||||||
|
&& _view.Step == GameStep.Guard
|
||||||
|
&& _view.ActivePlayer == _view.You;
|
||||||
|
|
||||||
|
private void ToggleGuardSelection(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_view == null || card.Player != _view.You)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (card.Zone == BoardZone.Hand)
|
||||||
|
{
|
||||||
|
ToggleGuardHand(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.Zone is BoardZone.Vanguard or BoardZone.RearGuard)
|
||||||
|
{
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
var intercept = legal.OfType<InterceptIntent>()
|
||||||
|
.FirstOrDefault(intent => intent.Slot == card.Slot);
|
||||||
|
if (intercept == null)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
"Эта карта сейчас не может перехватить атаку.",
|
||||||
|
new Color(1f, 0.55f, 0.45f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_guardSelectedInterceptSlot =
|
||||||
|
_guardSelectedInterceptSlot == card.Slot ? -1 : card.Slot;
|
||||||
|
RefreshGuardSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleGuardHand(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_guardSelected.Remove(card.InstanceId))
|
||||||
|
{
|
||||||
|
RefreshGuardSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
var variants = legal.OfType<GuardIntent>()
|
||||||
|
.Where(intent => intent.PrintingId == card.PrintingId)
|
||||||
|
.ToList();
|
||||||
|
if (variants.Count == 0)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
"Эту карту нельзя использовать для защиты.",
|
||||||
|
new Color(1f, 0.55f, 0.45f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var distinct = variants
|
||||||
|
.GroupBy(intent => intent.DiscardPrintingId ?? string.Empty)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList();
|
||||||
|
var discardVariants = distinct
|
||||||
|
.Where(intent => !string.IsNullOrEmpty(intent.DiscardPrintingId))
|
||||||
|
.ToList();
|
||||||
|
if (discardVariants.Count == 0)
|
||||||
|
{
|
||||||
|
AddGuardSelection(card.InstanceId, distinct[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discardVariants.Count == 1)
|
||||||
|
{
|
||||||
|
AddGuardSelection(card.InstanceId, discardVariants[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_guardDiscardInstanceId = card.InstanceId;
|
||||||
|
_guardDiscardPrintingId = card.PrintingId;
|
||||||
|
StartDiscardSelection(
|
||||||
|
"Perfect Guard: выберите карту в руке для сброса.",
|
||||||
|
null,
|
||||||
|
discardVariants,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_guardDiscardInstanceId = 0;
|
||||||
|
_guardDiscardPrintingId = null;
|
||||||
|
RefreshGuardSelection();
|
||||||
|
});
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddGuardSelection(int instanceId, GuardIntent intent)
|
||||||
|
{
|
||||||
|
_guardSelected[instanceId] = intent;
|
||||||
|
RefreshGuardSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshGuardSelection()
|
||||||
|
{
|
||||||
|
if (_view == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var count = _guardSelected.Count
|
||||||
|
+ (_guardSelectedInterceptSlot >= 0 ? 1 : 0);
|
||||||
|
SetStatus(
|
||||||
|
count == 0
|
||||||
|
? "Защита: выберите карты в руке или на поле."
|
||||||
|
: "Защита: выбрано " + count + " карт.",
|
||||||
|
new Color(0.72f, 0.86f, 1f));
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearGuardSelection()
|
||||||
|
{
|
||||||
|
_guardSelected.Clear();
|
||||||
|
_guardSelectedInterceptSlot = -1;
|
||||||
|
_guardDiscardInstanceId = 0;
|
||||||
|
_guardDiscardPrintingId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PruneGuardSelection()
|
||||||
|
{
|
||||||
|
if (!IsGuardStep())
|
||||||
|
{
|
||||||
|
ClearGuardSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You);
|
||||||
|
var hand = self?.Hand ?? new List<CardView>();
|
||||||
|
var handIds = new HashSet<int>(hand.Select(card => card.InstanceId));
|
||||||
|
foreach (var instanceId in _guardSelected.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (!handIds.Contains(instanceId))
|
||||||
|
_guardSelected.Remove(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_guardSelectedInterceptSlot >= 0
|
||||||
|
&& !(_view.LegalIntents ?? new List<IntentPayload>())
|
||||||
|
.OfType<InterceptIntent>()
|
||||||
|
.Any(intent => intent.Slot == _guardSelectedInterceptSlot))
|
||||||
|
{
|
||||||
|
_guardSelectedInterceptSlot = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GuardSelectionShield()
|
||||||
|
{
|
||||||
|
var shield = 0;
|
||||||
|
foreach (var guard in _guardSelected.Values)
|
||||||
|
{
|
||||||
|
var meta = ResolveCatalogItem(guard.PrintingId);
|
||||||
|
if (meta != null && !meta.IsSentinel)
|
||||||
|
shield += Math.Max(0, meta.Shield);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shield;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ConfirmGuardSelection()
|
||||||
|
{
|
||||||
|
if (_view == null
|
||||||
|
|| _connection == null
|
||||||
|
|| _cts == null
|
||||||
|
|| _sending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var intents = new List<IntentPayload>();
|
||||||
|
if (_guardSelectedInterceptSlot >= 0)
|
||||||
|
{
|
||||||
|
var intercept = (_view.LegalIntents ?? new List<IntentPayload>())
|
||||||
|
.OfType<InterceptIntent>()
|
||||||
|
.FirstOrDefault(intent => intent.Slot == _guardSelectedInterceptSlot);
|
||||||
|
if (intercept != null)
|
||||||
|
intents.Add(intercept);
|
||||||
|
}
|
||||||
|
|
||||||
|
intents.AddRange(_guardSelected.Values);
|
||||||
|
|
||||||
|
if (intents.Count == 0)
|
||||||
|
{
|
||||||
|
OnIntentClicked(new PassGuardIntent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearGuardSelection();
|
||||||
|
Render();
|
||||||
|
_sending = true;
|
||||||
|
SetStatus("Отправляем защиту…", Muted);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var acceptedAll = true;
|
||||||
|
foreach (var intent in intents)
|
||||||
|
{
|
||||||
|
var result = await _connection.SubmitAsync(intent, _cts.Token);
|
||||||
|
if (!result.Accepted)
|
||||||
|
{
|
||||||
|
acceptedAll = false;
|
||||||
|
SetStatus(
|
||||||
|
"Защита отклонена (" + result.Code + "): "
|
||||||
|
+ (result.Reason ?? "без причины"),
|
||||||
|
new Color(1f, 0.55f, 0.45f));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptedAll)
|
||||||
|
{
|
||||||
|
// "Защищаться" is the final confirmation: add the selected
|
||||||
|
// guards and pass the guard step in one action.
|
||||||
|
await _connection.SubmitAsync(new PassGuardIntent(), _cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
await RequestSnapshotAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// экран закрыли
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Debug.LogException(exception);
|
||||||
|
SetStatus(
|
||||||
|
"Ошибка отправки защиты: " + exception.Message,
|
||||||
|
new Color(1f, 0.55f, 0.45f));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sending = false;
|
||||||
|
if (_view != null)
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool RequiresCounterBlast(ActivateAbilityIntent intent)
|
||||||
|
{
|
||||||
|
if (intent == null || _view == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var card = InstanceAt(_view.You, intent.InstanceId);
|
||||||
|
var meta = ResolveCatalogItem(card?.PrintingId ?? string.Empty);
|
||||||
|
if (meta == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var text = ((meta.TextEn ?? string.Empty) + " "
|
||||||
|
+ (meta.TextRu ?? string.Empty)).ToLowerInvariant();
|
||||||
|
return text.Contains("counter-blast")
|
||||||
|
|| text.Contains("counter blast")
|
||||||
|
|| text.Contains("встречн");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ActivateAbility(ActivateAbilityIntent intent)
|
||||||
|
{
|
||||||
|
if (intent == null || _view == null || _pendingAbilityCost != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(intent.DiscardPrintingId))
|
||||||
|
{
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
var variants = legal.OfType<ActivateAbilityIntent>()
|
||||||
|
.Where(variant => variant.InstanceId == intent.InstanceId
|
||||||
|
&& !string.IsNullOrEmpty(variant.DiscardPrintingId))
|
||||||
|
.ToList();
|
||||||
|
_pendingAbilityCost = null;
|
||||||
|
_abilityCostDiscardInstanceId = 0;
|
||||||
|
_abilityDamageIndex = -1;
|
||||||
|
StartDiscardSelection(
|
||||||
|
"Выберите карту в руке для сброса.",
|
||||||
|
variants,
|
||||||
|
null);
|
||||||
|
Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RequiresCounterBlast(intent))
|
||||||
|
{
|
||||||
|
BeginAbilityCost(intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnIntentClicked(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginAbilityCost(ActivateAbilityIntent intent)
|
||||||
|
{
|
||||||
|
_pendingAbilityCost = intent;
|
||||||
|
_abilityCostDiscardInstanceId = 0;
|
||||||
|
_abilityDamageIndex = -1;
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_highlightSlots.Clear();
|
||||||
|
SetStatus(
|
||||||
|
"Оплата способности: выберите карты для цены.",
|
||||||
|
new Color(0.98f, 0.82f, 0.42f));
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmAbilityCost()
|
||||||
|
{
|
||||||
|
if (_pendingAbilityCost == null || _view == null || _sending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (RequiresCounterBlast(_pendingAbilityCost)
|
||||||
|
&& _abilityDamageIndex < 0)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
"Выберите карту урона для Counter Blast.",
|
||||||
|
new Color(1f, 0.70f, 0.35f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activate = new ActivateAbilityIntent
|
||||||
|
{
|
||||||
|
InstanceId = _pendingAbilityCost.InstanceId,
|
||||||
|
DiscardPrintingId = _pendingAbilityCost.DiscardPrintingId,
|
||||||
|
DamageIndex = _abilityDamageIndex,
|
||||||
|
};
|
||||||
|
ClearAbilityCostSelection();
|
||||||
|
OnIntentClicked(activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearAbilityCostSelection()
|
||||||
|
{
|
||||||
|
_pendingAbilityCost = null;
|
||||||
|
_abilityCostDiscardInstanceId = 0;
|
||||||
|
_abilityDamageIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PruneAbilityCostSelection()
|
||||||
|
{
|
||||||
|
if (_pendingAbilityCost == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_view == null || _view.ActivePlayer != _view.You)
|
||||||
|
{
|
||||||
|
ClearAbilityCostSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stillLegal = (_view.LegalIntents ?? new List<IntentPayload>())
|
||||||
|
.OfType<ActivateAbilityIntent>()
|
||||||
|
.Any(intent => intent.InstanceId == _pendingAbilityCost.InstanceId);
|
||||||
|
if (!stillLegal)
|
||||||
|
ClearAbilityCostSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAbilityCostReady()
|
||||||
|
{
|
||||||
|
if (_pendingAbilityCost == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !RequiresCounterBlast(_pendingAbilityCost)
|
||||||
|
|| _abilityDamageIndex >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleGuardFromDrop(BoardCardRef source)
|
||||||
|
{
|
||||||
|
if (!IsGuardStep())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ToggleGuardSelection(source);
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenCardMenu(BoardCardRef card)
|
private void OpenCardMenu(BoardCardRef card)
|
||||||
{ if (_view == null || _board3D == null)
|
{ if (_view == null || _board3D == null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -2140,7 +2601,7 @@ namespace Vanguard.UI
|
||||||
.ToList();
|
.ToList();
|
||||||
if (discardVariants.Count == 0)
|
if (discardVariants.Count == 0)
|
||||||
{
|
{
|
||||||
OnIntentClicked(free ?? distinct[0]);
|
ActivateAbility(free ?? distinct[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2152,7 +2613,7 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
new CardActionMenuItem(
|
new CardActionMenuItem(
|
||||||
"Без сброса",
|
"Без сброса",
|
||||||
() => OnIntentClicked(free)),
|
() => ActivateAbility(free)),
|
||||||
new CardActionMenuItem(
|
new CardActionMenuItem(
|
||||||
"Сбросить карту в руке",
|
"Сбросить карту в руке",
|
||||||
() => StartDiscardSelection(
|
() => StartDiscardSelection(
|
||||||
|
|
@ -2209,8 +2670,12 @@ namespace Vanguard.UI
|
||||||
item => item.DiscardPrintingId == printing);
|
item => item.DiscardPrintingId == printing);
|
||||||
if (ability != null)
|
if (ability != null)
|
||||||
{
|
{
|
||||||
|
var discardInstanceId = card.InstanceId;
|
||||||
FinishDiscardSelection();
|
FinishDiscardSelection();
|
||||||
OnIntentClicked(ability);
|
_pendingAbilityCost = ability;
|
||||||
|
_abilityCostDiscardInstanceId = discardInstanceId;
|
||||||
|
_abilityDamageIndex = -1;
|
||||||
|
Render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2218,7 +2683,16 @@ namespace Vanguard.UI
|
||||||
item => item.DiscardPrintingId == printing);
|
item => item.DiscardPrintingId == printing);
|
||||||
if (guard != null)
|
if (guard != null)
|
||||||
{
|
{
|
||||||
|
var guardInstanceId = _guardDiscardInstanceId;
|
||||||
FinishDiscardSelection();
|
FinishDiscardSelection();
|
||||||
|
if (guardInstanceId != 0)
|
||||||
|
{
|
||||||
|
_guardDiscardInstanceId = 0;
|
||||||
|
_guardDiscardPrintingId = null;
|
||||||
|
AddGuardSelection(guardInstanceId, guard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
OnIntentClicked(guard);
|
OnIntentClicked(guard);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2253,6 +2727,8 @@ namespace Vanguard.UI
|
||||||
_discardGuardChoices.Clear();
|
_discardGuardChoices.Clear();
|
||||||
_discardCancelAction = null;
|
_discardCancelAction = null;
|
||||||
_discardPrompt = null;
|
_discardPrompt = null;
|
||||||
|
_guardDiscardInstanceId = 0;
|
||||||
|
_guardDiscardPrintingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelDiscardSelection()
|
private void CancelDiscardSelection()
|
||||||
|
|
@ -2354,6 +2830,14 @@ namespace Vanguard.UI
|
||||||
if (_view == null || _sending || _inputLock.IsLocked)
|
if (_view == null || _sending || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (IsGuardStep()
|
||||||
|
&& target.Zone == BoardZone.Guardian
|
||||||
|
&& (source.Zone == BoardZone.Hand || source.IsField))
|
||||||
|
{
|
||||||
|
ToggleGuardFromDrop(source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
IntentPayload intent = null;
|
IntentPayload intent = null;
|
||||||
|
|
||||||
|
|
@ -2486,6 +2970,30 @@ namespace Vanguard.UI
|
||||||
if (_view == null || _inputLock.IsLocked)
|
if (_view == null || _inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_discardChoiceActive)
|
||||||
|
{
|
||||||
|
CancelDiscardSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingAbilityCost != null)
|
||||||
|
{
|
||||||
|
ClearAbilityCostSelection();
|
||||||
|
SetStatus("Оплата способности отменена.", Muted);
|
||||||
|
Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGuardStep()
|
||||||
|
&& (_guardSelected.Count > 0
|
||||||
|
|| _guardSelectedInterceptSlot >= 0))
|
||||||
|
{
|
||||||
|
ClearGuardSelection();
|
||||||
|
SetStatus("Выбор защиты снят.", Muted);
|
||||||
|
Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (card.Zone == BoardZone.Vanguard)
|
if (card.Zone == BoardZone.Vanguard)
|
||||||
OpenZone(BoardZone.Soul, card.Player);
|
OpenZone(BoardZone.Soul, card.Player);
|
||||||
}
|
}
|
||||||
|
|
@ -2566,6 +3074,15 @@ namespace Vanguard.UI
|
||||||
? "Выберите карту в руке для сброса."
|
? "Выберите карту в руке для сброса."
|
||||||
: _discardPrompt;
|
: _discardPrompt;
|
||||||
}
|
}
|
||||||
|
else if (_pendingAbilityCost != null)
|
||||||
|
{
|
||||||
|
title = "ОПЛАТА";
|
||||||
|
hint = RequiresCounterBlast(_pendingAbilityCost)
|
||||||
|
? _abilityDamageIndex < 0
|
||||||
|
? "Выберите карту урона для Counter Blast."
|
||||||
|
: "Оплата выбрана. Подтвердите в панели действий."
|
||||||
|
: "Оплата выбрана. Подтвердите в панели действий.";
|
||||||
|
}
|
||||||
else if (_pendingBoosterSlot >= 0 && _pendingIntents.Count > 0)
|
else if (_pendingBoosterSlot >= 0 && _pendingIntents.Count > 0)
|
||||||
{
|
{
|
||||||
title = "ПОДДЕРЖКА";
|
title = "ПОДДЕРЖКА";
|
||||||
|
|
@ -2618,7 +3135,12 @@ namespace Vanguard.UI
|
||||||
else if (_view.Step == GameStep.Guard)
|
else if (_view.Step == GameStep.Guard)
|
||||||
{
|
{
|
||||||
title = "ЗАЩИТА";
|
title = "ЗАЩИТА";
|
||||||
hint = "Выберите защиту или не защищайтесь.";
|
var guardCount = _guardSelected.Count
|
||||||
|
+ (_guardSelectedInterceptSlot >= 0 ? 1 : 0);
|
||||||
|
hint = guardCount == 0
|
||||||
|
? "Кликните карты в руке или на поле для защиты."
|
||||||
|
: "Выбрано карт: " + guardCount
|
||||||
|
+ ". Щит: +" + GuardSelectionShield() + ".";
|
||||||
}
|
}
|
||||||
else if (_view.Phase == GamePhase.Battle)
|
else if (_view.Phase == GamePhase.Battle)
|
||||||
{
|
{
|
||||||
|
|
@ -2661,7 +3183,7 @@ namespace Vanguard.UI
|
||||||
_phasePanel.anchorMax = new Vector2(1f, 0.5f);
|
_phasePanel.anchorMax = new Vector2(1f, 0.5f);
|
||||||
_phasePanel.pivot = new Vector2(1f, 0.5f);
|
_phasePanel.pivot = new Vector2(1f, 0.5f);
|
||||||
_phasePanel.anchoredPosition = new Vector2(-468f, 0f);
|
_phasePanel.anchoredPosition = new Vector2(-468f, 0f);
|
||||||
_phasePanel.sizeDelta = new Vector2(156f, 188f);
|
_phasePanel.sizeDelta = new Vector2(190f, 220f);
|
||||||
UiKit.AddVerticalLayout(_phasePanel, 8, 6);
|
UiKit.AddVerticalLayout(_phasePanel, 8, 6);
|
||||||
_phasePanel.gameObject.SetActive(false);
|
_phasePanel.gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
@ -2672,12 +3194,16 @@ namespace Vanguard.UI
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ClearChildren(_phasePanel);
|
ClearChildren(_phasePanel);
|
||||||
if (view == null || _pendingIntents.Count > 0)
|
if (view == null
|
||||||
|
|| _pendingIntents.Count > 0
|
||||||
|
|| _discardChoiceActive)
|
||||||
{
|
{
|
||||||
_phasePanel.gameObject.SetActive(false);
|
_phasePanel.gameObject.SetActive(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isGuardStep = IsGuardStep();
|
||||||
|
var isAbilityCost = _pendingAbilityCost != null;
|
||||||
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||||
.Where(IsPhaseIntent)
|
.Where(IsPhaseIntent)
|
||||||
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
||||||
|
|
@ -2685,7 +3211,7 @@ namespace Vanguard.UI
|
||||||
// inactive finish button after the exchange is misleading.
|
// inactive finish button after the exchange is misleading.
|
||||||
.Where(intent => !(intent is FinishMulliganIntent && _mulliganUsed))
|
.Where(intent => !(intent is FinishMulliganIntent && _mulliganUsed))
|
||||||
.ToList();
|
.ToList();
|
||||||
if (intents.Count == 0)
|
if (intents.Count == 0 && !isGuardStep && !isAbilityCost)
|
||||||
{
|
{
|
||||||
_phasePanel.gameObject.SetActive(false);
|
_phasePanel.gameObject.SetActive(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -2695,12 +3221,20 @@ namespace Vanguard.UI
|
||||||
var title = UiKit.CreateText(
|
var title = UiKit.CreateText(
|
||||||
_phasePanel,
|
_phasePanel,
|
||||||
"PhaseTitle",
|
"PhaseTitle",
|
||||||
"ФАЗЫ",
|
"ДЕЙСТВИЯ",
|
||||||
14,
|
14,
|
||||||
new Color(0.72f, 0.80f, 0.94f));
|
new Color(0.72f, 0.80f, 0.94f));
|
||||||
title.fontStyle = FontStyle.Bold;
|
title.fontStyle = FontStyle.Bold;
|
||||||
UiKit.SetPreferredHeight(title, 22f);
|
UiKit.SetPreferredHeight(title, 22f);
|
||||||
|
|
||||||
|
if (isGuardStep)
|
||||||
|
AddGuardPhaseButton();
|
||||||
|
if (isAbilityCost)
|
||||||
|
{
|
||||||
|
AddAbilityCostPhaseButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var ride in (view.LegalIntents ?? new List<IntentPayload>())
|
foreach (var ride in (view.LegalIntents ?? new List<IntentPayload>())
|
||||||
.OfType<RideIntent>()
|
.OfType<RideIntent>()
|
||||||
.GroupBy(item => item.PrintingId)
|
.GroupBy(item => item.PrintingId)
|
||||||
|
|
@ -2744,6 +3278,45 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddGuardPhaseButton()
|
||||||
|
{
|
||||||
|
var count = _guardSelected.Count
|
||||||
|
+ (_guardSelectedInterceptSlot >= 0 ? 1 : 0);
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_phasePanel,
|
||||||
|
"Phase_Guard",
|
||||||
|
count == 0 ? "Не защищаться" : "Защищаться",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (_guardSelected.Count == 0
|
||||||
|
&& _guardSelectedInterceptSlot < 0)
|
||||||
|
{
|
||||||
|
OnIntentClicked(new PassGuardIntent());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfirmGuardSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
14);
|
||||||
|
button.interactable = !_inputLock.IsLocked && !_sending;
|
||||||
|
UiKit.SetPreferredHeight(button, 40f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddAbilityCostPhaseButton()
|
||||||
|
{
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_phasePanel,
|
||||||
|
"Phase_ConfirmAbilityCost",
|
||||||
|
"Подтвердить оплату",
|
||||||
|
ConfirmAbilityCost,
|
||||||
|
14);
|
||||||
|
button.interactable = !_inputLock.IsLocked
|
||||||
|
&& !_sending
|
||||||
|
&& IsAbilityCostReady();
|
||||||
|
UiKit.SetPreferredHeight(button, 40f);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsPhaseIntent(IntentPayload intent) =>
|
private static bool IsPhaseIntent(IntentPayload intent) =>
|
||||||
intent is FinishMulliganIntent or SkipRideIntent
|
intent is FinishMulliganIntent or SkipRideIntent
|
||||||
or EnterBattleIntent or EndTurnIntent;
|
or EnterBattleIntent or EndTurnIntent;
|
||||||
|
|
@ -2838,7 +3411,7 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
var attackPower = attacker.Power;
|
var attackPower = attacker.Power;
|
||||||
var shield = defender.Power + battle.GuardShield;
|
var shield = defender.Power + battle.GuardShield + GuardSelectionShield();
|
||||||
var text = battle.Nullified
|
var text = battle.Nullified
|
||||||
? "POWER " + attackPower + " · NULLIFIED"
|
? "POWER " + attackPower + " · NULLIFIED"
|
||||||
: "POWER " + attackPower + " vs SHIELD " + shield;
|
: "POWER " + attackPower + " vs SHIELD " + shield;
|
||||||
|
|
@ -3073,7 +3646,10 @@ namespace Vanguard.UI
|
||||||
if (sources.Count == 1)
|
if (sources.Count == 1)
|
||||||
{
|
{
|
||||||
ClearSelection(rebuild: false);
|
ClearSelection(rebuild: false);
|
||||||
OnIntentClicked(sources[0]);
|
if (sources[0] is ActivateAbilityIntent ability)
|
||||||
|
ActivateAbility(ability);
|
||||||
|
else
|
||||||
|
OnIntentClicked(sources[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3106,7 +3682,10 @@ namespace Vanguard.UI
|
||||||
if (withTargets.Count == 0)
|
if (withTargets.Count == 0)
|
||||||
{
|
{
|
||||||
ClearSelection(rebuild: false);
|
ClearSelection(rebuild: false);
|
||||||
OnIntentClicked(sources[0]);
|
if (sources[0] is ActivateAbilityIntent ability)
|
||||||
|
ActivateAbility(ability);
|
||||||
|
else
|
||||||
|
OnIntentClicked(sources[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3354,7 +3933,6 @@ namespace Vanguard.UI
|
||||||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||||||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||||
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
||||||
var guards = playable.OfType<GuardIntent>().ToList();
|
|
||||||
var rides = playable.OfType<RideIntent>().ToList();
|
var rides = playable.OfType<RideIntent>().ToList();
|
||||||
var board = playable
|
var board = playable
|
||||||
.Where(intent => IsBoardIntent(intent)
|
.Where(intent => IsBoardIntent(intent)
|
||||||
|
|
@ -3365,6 +3943,7 @@ namespace Vanguard.UI
|
||||||
var global = playable
|
var global = playable
|
||||||
.Where(intent => !IsBoardIntent(intent)
|
.Where(intent => !IsBoardIntent(intent)
|
||||||
&& !IsPhaseIntent(intent)
|
&& !IsPhaseIntent(intent)
|
||||||
|
&& intent is not PassGuardIntent
|
||||||
&& intent is not ActivateAbilityIntent
|
&& intent is not ActivateAbilityIntent
|
||||||
&& intent is not ResolveAbilityIntent
|
&& intent is not ResolveAbilityIntent
|
||||||
&& !(intent is ChooseIntent
|
&& !(intent is ChooseIntent
|
||||||
|
|
@ -3464,12 +4043,9 @@ namespace Vanguard.UI
|
||||||
UiKit.SetPreferredHeight(toggle, 38f);
|
UiKit.SetPreferredHeight(toggle, 38f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (abilities.Count > 0)
|
if (abilities.Count > 0 && _pendingAbilityCost == null)
|
||||||
AddAbilityButton(abilities);
|
AddAbilityButton(abilities);
|
||||||
|
|
||||||
if (guards.Count > 0)
|
|
||||||
AddGuardButton(guards);
|
|
||||||
|
|
||||||
if (rides.Count > 0)
|
if (rides.Count > 0)
|
||||||
AddRideButtons(rides);
|
AddRideButtons(rides);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,71 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Guard_Control_Is_One_Button_In_Action_Panel()
|
||||||
|
{
|
||||||
|
var config = new AppConfig();
|
||||||
|
config.Load();
|
||||||
|
var tokens = new TokenStore();
|
||||||
|
var api = new ApiClient(config, tokens);
|
||||||
|
var player = new PlayerState();
|
||||||
|
var auth = new AuthService(api, tokens, player);
|
||||||
|
var meta = new MetaService(api, player);
|
||||||
|
var canvas = UiKit.CreateCanvas("Guard Panel Test Canvas");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var battle = new BattleView(canvas.transform, meta, () => { });
|
||||||
|
var match = new MatchView
|
||||||
|
{
|
||||||
|
You = 0,
|
||||||
|
ActivePlayer = 0,
|
||||||
|
Phase = GamePhase.Battle,
|
||||||
|
Step = GameStep.Guard,
|
||||||
|
LegalIntents = new List<IntentPayload>
|
||||||
|
{
|
||||||
|
new GuardIntent { PrintingId = "BT01/004EN" },
|
||||||
|
new PassGuardIntent(),
|
||||||
|
new ConcedeIntent(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var viewField = typeof(BattleView)
|
||||||
|
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
var rebuild = typeof(BattleView)
|
||||||
|
.GetMethod("RebuildPhaseControls", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
viewField.SetValue(battle, match);
|
||||||
|
rebuild.Invoke(battle, new object[] { match });
|
||||||
|
|
||||||
|
Assert.IsTrue(
|
||||||
|
FindRect("PhasePanel").gameObject.activeSelf,
|
||||||
|
"Guard controls must be in the action panel");
|
||||||
|
var guardButton = Resources.FindObjectsOfTypeAll<Button>()
|
||||||
|
.FirstOrDefault(button => button.name == "Phase_Guard");
|
||||||
|
Assert.IsNotNull(guardButton);
|
||||||
|
Assert.AreEqual(
|
||||||
|
"Не защищаться",
|
||||||
|
guardButton.GetComponentInChildren<Text>().text);
|
||||||
|
Assert.IsFalse(
|
||||||
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.Any(go => go.name == "GuardAction"),
|
||||||
|
"Legacy guard button must not remain in the side panel");
|
||||||
|
Assert.IsFalse(
|
||||||
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.Any(go => go.name == "Phase_PassGuardIntent"),
|
||||||
|
"Pass guard must be the same single button");
|
||||||
|
Assert.AreEqual(
|
||||||
|
"ДЕЙСТВИЯ",
|
||||||
|
Resources.FindObjectsOfTypeAll<Text>()
|
||||||
|
.First(text => text.name == "PhaseTitle").text);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(canvas.gameObject);
|
||||||
|
Cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Optional_Ability_Shows_Overlay_Instead_Of_Action_Panel()
|
public void Optional_Ability_Shows_Overlay_Instead_Of_Action_Panel()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,42 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Guardian_And_Battle_Guard_Cards_Are_Not_Duplicated()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var board = GameBoard3D.Create(null, config);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
board.SetVisible(true);
|
||||||
|
var view = BuildView(you: 0);
|
||||||
|
var self = view.Players.Single(player => player.Index == 0);
|
||||||
|
view.Battle = new BattleView
|
||||||
|
{
|
||||||
|
AttackerPlayer = 1,
|
||||||
|
AttackerSlot = 3,
|
||||||
|
TargetSlot = -1,
|
||||||
|
GuardCards = self.Guardian,
|
||||||
|
};
|
||||||
|
|
||||||
|
board.Render(view, new BoardRenderState());
|
||||||
|
|
||||||
|
var guardianCards = ActiveCards()
|
||||||
|
.Where(card => card.Ref.Zone == BoardZone.Guardian
|
||||||
|
&& card.Ref.Player == 0
|
||||||
|
&& card.Ref.InstanceId == 7)
|
||||||
|
.ToList();
|
||||||
|
Assert.AreEqual(
|
||||||
|
1,
|
||||||
|
guardianCards.Count,
|
||||||
|
"A guard card present in both Guardian and Battle.GuardCards must render once");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(board.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player()
|
public void Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,28 @@
|
||||||
# Передача сессии
|
# Передача сессии
|
||||||
|
|
||||||
Состояние проекта на **22 октября 2026**. Документ нужен, чтобы новая сессия
|
Состояние проекта на **23 октября 2026**. Документ нужен, чтобы новая сессия
|
||||||
продолжила работу без повторного разбора истории. Краткая оперативная
|
продолжила работу без повторного разбора истории. Краткая оперативная
|
||||||
сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю.
|
сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-10-23 — защита и оплата способностей
|
||||||
|
|
||||||
|
- Панель «Фазы» переименована в «Действия»; защита перенесена туда.
|
||||||
|
- Защита выбирается кликом/драгом по картам в руке и перехватчикам на поле.
|
||||||
|
В панели одна кнопка: «Не защищаться» без выбора, «Защищаться» с выбранными
|
||||||
|
картами. Щит выбранных карт сразу входит в `POWER vs SHIELD`; после
|
||||||
|
подтверждения карты отправляются в Guardian, шаг защиты завершается.
|
||||||
|
- Дубли карт в Guardian (`player.Guardian` + `Battle.GuardCards`) устранены
|
||||||
|
дедупликацией по `InstanceId`.
|
||||||
|
- Оплата активируемых способностей: сброс выбирается в руке, Counter Blast —
|
||||||
|
кликом по урону. Доступные и выбранные карты цены подсвечиваются; оплата
|
||||||
|
подтверждается кнопкой «Подтвердить оплату» в панели «Действия».
|
||||||
|
Урон больше не подсвечивается вне режима оплаты. Правый клик отменяет выбор.
|
||||||
|
- Тесты клиента: **38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. Проект
|
## 1. Проект
|
||||||
|
|
||||||
Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**.
|
Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**.
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,21 @@
|
||||||
`P/C`.
|
`P/C`.
|
||||||
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
||||||
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
||||||
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Фазы».
|
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Действия».
|
||||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||||
нескольких вариантах открывается overlay выбора способности. Карта для
|
нескольких вариантах открывается overlay выбора способности. Карта для
|
||||||
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
||||||
Perfect Guard также просит выбрать карту сброса в руке.
|
Perfect Guard также просит выбрать карту сброса в руке.
|
||||||
- В фазе защиты доступны «Защититься» (если есть допустимые карты) и
|
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
||||||
«Не защищаться»; карта защиты и карта сброса выбираются в руке.
|
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
||||||
|
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
||||||
|
отменяет выбор.
|
||||||
|
- Защита: в панели «Действия» одна кнопка — «Не защищаться» без выбора и
|
||||||
|
«Защищаться» с выбранными картами. Карты защиты и перехвата выбираются
|
||||||
|
кликом/драгом по объектам; допустимые подсвечиваются, выбранные — зелёным.
|
||||||
|
Щит выбранных карт сразу учитывается в счётчике; после подтверждения карты
|
||||||
|
уходят в зону защиты, шаг защиты завершается. Дубли карт в Guardian
|
||||||
|
устранены.
|
||||||
- Локальный standalone работает в фоне (`runInBackground`), чтобы heartbeat
|
- Локальный standalone работает в фоне (`runInBackground`), чтобы heartbeat
|
||||||
WebSocket не останавливался при потере фокуса.
|
WebSocket не останавливался при потере фокуса.
|
||||||
- При потере соединения весь бой блокируется полноэкранным overlay
|
- При потере соединения весь бой блокируется полноэкранным overlay
|
||||||
|
|
@ -102,9 +110,9 @@
|
||||||
Кнопка внутри overlay позволяет выйти в меню.
|
Кнопка внутри overlay позволяет выйти в меню.
|
||||||
- Правую панель действий можно временно скрыть кнопкой «Скрыть панель»
|
- Правую панель действий можно временно скрыть кнопкой «Скрыть панель»
|
||||||
в верхней строке; поле остаётся видимым, вернуть — «Показать панель».
|
в верхней строке; поле остаётся видимым, вернуть — «Показать панель».
|
||||||
При скрытии панель фаз и блоки противника сдвигаются вправо, а рука
|
При скрытии панель действий и блоки противника сдвигаются вправо, а рука
|
||||||
центрируется по горизонтали; при показе возвращается влево.
|
центрируется по горизонтали; при показе возвращается влево.
|
||||||
- Кнопка муллигана находится в панели «Фазы» и динамическая: без выбора —
|
- Кнопка муллигана находится в панели «Действия» и динамическая: без выбора —
|
||||||
«Закончить муллиган», с выбором — «Заменить X карт».
|
«Закончить муллиган», с выбором — «Заменить X карт».
|
||||||
- Trigger-зона у противника расположена над его колодой по направлению
|
- Trigger-зона у противника расположена над его колодой по направлению
|
||||||
к центру стола (зеркально своей), а не за дальним краем.
|
к центру стола (зеркально своей), а не за дальним краем.
|
||||||
|
|
@ -115,7 +123,7 @@
|
||||||
только закрытые карты. Душа — юниты под VC: отдельной стопки нет,
|
только закрытые карты. Душа — юниты под VC: отдельной стопки нет,
|
||||||
ПКМ по VC открывает список. Дроп по умолчанию пуст; сверху показывает
|
ПКМ по VC открывает список. Дроп по умолчанию пуст; сверху показывает
|
||||||
последнюю карту, ЛКМ — список всего дропа. Колода и дроп поменяны
|
последнюю карту, ЛКМ — список всего дропа. Колода и дроп поменяны
|
||||||
местами (колода ближе к игроку). Справа от круга защиты — панель фаз:
|
местами (колода ближе к игроку). Справа от круга защиты — панель действий:
|
||||||
конец муллигана (авто после обмена), скип смены, конец главной/боевой;
|
конец муллигана (авто после обмена), скип смены, конец главной/боевой;
|
||||||
боевая завершается сама, если нет атак и активаций. Рука противника —
|
боевая завершается сама, если нет атак и активаций. Рука противника —
|
||||||
веер закрытых карт у дальнего края стола, только рубашки.
|
веер закрытых карт у дальнего края стола, только рубашки.
|
||||||
|
|
@ -229,13 +237,14 @@ Standalone Windows:
|
||||||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **36/36** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **38/38** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||||
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
||||||
стрелка атаки, prefab-оверлеи, раскладка на 1920×1080, авто-завершение
|
стрелка атаки, prefab-оверлеи, раскладка на 1920×1080, авто-завершение
|
||||||
муллигана без кнопки после обмена), `SCENE_SMOKE
|
муллигана без кнопки после обмена, одна кнопка защиты в панели действий,
|
||||||
|
отсутствие дублей карт в Guardian), `SCENE_SMOKE
|
||||||
success=True`, `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
success=True`, `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||||
|
|
||||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue