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 HashSet<BoardSlotKey> Highlights { get; } = new HashSet<BoardSlotKey>();
|
||||
public HashSet<BoardSlotKey> SelectedHighlights { get; } = new HashSet<BoardSlotKey>();
|
||||
public HashSet<BoardSlotKey> DropTargets { get; } = new HashSet<BoardSlotKey>();
|
||||
public bool GuardianDropAllowed { get; set; }
|
||||
public HashSet<int> AvailableHandInstanceIds { get; } = new HashSet<int>();
|
||||
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
||||
public bool MulliganSelecting { get; set; }
|
||||
public bool DamageChoice { get; set; }
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ namespace Vanguard.Board3D
|
|||
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)
|
||||
return;
|
||||
|
|
@ -93,7 +93,7 @@ namespace Vanguard.Board3D
|
|||
BoardMaterials.SetColor(
|
||||
_block,
|
||||
_renderer.sharedMaterial,
|
||||
highlighted ? _highlightColor : _baseColor);
|
||||
highlighted ? (color ?? _highlightColor) : _baseColor);
|
||||
_renderer.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Vanguard.Board3D
|
|||
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
||||
private readonly BoardZoneView[] _triggerZones = new BoardZoneView[2];
|
||||
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 List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
||||
private bool _guardianDropAllowed;
|
||||
|
|
@ -690,9 +691,11 @@ namespace Vanguard.Board3D
|
|||
Stats = "P" + card.Power + " C" + card.Critical,
|
||||
StatsModified = card.Power != card.BasePower
|
||||
|| card.Critical != card.BaseCritical,
|
||||
Highlight = state.Highlights.Contains(key)
|
||||
? _config.highlightColor
|
||||
: (Color?)null,
|
||||
Highlight = state.SelectedHighlights.Contains(key)
|
||||
? _config.selectedColor
|
||||
: 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
|
||||
? view.Battle.GuardCards ?? 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)
|
||||
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
|
||||
{
|
||||
Key = "G:" + player.Index + ":" + card.InstanceId,
|
||||
|
|
@ -727,27 +738,6 @@ namespace Vanguard.Board3D
|
|||
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(
|
||||
|
|
@ -843,7 +833,8 @@ namespace Vanguard.Board3D
|
|||
Color? highlight = null;
|
||||
if (state.SelectedHandInstanceIds.Contains(card.InstanceId))
|
||||
highlight = _config.selectedColor;
|
||||
else if (state.MulliganSelecting)
|
||||
else if (state.AvailableHandInstanceIds.Contains(card.InstanceId)
|
||||
|| state.MulliganSelecting)
|
||||
highlight = _config.highlightColor;
|
||||
|
||||
specs.Add(new CardSpec
|
||||
|
|
@ -1025,6 +1016,10 @@ namespace Vanguard.Board3D
|
|||
foreach (var key in state.Highlights)
|
||||
_activeHighlights.Add(key);
|
||||
|
||||
_selectedHighlights.Clear();
|
||||
foreach (var key in state.SelectedHighlights)
|
||||
_selectedHighlights.Add(key);
|
||||
|
||||
_validDropSlots.Clear();
|
||||
foreach (var key in state.DropTargets)
|
||||
_validDropSlots.Add(key);
|
||||
|
|
@ -1037,9 +1032,13 @@ namespace Vanguard.Board3D
|
|||
{
|
||||
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));
|
||||
slot.SetHighlighted(highlighted);
|
||||
slot.SetHighlighted(
|
||||
highlighted,
|
||||
selected ? _config.selectedColor : _config.highlightColor);
|
||||
}
|
||||
|
||||
if (_guardianZone != null)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,13 @@ namespace Vanguard.UI
|
|||
private Action _discardCancelAction;
|
||||
private int _pendingBoosterSlot = -1;
|
||||
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 readonly HashSet<int> _mulliganSelected = new HashSet<int>();
|
||||
private bool _mulliganUsed;
|
||||
|
|
@ -285,6 +292,8 @@ namespace Vanguard.UI
|
|||
_abilityDamageIndex = -1;
|
||||
_pendingBoosterSlot = -1;
|
||||
_pendingBoostAttackerSlot = -2;
|
||||
ClearGuardSelection();
|
||||
ClearAbilityCostSelection();
|
||||
_triggerPowerSlot = null;
|
||||
_showAllIntents = false;
|
||||
_autoFinishMulligan = false;
|
||||
|
|
@ -747,6 +756,8 @@ namespace Vanguard.UI
|
|||
_autoFinishMulliganAt = 0f;
|
||||
}
|
||||
FinishDiscardSelection();
|
||||
PruneGuardSelection();
|
||||
PruneAbilityCostSelection();
|
||||
_triggerPowerSlot = null;
|
||||
PrepareTriggerSelection();
|
||||
if (_view.Clock != null)
|
||||
|
|
@ -1088,6 +1099,16 @@ namespace Vanguard.UI
|
|||
if (_discardChoiceActive)
|
||||
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 (intent is ActivateAbilityIntent activate)
|
||||
|
|
@ -1660,9 +1681,13 @@ namespace Vanguard.UI
|
|||
var state = new BoardRenderState
|
||||
{
|
||||
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(),
|
||||
AbilityDamageIndex = _abilityDamageIndex,
|
||||
AbilityDamageIndex = IsAbilityDamageTargeting()
|
||||
? _abilityDamageIndex
|
||||
: -1,
|
||||
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())
|
||||
{
|
||||
var hand = _view.Players
|
||||
|
|
@ -1769,15 +1852,17 @@ namespace Vanguard.UI
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You);
|
||||
if (self == null || self.FaceUpDamage.Count == 0)
|
||||
return false;
|
||||
|
||||
return (_view.LegalIntents ?? new List<IntentPayload>())
|
||||
.Any(intent => intent is ActivateAbilityIntent or ResolveAbilityIntent);
|
||||
return self != null && self.FaceUpDamage.Count > 0;
|
||||
}
|
||||
|
||||
private void OnBoard3DCardClicked(BoardCardRef card)
|
||||
|
|
@ -1800,6 +1885,20 @@ namespace Vanguard.UI
|
|||
if (_inputLock.IsLocked)
|
||||
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
|
||||
|| IsMulliganSelecting()
|
||||
|| 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)
|
||||
{ if (_view == null || _board3D == null)
|
||||
return;
|
||||
|
|
@ -2140,7 +2601,7 @@ namespace Vanguard.UI
|
|||
.ToList();
|
||||
if (discardVariants.Count == 0)
|
||||
{
|
||||
OnIntentClicked(free ?? distinct[0]);
|
||||
ActivateAbility(free ?? distinct[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2152,7 +2613,7 @@ namespace Vanguard.UI
|
|||
{
|
||||
new CardActionMenuItem(
|
||||
"Без сброса",
|
||||
() => OnIntentClicked(free)),
|
||||
() => ActivateAbility(free)),
|
||||
new CardActionMenuItem(
|
||||
"Сбросить карту в руке",
|
||||
() => StartDiscardSelection(
|
||||
|
|
@ -2209,8 +2670,12 @@ namespace Vanguard.UI
|
|||
item => item.DiscardPrintingId == printing);
|
||||
if (ability != null)
|
||||
{
|
||||
var discardInstanceId = card.InstanceId;
|
||||
FinishDiscardSelection();
|
||||
OnIntentClicked(ability);
|
||||
_pendingAbilityCost = ability;
|
||||
_abilityCostDiscardInstanceId = discardInstanceId;
|
||||
_abilityDamageIndex = -1;
|
||||
Render();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2218,7 +2683,16 @@ namespace Vanguard.UI
|
|||
item => item.DiscardPrintingId == printing);
|
||||
if (guard != null)
|
||||
{
|
||||
var guardInstanceId = _guardDiscardInstanceId;
|
||||
FinishDiscardSelection();
|
||||
if (guardInstanceId != 0)
|
||||
{
|
||||
_guardDiscardInstanceId = 0;
|
||||
_guardDiscardPrintingId = null;
|
||||
AddGuardSelection(guardInstanceId, guard);
|
||||
return;
|
||||
}
|
||||
|
||||
OnIntentClicked(guard);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2253,6 +2727,8 @@ namespace Vanguard.UI
|
|||
_discardGuardChoices.Clear();
|
||||
_discardCancelAction = null;
|
||||
_discardPrompt = null;
|
||||
_guardDiscardInstanceId = 0;
|
||||
_guardDiscardPrintingId = null;
|
||||
}
|
||||
|
||||
private void CancelDiscardSelection()
|
||||
|
|
@ -2354,6 +2830,14 @@ namespace Vanguard.UI
|
|||
if (_view == null || _sending || _inputLock.IsLocked)
|
||||
return;
|
||||
|
||||
if (IsGuardStep()
|
||||
&& target.Zone == BoardZone.Guardian
|
||||
&& (source.Zone == BoardZone.Hand || source.IsField))
|
||||
{
|
||||
ToggleGuardFromDrop(source);
|
||||
return;
|
||||
}
|
||||
|
||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||
IntentPayload intent = null;
|
||||
|
||||
|
|
@ -2486,6 +2970,30 @@ namespace Vanguard.UI
|
|||
if (_view == null || _inputLock.IsLocked)
|
||||
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)
|
||||
OpenZone(BoardZone.Soul, card.Player);
|
||||
}
|
||||
|
|
@ -2566,6 +3074,15 @@ namespace Vanguard.UI
|
|||
? "Выберите карту в руке для сброса."
|
||||
: _discardPrompt;
|
||||
}
|
||||
else if (_pendingAbilityCost != null)
|
||||
{
|
||||
title = "ОПЛАТА";
|
||||
hint = RequiresCounterBlast(_pendingAbilityCost)
|
||||
? _abilityDamageIndex < 0
|
||||
? "Выберите карту урона для Counter Blast."
|
||||
: "Оплата выбрана. Подтвердите в панели действий."
|
||||
: "Оплата выбрана. Подтвердите в панели действий.";
|
||||
}
|
||||
else if (_pendingBoosterSlot >= 0 && _pendingIntents.Count > 0)
|
||||
{
|
||||
title = "ПОДДЕРЖКА";
|
||||
|
|
@ -2618,7 +3135,12 @@ namespace Vanguard.UI
|
|||
else if (_view.Step == GameStep.Guard)
|
||||
{
|
||||
title = "ЗАЩИТА";
|
||||
hint = "Выберите защиту или не защищайтесь.";
|
||||
var guardCount = _guardSelected.Count
|
||||
+ (_guardSelectedInterceptSlot >= 0 ? 1 : 0);
|
||||
hint = guardCount == 0
|
||||
? "Кликните карты в руке или на поле для защиты."
|
||||
: "Выбрано карт: " + guardCount
|
||||
+ ". Щит: +" + GuardSelectionShield() + ".";
|
||||
}
|
||||
else if (_view.Phase == GamePhase.Battle)
|
||||
{
|
||||
|
|
@ -2661,7 +3183,7 @@ namespace Vanguard.UI
|
|||
_phasePanel.anchorMax = new Vector2(1f, 0.5f);
|
||||
_phasePanel.pivot = new Vector2(1f, 0.5f);
|
||||
_phasePanel.anchoredPosition = new Vector2(-468f, 0f);
|
||||
_phasePanel.sizeDelta = new Vector2(156f, 188f);
|
||||
_phasePanel.sizeDelta = new Vector2(190f, 220f);
|
||||
UiKit.AddVerticalLayout(_phasePanel, 8, 6);
|
||||
_phasePanel.gameObject.SetActive(false);
|
||||
}
|
||||
|
|
@ -2672,12 +3194,16 @@ namespace Vanguard.UI
|
|||
return;
|
||||
|
||||
ClearChildren(_phasePanel);
|
||||
if (view == null || _pendingIntents.Count > 0)
|
||||
if (view == null
|
||||
|| _pendingIntents.Count > 0
|
||||
|| _discardChoiceActive)
|
||||
{
|
||||
_phasePanel.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var isGuardStep = IsGuardStep();
|
||||
var isAbilityCost = _pendingAbilityCost != null;
|
||||
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||
.Where(IsPhaseIntent)
|
||||
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
||||
|
|
@ -2685,7 +3211,7 @@ namespace Vanguard.UI
|
|||
// inactive finish button after the exchange is misleading.
|
||||
.Where(intent => !(intent is FinishMulliganIntent && _mulliganUsed))
|
||||
.ToList();
|
||||
if (intents.Count == 0)
|
||||
if (intents.Count == 0 && !isGuardStep && !isAbilityCost)
|
||||
{
|
||||
_phasePanel.gameObject.SetActive(false);
|
||||
return;
|
||||
|
|
@ -2695,12 +3221,20 @@ namespace Vanguard.UI
|
|||
var title = UiKit.CreateText(
|
||||
_phasePanel,
|
||||
"PhaseTitle",
|
||||
"ФАЗЫ",
|
||||
"ДЕЙСТВИЯ",
|
||||
14,
|
||||
new Color(0.72f, 0.80f, 0.94f));
|
||||
title.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(title, 22f);
|
||||
|
||||
if (isGuardStep)
|
||||
AddGuardPhaseButton();
|
||||
if (isAbilityCost)
|
||||
{
|
||||
AddAbilityCostPhaseButton();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var ride in (view.LegalIntents ?? new List<IntentPayload>())
|
||||
.OfType<RideIntent>()
|
||||
.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) =>
|
||||
intent is FinishMulliganIntent or SkipRideIntent
|
||||
or EnterBattleIntent or EndTurnIntent;
|
||||
|
|
@ -2838,7 +3411,7 @@ namespace Vanguard.UI
|
|||
}
|
||||
|
||||
var attackPower = attacker.Power;
|
||||
var shield = defender.Power + battle.GuardShield;
|
||||
var shield = defender.Power + battle.GuardShield + GuardSelectionShield();
|
||||
var text = battle.Nullified
|
||||
? "POWER " + attackPower + " · NULLIFIED"
|
||||
: "POWER " + attackPower + " vs SHIELD " + shield;
|
||||
|
|
@ -3073,7 +3646,10 @@ namespace Vanguard.UI
|
|||
if (sources.Count == 1)
|
||||
{
|
||||
ClearSelection(rebuild: false);
|
||||
OnIntentClicked(sources[0]);
|
||||
if (sources[0] is ActivateAbilityIntent ability)
|
||||
ActivateAbility(ability);
|
||||
else
|
||||
OnIntentClicked(sources[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -3106,7 +3682,10 @@ namespace Vanguard.UI
|
|||
if (withTargets.Count == 0)
|
||||
{
|
||||
ClearSelection(rebuild: false);
|
||||
OnIntentClicked(sources[0]);
|
||||
if (sources[0] is ActivateAbilityIntent ability)
|
||||
ActivateAbility(ability);
|
||||
else
|
||||
OnIntentClicked(sources[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -3354,7 +3933,6 @@ namespace Vanguard.UI
|
|||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
||||
var guards = playable.OfType<GuardIntent>().ToList();
|
||||
var rides = playable.OfType<RideIntent>().ToList();
|
||||
var board = playable
|
||||
.Where(intent => IsBoardIntent(intent)
|
||||
|
|
@ -3365,6 +3943,7 @@ namespace Vanguard.UI
|
|||
var global = playable
|
||||
.Where(intent => !IsBoardIntent(intent)
|
||||
&& !IsPhaseIntent(intent)
|
||||
&& intent is not PassGuardIntent
|
||||
&& intent is not ActivateAbilityIntent
|
||||
&& intent is not ResolveAbilityIntent
|
||||
&& !(intent is ChooseIntent
|
||||
|
|
@ -3464,12 +4043,9 @@ namespace Vanguard.UI
|
|||
UiKit.SetPreferredHeight(toggle, 38f);
|
||||
}
|
||||
|
||||
if (abilities.Count > 0)
|
||||
if (abilities.Count > 0 && _pendingAbilityCost == null)
|
||||
AddAbilityButton(abilities);
|
||||
|
||||
if (guards.Count > 0)
|
||||
AddGuardButton(guards);
|
||||
|
||||
if (rides.Count > 0)
|
||||
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]
|
||||
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]
|
||||
public void Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
# Передача сессии
|
||||
|
||||
Состояние проекта на **22 октября 2026**. Документ нужен, чтобы новая сессия
|
||||
Состояние проекта на **23 октября 2026**. Документ нужен, чтобы новая сессия
|
||||
продолжила работу без повторного разбора истории. Краткая оперативная
|
||||
сводка — `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. Проект
|
||||
|
||||
Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**.
|
||||
|
|
|
|||
|
|
@ -87,13 +87,21 @@
|
|||
`P/C`.
|
||||
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
||||
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
||||
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Фазы».
|
||||
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Действия».
|
||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||
нескольких вариантах открывается overlay выбора способности. Карта для
|
||||
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
||||
Perfect Guard также просит выбрать карту сброса в руке.
|
||||
- В фазе защиты доступны «Защититься» (если есть допустимые карты) и
|
||||
«Не защищаться»; карта защиты и карта сброса выбираются в руке.
|
||||
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
||||
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
||||
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
||||
отменяет выбор.
|
||||
- Защита: в панели «Действия» одна кнопка — «Не защищаться» без выбора и
|
||||
«Защищаться» с выбранными картами. Карты защиты и перехвата выбираются
|
||||
кликом/драгом по объектам; допустимые подсвечиваются, выбранные — зелёным.
|
||||
Щит выбранных карт сразу учитывается в счётчике; после подтверждения карты
|
||||
уходят в зону защиты, шаг защиты завершается. Дубли карт в Guardian
|
||||
устранены.
|
||||
- Локальный standalone работает в фоне (`runInBackground`), чтобы heartbeat
|
||||
WebSocket не останавливался при потере фокуса.
|
||||
- При потере соединения весь бой блокируется полноэкранным overlay
|
||||
|
|
@ -102,9 +110,9 @@
|
|||
Кнопка внутри overlay позволяет выйти в меню.
|
||||
- Правую панель действий можно временно скрыть кнопкой «Скрыть панель»
|
||||
в верхней строке; поле остаётся видимым, вернуть — «Показать панель».
|
||||
При скрытии панель фаз и блоки противника сдвигаются вправо, а рука
|
||||
При скрытии панель действий и блоки противника сдвигаются вправо, а рука
|
||||
центрируется по горизонтали; при показе возвращается влево.
|
||||
- Кнопка муллигана находится в панели «Фазы» и динамическая: без выбора —
|
||||
- Кнопка муллигана находится в панели «Действия» и динамическая: без выбора —
|
||||
«Закончить муллиган», с выбором — «Заменить X карт».
|
||||
- Trigger-зона у противника расположена над его колодой по направлению
|
||||
к центру стола (зеркально своей), а не за дальним краем.
|
||||
|
|
@ -115,7 +123,7 @@
|
|||
только закрытые карты. Душа — юниты под VC: отдельной стопки нет,
|
||||
ПКМ по VC открывает список. Дроп по умолчанию пуст; сверху показывает
|
||||
последнюю карту, ЛКМ — список всего дропа. Колода и дроп поменяны
|
||||
местами (колода ближе к игроку). Справа от круга защиты — панель фаз:
|
||||
местами (колода ближе к игроку). Справа от круга защиты — панель действий:
|
||||
конец муллигана (авто после обмена), скип смены, конец главной/боевой;
|
||||
боевая завершается сама, если нет атак и активаций. Рука противника —
|
||||
веер закрытых карт у дальнего края стола, только рубашки.
|
||||
|
|
@ -229,13 +237,14 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **36/36** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **38/38** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
||||
стрелка атаки, prefab-оверлеи, раскладка на 1920×1080, авто-завершение
|
||||
муллигана без кнопки после обмена), `SCENE_SMOKE
|
||||
муллигана без кнопки после обмена, одна кнопка защиты в панели действий,
|
||||
отсутствие дублей карт в Guardian), `SCENE_SMOKE
|
||||
success=True`, `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
|
|
|
|||
Loading…
Reference in New Issue