Add battle targeting, action groups, and event log
CI / test (push) Successful in 9m52s
Details
CI / test (push) Successful in 9m52s
Details
This commit is contained in:
parent
0e528586b4
commit
0e4737ac22
|
|
@ -276,7 +276,8 @@
|
||||||
- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер,
|
- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер,
|
||||||
сила/крит; REST/скрытая карта.
|
сила/крит; REST/скрытая карта.
|
||||||
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
||||||
- [x] Ввод: кнопки легальных интентов; подсветка целей — дальше.
|
- [x] Ввод: кнопки легальных интентов; клик по картам/зонам,
|
||||||
|
подсветка целей, группировка действий и журнал боя.
|
||||||
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
||||||
- [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard.
|
- [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard.
|
||||||
- [ ] Скип/ускорение анимаций.
|
- [ ] Скип/ускорение анимаций.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ namespace Vanguard.UI
|
||||||
private readonly Text _concedeLabel;
|
private readonly Text _concedeLabel;
|
||||||
private readonly Button _reconnect;
|
private readonly Button _reconnect;
|
||||||
private readonly RectTransform _result;
|
private readonly RectTransform _result;
|
||||||
|
private RectTransform _logContent;
|
||||||
private Text _resultText;
|
private Text _resultText;
|
||||||
|
private readonly List<string> _logLines = new List<string>();
|
||||||
|
|
||||||
private GameConnection _connection;
|
private GameConnection _connection;
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource _cts;
|
||||||
|
|
@ -52,9 +54,12 @@ namespace Vanguard.UI
|
||||||
private bool _snapshotPending;
|
private bool _snapshotPending;
|
||||||
private bool _finished;
|
private bool _finished;
|
||||||
private bool _confirmConcede;
|
private bool _confirmConcede;
|
||||||
|
private bool _showAllIntents;
|
||||||
private float _confirmDeadline;
|
private float _confirmDeadline;
|
||||||
private long _clockServerMs;
|
private long _clockServerMs;
|
||||||
private long _clockReceivedMs;
|
private long _clockReceivedMs;
|
||||||
|
private readonly List<IntentPayload> _pendingIntents = new List<IntentPayload>();
|
||||||
|
private readonly HashSet<string> _highlightKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||||||
{
|
{
|
||||||
|
|
@ -116,7 +121,17 @@ namespace Vanguard.UI
|
||||||
_status = UiKit.CreateText(
|
_status = UiKit.CreateText(
|
||||||
right, "Status", string.Empty, 16, Muted, TextAnchor.UpperLeft);
|
right, "Status", string.Empty, 16, Muted, TextAnchor.UpperLeft);
|
||||||
_status.horizontalOverflow = HorizontalWrapMode.Wrap;
|
_status.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
UiKit.SetPreferredHeight(_status, 110f);
|
UiKit.SetPreferredHeight(_status, 74f);
|
||||||
|
|
||||||
|
var logTitle = UiKit.CreateText(
|
||||||
|
right, "LogTitle", "Журнал", 16, new Color(0.72f, 0.80f, 0.94f), TextAnchor.MiddleLeft);
|
||||||
|
logTitle.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(logTitle, 22f);
|
||||||
|
|
||||||
|
var logScroll = UiKit.CreateScrollView(right, "LogScroll", out _logContent, 3f);
|
||||||
|
var logElement = logScroll.gameObject.AddComponent<LayoutElement>();
|
||||||
|
logElement.preferredHeight = 142f;
|
||||||
|
logElement.minHeight = 80f;
|
||||||
|
|
||||||
_reconnect = UiKit.CreateButton(right, "Reconnect", "Переподключиться", OnReconnectClicked, 18);
|
_reconnect = UiKit.CreateButton(right, "Reconnect", "Переподключиться", OnReconnectClicked, 18);
|
||||||
UiKit.SetPreferredHeight(_reconnect, 46f);
|
UiKit.SetPreferredHeight(_reconnect, 46f);
|
||||||
|
|
@ -138,6 +153,11 @@ namespace Vanguard.UI
|
||||||
_concede.interactable = true;
|
_concede.interactable = true;
|
||||||
_reconnect.gameObject.SetActive(false);
|
_reconnect.gameObject.SetActive(false);
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_highlightKeys.Clear();
|
||||||
|
_showAllIntents = false;
|
||||||
|
_logLines.Clear();
|
||||||
|
RebuildLog();
|
||||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||||
SetStatus("Подключаемся к матчу…", Muted);
|
SetStatus("Подключаемся к матчу…", Muted);
|
||||||
_root.gameObject.SetActive(true);
|
_root.gameObject.SetActive(true);
|
||||||
|
|
@ -286,6 +306,8 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
_view = snapshot.View;
|
_view = snapshot.View;
|
||||||
_finished = _view.IsFinished;
|
_finished = _view.IsFinished;
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_highlightKeys.Clear();
|
||||||
if (_view.Clock != null)
|
if (_view.Clock != null)
|
||||||
{
|
{
|
||||||
_clockServerMs = _view.Clock.ServerTimeMs;
|
_clockServerMs = _view.Clock.ServerTimeMs;
|
||||||
|
|
@ -297,9 +319,78 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnEvents(ServerEvents events)
|
private void OnEvents(ServerEvents events)
|
||||||
{
|
{
|
||||||
|
if (events?.Events != null)
|
||||||
|
{
|
||||||
|
foreach (var envelope in events.Events)
|
||||||
|
{
|
||||||
|
var line = Describe(envelope.Event);
|
||||||
|
if (!string.IsNullOrWhiteSpace(line))
|
||||||
|
_logLines.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_logLines.Count > 40)
|
||||||
|
_logLines.RemoveAt(0);
|
||||||
|
|
||||||
|
RebuildLog();
|
||||||
|
}
|
||||||
|
|
||||||
_ = RequestSnapshotAsync();
|
_ = RequestSnapshotAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RebuildLog()
|
||||||
|
{
|
||||||
|
if (_logContent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClearChildren(_logContent);
|
||||||
|
var start = Mathf.Max(0, _logLines.Count - 8);
|
||||||
|
for (var i = start; i < _logLines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = UiKit.CreateText(
|
||||||
|
_logContent, "LogLine", _logLines[i], 13,
|
||||||
|
i == _logLines.Count - 1
|
||||||
|
? new Color(0.92f, 0.94f, 1f)
|
||||||
|
: new Color(0.60f, 0.68f, 0.82f),
|
||||||
|
TextAnchor.MiddleLeft);
|
||||||
|
line.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
UiKit.SetPreferredHeight(line, 30f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Describe(EventView view)
|
||||||
|
{
|
||||||
|
if (view == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return view switch
|
||||||
|
{
|
||||||
|
UnitRodeView ride => "Ride: " + CardName(ride.PrintingId),
|
||||||
|
UnitCalledView call => "Call: " + CardName(call.PrintingId) + " → " + SlotName(call.Slot),
|
||||||
|
UnitMovedView move => "Move: " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot),
|
||||||
|
AttackDeclaredView attack => "Атака: " + SlotName(attack.AttackerSlot)
|
||||||
|
+ " → " + SlotName(attack.TargetSlot),
|
||||||
|
GuardAddedView guard => "Guard: " + CardName(guard.PrintingId)
|
||||||
|
+ " (+" + guard.Shield + ")",
|
||||||
|
PerfectGuardView perfect => "Perfect Guard: " + CardName(perfect.SentinelPrintingId),
|
||||||
|
DriveCheckedView drive => "Drive: " + CardName(drive.PrintingId)
|
||||||
|
+ " · " + drive.Trigger,
|
||||||
|
TriggerResolvedView trigger => "Триггер: " + CardName(trigger.PrintingId),
|
||||||
|
BattleResolvedView battle => battle.Hit ? "Атака прошла." : "Атака отбита.",
|
||||||
|
DamageTakenView damage => "Урон: " + PlayerName(damage.Player)
|
||||||
|
+ " +" + damage.Amount + " (всего " + damage.Total + ")",
|
||||||
|
HealedView heal => "Heal: " + PlayerName(heal.Player),
|
||||||
|
UnitRetiredView retire => "Retire: " + SlotName(retire.Slot),
|
||||||
|
InterceptedView intercept => "Intercept: " + CardName(intercept.PrintingId),
|
||||||
|
CardDrawnView draw => "Карта: " + PlayerName(draw.Player),
|
||||||
|
PhaseChangedView phase => "Фаза: " + phase.Phase,
|
||||||
|
GameFinishedView finish => "Партия завершена.",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string PlayerName(int player) =>
|
||||||
|
_view == null ? "P" + player : player == _view.You ? "Вы" : "Противник";
|
||||||
|
|
||||||
private void OnClock(ServerClock clock)
|
private void OnClock(ServerClock clock)
|
||||||
{
|
{
|
||||||
if (!_root.gameObject.activeSelf || clock?.Clock == null)
|
if (!_root.gameObject.activeSelf || clock?.Clock == null)
|
||||||
|
|
@ -358,6 +449,8 @@ namespace Vanguard.UI
|
||||||
if (_sending || _connection == null || _cts == null || _view == null)
|
if (_sending || _connection == null || _cts == null || _view == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_highlightKeys.Clear();
|
||||||
_sending = true;
|
_sending = true;
|
||||||
SetStatus("Отправляем: " + Label(intent), Muted);
|
SetStatus("Отправляем: " + Label(intent), Muted);
|
||||||
|
|
||||||
|
|
@ -504,13 +597,13 @@ namespace Vanguard.UI
|
||||||
rowElement.minHeight = 166f;
|
rowElement.minHeight = 166f;
|
||||||
UiKit.AddHorizontalLayout(row, 3, 7);
|
UiKit.AddHorizontalLayout(row, 3, 7);
|
||||||
|
|
||||||
AddCardBox(row, player.Vanguard, "V", compact: false);
|
AddCardBox(row, player.Vanguard, "V", compact: false, player.Index, -1);
|
||||||
for (var i = 0; i < 5; i++)
|
for (var i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
var card = player.RearGuards != null && i < player.RearGuards.Length
|
var card = player.RearGuards != null && i < player.RearGuards.Length
|
||||||
? player.RearGuards[i]
|
? player.RearGuards[i]
|
||||||
: null;
|
: null;
|
||||||
AddCardBox(row, card, "R" + (i + 1), compact: false);
|
AddCardBox(row, card, "R" + (i + 1), compact: false, player.Index, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,7 +681,7 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var card in self.Hand)
|
foreach (var card in self.Hand)
|
||||||
AddCardBox(content, card, "Рука", compact: true);
|
AddCardBox(content, card, "Рука", compact: true, self.Index, -2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddInfoLine(Transform parent, string text)
|
private void AddInfoLine(Transform parent, string text)
|
||||||
|
|
@ -598,7 +691,13 @@ namespace Vanguard.UI
|
||||||
UiKit.SetPreferredHeight(line, 22f);
|
UiKit.SetPreferredHeight(line, 22f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddCardBox(Transform parent, CardView card, string slot, bool compact)
|
private void AddCardBox(
|
||||||
|
Transform parent,
|
||||||
|
CardView card,
|
||||||
|
string slot,
|
||||||
|
bool compact,
|
||||||
|
int playerIndex,
|
||||||
|
int slotIndex)
|
||||||
{
|
{
|
||||||
var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found)
|
var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found)
|
||||||
? found
|
? found
|
||||||
|
|
@ -619,9 +718,19 @@ namespace Vanguard.UI
|
||||||
element.preferredHeight = compact ? 142f : 162f;
|
element.preferredHeight = compact ? 142f : 162f;
|
||||||
element.flexibleWidth = 0f;
|
element.flexibleWidth = 0f;
|
||||||
|
|
||||||
|
var highlighted = _highlightKeys.Contains(
|
||||||
|
TargetKey(playerIndex, slotIndex, card != null ? card.PrintingId : null));
|
||||||
|
var button = panel.gameObject.AddComponent<Button>();
|
||||||
|
button.targetGraphic = panel.GetComponent<Image>();
|
||||||
|
button.onClick.AddListener(() => OnBoardTargetClicked(new BoardTarget(
|
||||||
|
playerIndex, slotIndex, card != null ? card.PrintingId : null,
|
||||||
|
card != null ? card.InstanceId : 0, slotIndex == -2)));
|
||||||
|
|
||||||
var outline = panel.gameObject.AddComponent<Outline>();
|
var outline = panel.gameObject.AddComponent<Outline>();
|
||||||
outline.effectColor = frameColor;
|
outline.effectColor = highlighted ? new Color(1f, 0.82f, 0.28f) : frameColor;
|
||||||
outline.effectDistance = new Vector2(2f, -2f);
|
outline.effectDistance = highlighted
|
||||||
|
? new Vector2(3f, -3f)
|
||||||
|
: new Vector2(2f, -2f);
|
||||||
UiKit.AddVerticalLayout(panel, 4, 2);
|
UiKit.AddVerticalLayout(panel, 4, 2);
|
||||||
|
|
||||||
var slotText = UiKit.CreateText(
|
var slotText = UiKit.CreateText(
|
||||||
|
|
@ -734,12 +843,233 @@ namespace Vanguard.UI
|
||||||
private string CardName(string printingId) =>
|
private string CardName(string printingId) =>
|
||||||
_catalog.TryGetValue(printingId, out var meta) ? meta.NameEn : printingId;
|
_catalog.TryGetValue(printingId, out var meta) ? meta.NameEn : printingId;
|
||||||
|
|
||||||
|
private void OnBoardTargetClicked(BoardTarget target)
|
||||||
|
{
|
||||||
|
if (_view == null || _sending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
||||||
|
if (_pendingIntents.Count > 0)
|
||||||
|
{
|
||||||
|
var matches = _pendingIntents
|
||||||
|
.Where(intent => TryGetTarget(intent, out var candidate) && candidate.Matches(target))
|
||||||
|
.OrderBy(intent => intent is AttackIntent attack && attack.BoosterSlot >= 0 ? 1 : 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (matches.Count > 0)
|
||||||
|
{
|
||||||
|
ClearSelection(rebuild: false);
|
||||||
|
OnIntentClicked(matches[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sources = legal
|
||||||
|
.Where(intent => TryGetSource(intent, out var source) && source.Matches(target))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (sources.Count == 0)
|
||||||
|
{
|
||||||
|
SetStatus("Для этой карты сейчас нет доступного действия.", Muted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sources.Count == 1)
|
||||||
|
{
|
||||||
|
ClearSelection(rebuild: false);
|
||||||
|
OnIntentClicked(sources[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var withTargets = sources
|
||||||
|
.Where(intent => TryGetTarget(intent, out _))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (withTargets.Count == 0)
|
||||||
|
{
|
||||||
|
ClearSelection(rebuild: false);
|
||||||
|
OnIntentClicked(sources[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_pendingIntents.AddRange(withTargets);
|
||||||
|
RebuildHighlights();
|
||||||
|
RebuildIntents(_view);
|
||||||
|
SetStatus("Выберите цель на поле.", new Color(0.98f, 0.82f, 0.42f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSelection(bool rebuild = true)
|
||||||
|
{
|
||||||
|
_pendingIntents.Clear();
|
||||||
|
_highlightKeys.Clear();
|
||||||
|
if (rebuild && _view != null)
|
||||||
|
RebuildIntents(_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildHighlights()
|
||||||
|
{
|
||||||
|
_highlightKeys.Clear();
|
||||||
|
foreach (var intent in _pendingIntents)
|
||||||
|
{
|
||||||
|
if (TryGetTarget(intent, out var target))
|
||||||
|
_highlightKeys.Add(TargetKey(target.Player, target.Slot, target.PrintingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetSource(IntentPayload intent, out BoardTarget source)
|
||||||
|
{
|
||||||
|
source = default;
|
||||||
|
if (_view == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (intent)
|
||||||
|
{
|
||||||
|
case RideIntent ride:
|
||||||
|
source = new BoardTarget(_view.You, -2, ride.PrintingId, 0, true);
|
||||||
|
return true;
|
||||||
|
case CallIntent call:
|
||||||
|
source = new BoardTarget(_view.You, -2, call.PrintingId, 0, true);
|
||||||
|
return true;
|
||||||
|
case GuardIntent guard:
|
||||||
|
source = new BoardTarget(_view.You, -2, guard.PrintingId, 0, true);
|
||||||
|
return true;
|
||||||
|
case MoveIntent move:
|
||||||
|
source = FieldTarget(_view.You, move.FromSlot);
|
||||||
|
return true;
|
||||||
|
case AttackIntent attack:
|
||||||
|
source = FieldTarget(_view.You, attack.AttackerSlot);
|
||||||
|
return true;
|
||||||
|
case InterceptIntent intercept:
|
||||||
|
source = FieldTarget(_view.You, intercept.Slot);
|
||||||
|
return true;
|
||||||
|
case ActivateAbilityIntent activate:
|
||||||
|
source = InstanceTarget(_view.You, activate.InstanceId);
|
||||||
|
return source.InstanceId != 0;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetTarget(IntentPayload intent, out BoardTarget target)
|
||||||
|
{
|
||||||
|
target = default;
|
||||||
|
if (_view == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (intent)
|
||||||
|
{
|
||||||
|
case CallIntent call:
|
||||||
|
target = FieldTarget(_view.You, call.Slot);
|
||||||
|
return true;
|
||||||
|
case MoveIntent move:
|
||||||
|
target = FieldTarget(_view.You, move.ToSlot);
|
||||||
|
return true;
|
||||||
|
case AttackIntent attack:
|
||||||
|
target = FieldTarget(1 - _view.You, attack.TargetSlot);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoardTarget FieldTarget(int player, int slot)
|
||||||
|
{
|
||||||
|
var card = CardAt(player, slot);
|
||||||
|
return new BoardTarget(player, slot, card != null ? card.PrintingId : null,
|
||||||
|
card != null ? card.InstanceId : 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoardTarget InstanceTarget(int player, int instanceId)
|
||||||
|
{
|
||||||
|
var card = InstanceAt(player, instanceId);
|
||||||
|
if (card == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
if (_view != null
|
||||||
|
&& _view.Players.FirstOrDefault(item => item.Index == player)?.Vanguard is { } vanguard
|
||||||
|
&& vanguard.InstanceId == instanceId)
|
||||||
|
{
|
||||||
|
return new BoardTarget(player, -1, card.PrintingId, instanceId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var field = _view?.Players.FirstOrDefault(item => item.Index == player);
|
||||||
|
if (field?.RearGuards != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < field.RearGuards.Length; i++)
|
||||||
|
{
|
||||||
|
if (field.RearGuards[i]?.InstanceId == instanceId)
|
||||||
|
return new BoardTarget(player, i, card.PrintingId, instanceId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardView CardAt(int player, int slot)
|
||||||
|
{
|
||||||
|
var side = _view?.Players.FirstOrDefault(item => item.Index == player);
|
||||||
|
if (side == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (slot == -1)
|
||||||
|
return side.Vanguard;
|
||||||
|
|
||||||
|
return side.RearGuards != null && slot >= 0 && slot < side.RearGuards.Length
|
||||||
|
? side.RearGuards[slot]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardView InstanceAt(int player, int instanceId)
|
||||||
|
{
|
||||||
|
var side = _view?.Players.FirstOrDefault(item => item.Index == player);
|
||||||
|
if (side == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (side.Vanguard?.InstanceId == instanceId)
|
||||||
|
return side.Vanguard;
|
||||||
|
|
||||||
|
return side.RearGuards?.FirstOrDefault(card => card?.InstanceId == instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TargetKey(int player, int slot, string printingId) =>
|
||||||
|
player + ":" + slot + ":" + (printingId ?? string.Empty);
|
||||||
|
|
||||||
|
private static bool IsBoardIntent(IntentPayload intent) =>
|
||||||
|
intent is RideIntent or CallIntent or MoveIntent or AttackIntent
|
||||||
|
or GuardIntent or InterceptIntent or ActivateAbilityIntent;
|
||||||
|
|
||||||
private void RebuildIntents(MatchView view)
|
private void RebuildIntents(MatchView view)
|
||||||
{
|
{
|
||||||
ClearChildren(_intents);
|
ClearChildren(_intents);
|
||||||
|
|
||||||
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 board = playable.Where(IsBoardIntent).ToList();
|
||||||
|
var global = playable.Where(intent => !IsBoardIntent(intent)).ToList();
|
||||||
|
|
||||||
|
if (_pendingIntents.Count > 0)
|
||||||
|
{
|
||||||
|
var prompt = UiKit.CreateText(
|
||||||
|
_intents,
|
||||||
|
"TargetPrompt",
|
||||||
|
"Выберите цель на поле.\nНажмите «Отмена», чтобы снять выбор.",
|
||||||
|
16,
|
||||||
|
new Color(0.98f, 0.82f, 0.42f),
|
||||||
|
TextAnchor.UpperLeft);
|
||||||
|
prompt.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
UiKit.SetPreferredHeight(prompt, 64f);
|
||||||
|
|
||||||
|
var cancel = UiKit.CreateButton(
|
||||||
|
_intents, "CancelTarget", "Отмена", () =>
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
Render();
|
||||||
|
}, 17);
|
||||||
|
UiKit.SetPreferredHeight(cancel, 44f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (playable.Count == 0)
|
if (playable.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -753,17 +1083,51 @@ namespace Vanguard.UI
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var intent in playable)
|
if (board.Count > 0)
|
||||||
{
|
{
|
||||||
var captured = intent;
|
var hint = UiKit.CreateText(
|
||||||
var button = UiKit.CreateButton(
|
|
||||||
_intents,
|
_intents,
|
||||||
"Intent_" + captured.GetType().Name,
|
"BoardHint",
|
||||||
Label(captured),
|
"Нажмите на карту в руке или на поле.\nДопустимые цели подсветятся.",
|
||||||
() => OnIntentClicked(captured),
|
15,
|
||||||
17);
|
new Color(0.72f, 0.80f, 0.94f),
|
||||||
UiKit.SetPreferredHeight(button, 50f);
|
TextAnchor.UpperLeft);
|
||||||
|
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
UiKit.SetPreferredHeight(hint, 52f);
|
||||||
|
|
||||||
|
var toggle = UiKit.CreateButton(
|
||||||
|
_intents,
|
||||||
|
"ToggleAllIntents",
|
||||||
|
_showAllIntents ? "Скрыть все действия" : "Показать все действия",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_showAllIntents = !_showAllIntents;
|
||||||
|
RebuildIntents(_view);
|
||||||
|
},
|
||||||
|
15);
|
||||||
|
UiKit.SetPreferredHeight(toggle, 38f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var intent in global)
|
||||||
|
AddIntentButton(intent);
|
||||||
|
|
||||||
|
if (_showAllIntents)
|
||||||
|
{
|
||||||
|
foreach (var intent in board)
|
||||||
|
AddIntentButton(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddIntentButton(IntentPayload intent)
|
||||||
|
{
|
||||||
|
var captured = intent;
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_intents,
|
||||||
|
"Intent_" + captured.GetType().Name,
|
||||||
|
Label(captured),
|
||||||
|
() => OnIntentClicked(captured),
|
||||||
|
16);
|
||||||
|
UiKit.SetPreferredHeight(button, 46f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowResult(MatchView view)
|
private void ShowResult(MatchView view)
|
||||||
|
|
@ -879,5 +1243,40 @@ namespace Vanguard.UI
|
||||||
_status.text = text ?? string.Empty;
|
_status.text = text ?? string.Empty;
|
||||||
_status.color = color;
|
_status.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly struct BoardTarget
|
||||||
|
{
|
||||||
|
public BoardTarget(
|
||||||
|
int player,
|
||||||
|
int slot,
|
||||||
|
string printingId,
|
||||||
|
int instanceId,
|
||||||
|
bool isHand)
|
||||||
|
{
|
||||||
|
Player = player;
|
||||||
|
Slot = slot;
|
||||||
|
PrintingId = printingId;
|
||||||
|
InstanceId = instanceId;
|
||||||
|
IsHand = isHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Player { get; }
|
||||||
|
public int Slot { get; }
|
||||||
|
public string PrintingId { get; }
|
||||||
|
public int InstanceId { get; }
|
||||||
|
public bool IsHand { get; }
|
||||||
|
|
||||||
|
public bool Matches(BoardTarget other)
|
||||||
|
{
|
||||||
|
if (IsHand || other.IsHand)
|
||||||
|
{
|
||||||
|
return IsHand && other.IsHand
|
||||||
|
&& Player == other.Player
|
||||||
|
&& string.Equals(PrintingId, other.PrintingId, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Player == other.Player && Slot == other.Slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ http://localhost:5080
|
||||||
интенты, ответы на ping, закрытие и переподключение.
|
интенты, ответы на ping, закрытие и переподключение.
|
||||||
- Боевой gray-box: доска, рука, часы, кнопки всех легальных интентов,
|
- Боевой gray-box: доска, рука, часы, кнопки всех легальных интентов,
|
||||||
сдача с подтверждением и экран результата.
|
сдача с подтверждением и экран результата.
|
||||||
|
- Боевой UX: клик по картам/зонам, подсветка целей, сгруппированные
|
||||||
|
действия, журнал событий и подсказки.
|
||||||
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
||||||
триггер, сила/крит, REST и рубашка для скрытых карт.
|
триггер, сила/крит, REST и рубашка для скрытых карт.
|
||||||
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
||||||
|
|
|
||||||
|
|
@ -745,5 +745,7 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
||||||
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
||||||
действий. Коллекция использует те же арты.
|
действий. Коллекция использует те же арты.
|
||||||
|
- Боевой UX: клик по карте/зоне, подсветка целей, отдельная группировка
|
||||||
|
действий, журнал последних событий и подсказки выбора.
|
||||||
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
||||||
live-практика с ботом на стенде.
|
live-практика с ботом на стенде.
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@
|
||||||
- Боевой экран: процедурные карты с арт-градиентом по клану, рамкой,
|
- Боевой экран: процедурные карты с арт-градиентом по клану, рамкой,
|
||||||
именем, рангом, триггером, силой/критом и рубашкой; поля Vanguard +
|
именем, рангом, триггером, силой/критом и рубашкой; поля Vanguard +
|
||||||
5 Арьергардов у обоих, рука, damage/soul/drop, REST/скрытые карты.
|
5 Арьергардов у обоих, рука, damage/soul/drop, REST/скрытые карты.
|
||||||
|
- Взаимодействие в бою: клик по картам/зонам, подсветка целей,
|
||||||
|
группировка действий (карты → цели, глобальные кнопки отдельно),
|
||||||
|
журнал последних событий.
|
||||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||||
- Не готово: реальные арты/модели/анимации, локализация RU/EN, настройки,
|
- Не готово: реальные арты/модели/анимации, локализация RU/EN, настройки,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue